From ced26bc624c5dd44f679ca2ed2ae23e59370a802 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 28 Jul 2021 18:34:42 -0700 Subject: [PATCH] Geomap: implement basic tooltip support (#37318) Co-authored-by: Bryan Uribe --- .../src/components/TimeSeries/utils.ts | 2 +- public/app/plugins/panel/debug/CursorView.tsx | 18 ++-- .../app/plugins/panel/geomap/GeomapPanel.tsx | 87 +++++++++++++++++-- .../panel/geomap/components/DataHoverView.tsx | 56 ++++++++++++ .../components/ObservablePropsWrapper.tsx | 9 +- public/app/plugins/panel/geomap/event.ts | 16 ++++ .../panel/geomap/layers/data/markersLayer.tsx | 10 +-- 7 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 public/app/plugins/panel/geomap/components/DataHoverView.tsx create mode 100644 public/app/plugins/panel/geomap/event.ts diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 4e3c62e8385..6db692ab3fb 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -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; diff --git a/public/app/plugins/panel/debug/CursorView.tsx b/public/app/plugins/panel/debug/CursorView.tsx index 87a30504254..b2b2768fb40 100644 --- a/public/app/plugins/panel/debug/CursorView.tsx +++ b/public/app/plugins/panel/debug/CursorView.tsx @@ -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 { if (!event) { return
no events yet
; } + const { type, payload, origin } = event; return ( -
-

Origin: {(event.origin as any)?.path}

- Type: {event.type} -
{JSON.stringify(event.payload.point, null, '  ')}
-
+ +

Origin: {(origin as any)?.path}

+ Type: {type} +
{JSON.stringify(payload.point, null, '  ')}
+ {payload.data && ( + + )} +
); } } diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index c428be79ae9..341aa65f761 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -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; -interface State extends OverlayProps {} +interface State extends OverlayProps { + ttip?: GeomapHoverPayload; +} + export class GeomapPanel extends Component { globalCSS = getGlobalStyles(config.theme2); @@ -43,10 +57,14 @@ export class GeomapPanel extends Component { 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 { 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 { 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 { } render() { + const { ttip, topRight, bottomLeft } = this.state; + return ( <>
- +
+ + {ttip && ttip.data && ( + + + + )} + ); } diff --git a/public/app/plugins/panel/geomap/components/DataHoverView.tsx b/public/app/plugins/panel/geomap/components/DataHoverView.tsx new file mode 100644 index 00000000000..b7ea79edf6e --- /dev/null +++ b/public/app/plugins/panel/geomap/components/DataHoverView.tsx @@ -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 { + style = getStyles(config.theme2); + + render() { + const { data, rowIndex, columnIndex } = this.props; + if (!data || rowIndex == null) { + return null; + } + + return ( + + + {data.fields.map((f, i) => ( + + + + + ))} + +
{getFieldDisplayName(f, data)}:{fmt(f, rowIndex)}
+ ); + } +} + +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}; + `, +})); diff --git a/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx b/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx index 80f60a7e551..3d4b14b0fe4 100644 --- a/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx +++ b/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx @@ -22,17 +22,16 @@ export class ObservablePropsWrapper extends Component, State> { } 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 extends Component, State> { if (this.sub) { this.sub.unsubscribe(); } - console.log('ObservablePropsWrapper:unsubscribe'); } render() { const { subProps } = this.state; - console.log('RENDER (wrap)', subProps); return ; } } diff --git a/public/app/plugins/panel/geomap/event.ts b/public/app/plugins/panel/geomap/event.ts new file mode 100644 index 00000000000..7c6582f3f13 --- /dev/null +++ b/public/app/plugins/panel/geomap/event.ts @@ -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; +} diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx index 22b7ac231a7..0ef72b1f25a 100644 --- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx @@ -85,8 +85,7 @@ export const markersLayer: MapLayerRegistryItem = { /> } 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 = { 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 = { // Post updates to the legend component if (legend) { - console.log( 'UPDATE (marker layer)', colorDim); legendProps.next({ color: colorDim, size: sizeDim,