Geomap: implement basic tooltip support (#37318)

Co-authored-by: Bryan Uribe <buribe@hmc.edu>
This commit is contained in:
Ryan McKinley 2021-07-28 18:34:42 -07:00 committed by GitHub
parent 8b80d2256d
commit ced26bc624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 24 deletions

View File

@ -329,7 +329,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
key: '__global_',
filters: {
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
payload.columnIndex = dataIdx;
payload.rowIndex = dataIdx;
if (x < 0 && y < 0) {
payload.point[xScaleUnit] = null;
payload.point[yScaleKey] = null;

View File

@ -1,3 +1,4 @@
import React, { Component } from 'react';
import {
EventBus,
LegacyGraphHoverEvent,
@ -7,8 +8,9 @@ import {
DataHoverPayload,
BusEventWithPayload,
} from '@grafana/data';
import React, { Component } from 'react';
import { Subscription } from 'rxjs';
import { CustomScrollbar } from '@grafana/ui';
import { DataHoverView } from '../geomap/components/DataHoverView';
interface Props {
eventBus: EventBus;
@ -58,12 +60,16 @@ export class CursorView extends Component<Props, State> {
if (!event) {
return <div>no events yet</div>;
}
const { type, payload, origin } = event;
return (
<div>
<h2>Origin: {(event.origin as any)?.path}</h2>
<span>Type: {event.type}</span>
<pre>{JSON.stringify(event.payload.point, null, ' ')}</pre>
</div>
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<h3>Origin: {(origin as any)?.path}</h3>
<span>Type: {type}</span>
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>
{payload.data && (
<DataHoverView data={payload.data} rowIndex={payload.rowIndex} columnIndex={payload.columnIndex} />
)}
</CustomScrollbar>
);
}
}

View File

@ -1,6 +1,6 @@
import React, { Component, ReactNode } from 'react';
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry, defaultBaseLayer } from './layers/registry';
import { Map, View } from 'ol';
import { Map, MapBrowserEvent, View } from 'ol';
import Attribution from 'ol/control/Attribution';
import Zoom from 'ol/control/Zoom';
import ScaleLine from 'ol/control/ScaleLine';
@ -8,19 +8,30 @@ import BaseLayer from 'ol/layer/Base';
import { defaults as interactionDefaults } from 'ol/interaction';
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
import { PanelData, MapLayerHandler, MapLayerOptions, PanelProps, GrafanaTheme } from '@grafana/data';
import {
PanelData,
MapLayerHandler,
MapLayerOptions,
PanelProps,
GrafanaTheme,
DataHoverClearEvent,
DataHoverEvent,
DataFrame,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
import { centerPointRegistry, MapCenterID } from './view';
import { fromLonLat } from 'ol/proj';
import { fromLonLat, toLonLat } from 'ol/proj';
import { Coordinate } from 'ol/coordinate';
import { css } from '@emotion/css';
import { stylesFactory } from '@grafana/ui';
import { Portal, stylesFactory, VizTooltipContainer } from '@grafana/ui';
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
import { DebugOverlay } from './components/DebugOverlay';
import { getGlobalStyles } from './globalStyles';
import { Global } from '@emotion/react';
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
import { DataHoverView } from './components/DataHoverView';
interface MapLayerState {
config: MapLayerOptions;
@ -33,7 +44,10 @@ let sharedView: View | undefined = undefined;
export let lastGeomapPanelInstance: GeomapPanel | undefined = undefined;
type Props = PanelProps<GeomapPanelOptions>;
interface State extends OverlayProps {}
interface State extends OverlayProps {
ttip?: GeomapHoverPayload;
}
export class GeomapPanel extends Component<Props, State> {
globalCSS = getGlobalStyles(config.theme2);
@ -43,10 +57,14 @@ export class GeomapPanel extends Component<Props, State> {
layers: MapLayerState[] = [];
mouseWheelZoom?: MouseWheelZoom;
style = getStyles(config.theme);
hoverPayload: GeomapHoverPayload = { point: {}, pageX: -1, pageY: -1 };
readonly hoverEvent = new DataHoverEvent(this.hoverPayload);
constructor(props: Props) {
super(props);
this.state = {};
}
componentDidMount() {
lastGeomapPanelInstance = this;
}
@ -144,6 +162,53 @@ export class GeomapPanel extends Component<Props, State> {
this.initBasemap(options.basemap);
await this.initLayers(options.layers);
this.forceUpdate(); // first render
// Tooltip listener
this.map.on('pointermove', this.pointerMoveListener);
this.map.getViewport().addEventListener('mouseout', (evt) => {
this.props.eventBus.publish(new DataHoverClearEvent({ point: {} }));
});
};
pointerMoveListener = (evt: MapBrowserEvent) => {
if (!this.map) {
return;
}
const mouse = evt.originalEvent as any;
const pixel = this.map.getEventPixel(mouse);
const hover = toLonLat(this.map.getCoordinateFromPixel(pixel));
const { hoverPayload } = this;
hoverPayload.pageX = mouse.pageX;
hoverPayload.pageY = mouse.pageY;
hoverPayload.point = {
lat: hover[1],
lon: hover[0],
};
hoverPayload.data = undefined;
hoverPayload.columnIndex = undefined;
hoverPayload.rowIndex = undefined;
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
const features: GeomapHoverFeature[] = [];
this.map.forEachFeatureAtPixel(pixel, (feature, layer, geo) => {
if (!hoverPayload.data) {
const props = feature.getProperties();
const frame = props['frame'];
if (frame) {
hoverPayload.data = ttip.data = frame as DataFrame;
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
}
}
features.push({ feature, layer, geo });
});
this.hoverPayload.features = features.length ? features : undefined;
this.props.eventBus.publish(this.hoverEvent);
const currentTTip = this.state.ttip;
if (ttip.data !== currentTTip?.data || ttip.rowIndex !== currentTTip?.rowIndex) {
this.setState({ ttip: { ...hoverPayload } });
}
};
async initBasemap(cfg: MapLayerOptions) {
@ -187,6 +252,7 @@ export class GeomapPanel extends Component<Props, State> {
const handler = await item.create(this.map!, overlay, config.theme2);
const layer = handler.init();
(layer as any).___handler = handler;
this.map!.addLayer(layer);
this.layers.push({
config: overlay,
@ -284,13 +350,22 @@ export class GeomapPanel extends Component<Props, State> {
}
render() {
const { ttip, topRight, bottomLeft } = this.state;
return (
<>
<Global styles={this.globalCSS} />
<div className={this.style.wrap}>
<div className={this.style.map} ref={this.initMapRef}></div>
<GeomapOverlay {...this.state} />
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
</div>
<Portal>
{ttip && ttip.data && (
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }}>
<DataHoverView {...ttip} />
</VizTooltipContainer>
)}
</Portal>
</>
);
}

View File

@ -0,0 +1,56 @@
import React, { PureComponent } from 'react';
import { stylesFactory } from '@grafana/ui';
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { config } from 'app/core/config';
export interface Props {
data?: DataFrame; // source data
rowIndex?: number; // the hover row
columnIndex?: number; // the hover column
}
export class DataHoverView extends PureComponent<Props> {
style = getStyles(config.theme2);
render() {
const { data, rowIndex, columnIndex } = this.props;
if (!data || rowIndex == null) {
return null;
}
return (
<table className={this.style.infoWrap}>
<tbody>
{data.fields.map((f, i) => (
<tr key={`${i}/${rowIndex}`} className={i === columnIndex ? this.style.highlight : ''}>
<th>{getFieldDisplayName(f, data)}:</th>
<td>{fmt(f, rowIndex)}</td>
</tr>
))}
</tbody>
</table>
);
}
}
function fmt(field: Field, row: number): string {
const v = field.values.get(row);
if (field.display) {
return formattedValueToString(field.display(v));
}
return `${v}`;
}
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
infoWrap: css`
padding: 8px;
th {
font-weight: bold;
padding: 2px 10px 2px 0px;
}
`,
highlight: css`
background: ${theme.colors.action.hover};
`,
}));

View File

@ -22,17 +22,16 @@ export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
}
componentDidMount() {
console.log('ObservablePropsWrapper:subscribe');
this.sub = this.props.watch.subscribe({
next: (subProps: T) => {
console.log('ObservablePropsWrapper:NEXT', subProps);
//console.log('ObservablePropsWrapper:NEXT', subProps);
this.setState({ subProps });
},
complete: () => {
console.log('ObservablePropsWrapper:complete');
//console.log('ObservablePropsWrapper:complete');
},
error: (err) => {
console.log('ObservablePropsWrapper:error', err);
//console.log('ObservablePropsWrapper:error', err);
},
});
}
@ -41,12 +40,10 @@ export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
if (this.sub) {
this.sub.unsubscribe();
}
console.log('ObservablePropsWrapper:unsubscribe');
}
render() {
const { subProps } = this.state;
console.log('RENDER (wrap)', subProps);
return <this.props.child {...subProps} />;
}
}

View File

@ -0,0 +1,16 @@
import { FeatureLike } from 'ol/Feature';
import { SimpleGeometry } from 'ol/geom';
import { Layer } from 'ol/layer';
import { DataHoverPayload } from '@grafana/data';
export interface GeomapHoverFeature {
feature: FeatureLike;
layer: Layer;
geo: SimpleGeometry;
}
export interface GeomapHoverPayload extends DataHoverPayload {
features?: GeomapHoverFeature[];
pageX: number;
pageY: number;
}

View File

@ -85,8 +85,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
/>
}
const shape = markerMakers.getIfExists(config.shape) ?? circleMarker;
console.log( 'CREATE Marker layer', matchers);
return {
init: () => vectorLayer,
legend: legend,
@ -118,8 +117,10 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
const radius = sizeDim.get(i);
// Create a new Feature for each point returned from dataFrameToPoints
const dot = new Feature({
geometry: info.points[i],
const dot = new Feature( info.points[i] );
dot.setProperties({
frame,
rowIndex: i,
});
dot.setStyle(shape!.make(color, fillColor, radius));
@ -128,7 +129,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
// Post updates to the legend component
if (legend) {
console.log( 'UPDATE (marker layer)', colorDim);
legendProps.next({
color: colorDim,
size: sizeDim,