diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 28701e6e092..8c148f97e5c 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -68,8 +68,8 @@ export interface MapLayerOptions { */ export interface MapLayerHandler { init: () => BaseLayer; - legend?: () => ReactNode; update?: (data: PanelData) => void; + legend?: ReactNode; } /** diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index d15c926767a..c428be79ae9 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry, defaultBaseLayer } from './layers/registry'; import { Map, View } from 'ol'; import Attribution from 'ol/control/Attribution'; @@ -33,16 +33,20 @@ let sharedView: View | undefined = undefined; export let lastGeomapPanelInstance: GeomapPanel | undefined = undefined; type Props = PanelProps; -export class GeomapPanel extends Component { +interface State extends OverlayProps {} +export class GeomapPanel extends Component { globalCSS = getGlobalStyles(config.theme2); + counter = 0; map?: Map; basemap?: BaseLayer; layers: MapLayerState[] = []; mouseWheelZoom?: MouseWheelZoom; style = getStyles(config.theme); - overlayProps: OverlayProps = {}; - + constructor(props: Props) { + super(props); + this.state = {}; + } componentDidMount() { lastGeomapPanelInstance = this; } @@ -65,7 +69,7 @@ export class GeomapPanel extends Component { // External data changed if (layersChanged || this.props.data !== nextProps.data) { - this.dataChanged(nextProps.data, nextProps.options.controls.showLegend); + this.dataChanged(nextProps.data); } return true; // always? @@ -106,17 +110,12 @@ export class GeomapPanel extends Component { /** * Called when PanelData changes (query results etc) */ - dataChanged(data: PanelData, showLegend?: boolean) { - const legends: React.ReactNode[] = []; + dataChanged(data: PanelData) { for (const state of this.layers) { if (state.handler.update) { state.handler.update(data); } - if (showLegend && state.handler.legend) { - legends.push(state.handler.legend()); - } } - this.overlayProps.bottomLeft = legends; } initMapRef = async (div: HTMLDivElement) => { @@ -143,7 +142,7 @@ export class GeomapPanel extends Component { this.map.addInteraction(this.mouseWheelZoom); this.initControls(options.controls); this.initBasemap(options.basemap); - await this.initLayers(options.layers, options.controls?.showLegend); + await this.initLayers(options.layers); this.forceUpdate(); // first render }; @@ -166,7 +165,7 @@ export class GeomapPanel extends Component { this.map.getLayers().insertAt(0, this.basemap); } - async initLayers(layers: MapLayerOptions[], showLegend?: boolean) { + async initLayers(layers: MapLayerOptions[]) { // 1st remove existing layers for (const state of this.layers) { this.map!.removeLayer(state.layer); @@ -177,6 +176,7 @@ export class GeomapPanel extends Component { layers = []; } + const legends: React.ReactNode[] = []; this.layers = []; for (const overlay of layers) { const item = geomapLayerRegistry.getIfExists(overlay.type); @@ -193,7 +193,12 @@ export class GeomapPanel extends Component { layer, handler, }); + + if (handler.legend) { + legends.push(
{handler.legend}
); + } } + this.setState({ bottomLeft: legends }); // Update data after init layers this.dataChanged(this.props.data); @@ -270,12 +275,12 @@ export class GeomapPanel extends Component { } // Update the react overlays - const overlayProps: OverlayProps = {}; + let topRight: ReactNode[] = []; if (options.showDebug) { - overlayProps.topRight = []; + topRight = []; } - this.overlayProps = overlayProps; + this.setState({ topRight }); } render() { @@ -284,7 +289,7 @@ export class GeomapPanel extends Component {
- +
); diff --git a/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx b/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx new file mode 100644 index 00000000000..80f60a7e551 --- /dev/null +++ b/public/app/plugins/panel/geomap/components/ObservablePropsWrapper.tsx @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { Observable, Unsubscribable } from 'rxjs'; + +interface Props { + watch: Observable; + child: React.ComponentType; + initialSubProps: T; +} + +interface State { + subProps: T; +} + +export class ObservablePropsWrapper extends Component, State> { + sub?: Unsubscribable; + + constructor(props: Props) { + super(props); + this.state = { + subProps: props.initialSubProps, + }; + } + + componentDidMount() { + console.log('ObservablePropsWrapper:subscribe'); + this.sub = this.props.watch.subscribe({ + next: (subProps: T) => { + console.log('ObservablePropsWrapper:NEXT', subProps); + this.setState({ subProps }); + }, + complete: () => { + console.log('ObservablePropsWrapper:complete'); + }, + error: (err) => { + console.log('ObservablePropsWrapper:error', err); + }, + }); + } + + componentWillUnmount() { + 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/components/SimpleLegend.tsx b/public/app/plugins/panel/geomap/components/SimpleLegend.tsx deleted file mode 100644 index 4ac4522f8e9..00000000000 --- a/public/app/plugins/panel/geomap/components/SimpleLegend.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { PureComponent } from 'react'; -import { stylesFactory } from '@grafana/ui'; -import { FieldType, formattedValueToString, GrafanaTheme, PanelData, ThresholdsConfig } from '@grafana/data'; -import { css } from '@emotion/css'; -import { config } from 'app/core/config'; -import tinycolor from 'tinycolor2'; - -interface Props { - txt: string; - data?: PanelData; -} - -interface State {} - -export class SimpleLegend extends PureComponent { - style = getStyles(config.theme); - - constructor(props: Props) { - super(props); - this.state = {}; - } - - render() { - let fmt = (v: any) => `${v}`; - let thresholds: ThresholdsConfig | undefined; - const series = this.props.data?.series; - if (series) { - for (const frame of series) { - for (const field of frame.fields) { - if (field.type === FieldType.number && field.config.thresholds) { - thresholds = field.config.thresholds; - fmt = (v: any) => `${formattedValueToString(field.display!(v))}`; - break; - } - } - } - } - - return ( -
-
{this.props.txt}
- {thresholds && ( -
- {thresholds.steps.map((step, idx) => { - const next = thresholds!.steps[idx + 1]; - let info = ?; - if (idx === 0) { - info = < {fmt(next.value)}; - } else if (next) { - info = ( - - {fmt(step.value)} - {fmt(next.value)} - - ); - } else { - info = {fmt(step.value)} +; - } - return ( -
- - {info} -
- ); - })} -
- )} -
- ); - } -} - -const getStyles = stylesFactory((theme: GrafanaTheme) => ({ - infoWrap: css` - color: ${theme.colors.text}; - background: ${tinycolor(theme.colors.panelBg).setAlpha(0.7).toString()}; - border-radius: 2px; - padding: 8px; - `, - legend: css` - line-height: 18px; - color: #555; - display: flex; - flex-direction: column; - - i { - width: 18px; - height: 18px; - float: left; - margin-right: 8px; - opacity: 0.7; - } - `, - legendItem: css` - white-space: nowrap; - `, -})); diff --git a/public/app/plugins/panel/geomap/dims/editors/ScaleDimensionEditor.tsx b/public/app/plugins/panel/geomap/dims/editors/ScaleDimensionEditor.tsx index 8d16ad617f8..7a1ca461f35 100644 --- a/public/app/plugins/panel/geomap/dims/editors/ScaleDimensionEditor.tsx +++ b/public/app/plugins/panel/geomap/dims/editors/ScaleDimensionEditor.tsx @@ -93,6 +93,7 @@ export const ScaleDimensionEditor: FC v.value === fieldName); return ( <> @@ -108,7 +109,7 @@ export const ScaleDimensionEditor: FC - + )} @@ -116,12 +117,12 @@ export const ScaleDimensionEditor: FC - + - + diff --git a/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx new file mode 100644 index 00000000000..099b121711f --- /dev/null +++ b/public/app/plugins/panel/geomap/layers/data/MarkersLegend.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { Label, stylesFactory } from '@grafana/ui'; +import { formattedValueToString, getFieldColorModeForField, GrafanaTheme } from '@grafana/data'; +import { css } from '@emotion/css'; +import { config } from 'app/core/config'; +import { DimensionSupplier } from '../../dims/types'; +import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale'; + +export interface MarkersLegendProps { + color?: DimensionSupplier; + size?: DimensionSupplier; +} +export function MarkersLegend(props: MarkersLegendProps) { + const { color } = props; + if (!color || (!color.field && color.fixed)) { + return ( + <> + ) + } + const style = getStyles(config.theme); + + const fmt = (v: any) => `${formattedValueToString(color.field!.display!(v))}`; + const colorMode = getFieldColorModeForField(color!.field!); + + if (colorMode.isContinuous && colorMode.getColors) { + const colors = colorMode.getColors(config.theme2) + const colorRange = getMinMaxAndDelta(color.field!) + // TODO: explore showing mean on the gradiant scale + // const stats = reduceField({ + // field: color.field!, + // reducers: [ + // ReducerID.min, + // ReducerID.max, + // ReducerID.mean, + // // std dev? + // ] + // }) + + return <> + +
c).join(', ')}`}}> +
{fmt(colorRange.min)}
+
{fmt(colorRange.max)}
+
+ + } + + const thresholds = color.field?.config?.thresholds; + if (!thresholds) { + return
no thresholds????
; + } + + return ( +
+ {thresholds && ( +
+ {thresholds.steps.map((step:any, idx:number) => { + const next = thresholds!.steps[idx + 1]; + let info = ?; + if (idx === 0) { + info = < {fmt(next.value)}; + } else if (next) { + info = ( + + {fmt(step.value)} - {fmt(next.value)} + + ); + } else { + info = {fmt(step.value)} +; + } + return ( +
+ + {info} +
+ ); + })} +
+ )} +
+ ) +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => ({ + infoWrap: css` + color: #999; + background: #CCCC; + border-radius: 2px; + padding: 8px; + `, + legend: css` + line-height: 18px; + color: #555; + display: flex; + flex-direction: column; + + i { + width: 18px; + height: 18px; + float: left; + margin-right: 8px; + opacity: 0.7; + } + `, + legendItem: css` + white-space: nowrap; + `, + gradientContainer: css` + min-width: 200px; + display: flex; + justify-content: space-between; + ` +})); diff --git a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx index 99a7112a40f..22b7ac231a7 100644 --- a/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/markersLayer.tsx @@ -11,6 +11,11 @@ import { getScaledDimension, } from '../../dims/scale'; import { getColorDimension, } from '../../dims/color'; import { ScaleDimensionEditor } from '../../dims/editors/ScaleDimensionEditor'; import { ColorDimensionEditor } from '../../dims/editors/ColorDimensionEditor'; +import React from 'react'; +import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper'; +import { ReplaySubject } from 'rxjs'; +import { MarkersLegend, MarkersLegendProps } from './MarkersLegend'; +import { ReactNode } from 'react'; import { circleMarker, markerMakers } from '../../utils/regularShapes'; // Configuration options for Circle overlays @@ -19,6 +24,7 @@ export interface MarkersConfig { color: ColorDimensionConfig; fillOpacity: number; shape?: string; + showLegend?: boolean; } const defaultOptions: MarkersConfig = { @@ -32,6 +38,7 @@ const defaultOptions: MarkersConfig = { }, fillOpacity: 0.4, shape: 'circle', + showLegend: true, }; export const MARKERS_LAYER_ID = "markers"; @@ -68,10 +75,21 @@ export const markersLayer: MapLayerRegistryItem = { ...options?.config, }; + const legendProps= new ReplaySubject(1); + let legend:ReactNode = null; + if (config.showLegend) { + legend = + } const shape = markerMakers.getIfExists(config.shape) ?? circleMarker; + console.log( 'CREATE Marker layer', matchers); return { init: () => vectorLayer, + legend: legend, update: (data: PanelData) => { if(!data.series?.length) { return; // ignore empty @@ -107,8 +125,17 @@ export const markersLayer: MapLayerRegistryItem = { dot.setStyle(shape!.make(color, fillColor, radius)); features.push(dot); }; - } + // Post updates to the legend component + if (legend) { + console.log( 'UPDATE (marker layer)', colorDim); + legendProps.next({ + color: colorDim, + size: sizeDim, + }); + } + break; // Only the first frame for now! + } // Source reads the data and provides a set of features to visualize const vectorSource = new source.Vector({ features }); @@ -162,6 +189,12 @@ export const markersLayer: MapLayerRegistryItem = { step: 0.1, }, showIf: (cfg) => (markerMakers.getIfExists((cfg as any).config?.shape)?.hasFill), + }) + .addBooleanSwitch({ + path: 'config.showLegend', + name: 'Show legend', + description: 'Show legend', + defaultValue: defaultOptions.showLegend, }); }, diff --git a/public/app/plugins/panel/geomap/migrations.test.ts b/public/app/plugins/panel/geomap/migrations.test.ts index 603401579c9..fbe64d78869 100644 --- a/public/app/plugins/panel/geomap/migrations.test.ts +++ b/public/app/plugins/panel/geomap/migrations.test.ts @@ -52,7 +52,6 @@ describe('Worldmap Migrations', () => { }, "controls": Object { "mouseWheelZoom": true, - "showLegend": true, "showZoom": true, }, "layers": Array [], diff --git a/public/app/plugins/panel/geomap/migrations.ts b/public/app/plugins/panel/geomap/migrations.ts index 2c4eaf9a8dc..26a2fbdd880 100644 --- a/public/app/plugins/panel/geomap/migrations.ts +++ b/public/app/plugins/panel/geomap/migrations.ts @@ -31,7 +31,6 @@ export function worldmapToGeomapOptions(angular: any): { fieldConfig: FieldConfi }, controls: { showZoom: true, - showLegend: Boolean(angular.showLegend), mouseWheelZoom: Boolean(angular.mouseWheelZoom), }, basemap: { diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index b58f11d8593..a816b6e77ea 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -66,13 +66,6 @@ export const plugin = new PanelPlugin(GeomapPanel) name: 'Mouse wheel zoom', defaultValue: true, }) - .addBooleanSwitch({ - category, - path: 'controls.showLegend', - name: 'Show legend', - description: 'Show legend', - defaultValue: true, - }) .addBooleanSwitch({ category, path: 'controls.showAttribution', diff --git a/public/app/plugins/panel/geomap/types.ts b/public/app/plugins/panel/geomap/types.ts index b9205cdce6e..49791309a66 100644 --- a/public/app/plugins/panel/geomap/types.ts +++ b/public/app/plugins/panel/geomap/types.ts @@ -9,9 +9,6 @@ export interface ControlsOptions { // let the mouse wheel zoom mouseWheelZoom?: boolean; - // Add legend control - showLegend?: boolean; - // Lower right showAttribution?: boolean;