mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
Geomap: implement basic tooltip support (#37318)
Co-authored-by: Bryan Uribe <buribe@hmc.edu>
This commit is contained in:
parent
8b80d2256d
commit
ced26bc624
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
56
public/app/plugins/panel/geomap/components/DataHoverView.tsx
Normal file
56
public/app/plugins/panel/geomap/components/DataHoverView.tsx
Normal 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};
|
||||
`,
|
||||
}));
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
16
public/app/plugins/panel/geomap/event.ts
Normal file
16
public/app/plugins/panel/geomap/event.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user