Geomap: Display legend (#46886)

* Display legend for fixed colors and field; Hide tooltip on base layer;
This commit is contained in:
Adela Almasan 2022-03-30 09:41:13 -05:00 committed by GitHub
parent b52794601d
commit 118b87ee8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 25 deletions

View File

@ -46,6 +46,7 @@ type Props = PanelProps<GeomapPanelOptions>;
interface State extends OverlayProps { interface State extends OverlayProps {
ttip?: GeomapHoverPayload; ttip?: GeomapHoverPayload;
ttipOpen: boolean; ttipOpen: boolean;
legends: ReactNode[];
} }
export interface GeomapLayerActions { export interface GeomapLayerActions {
@ -82,7 +83,7 @@ export class GeomapPanel extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { ttipOpen: false }; this.state = { ttipOpen: false, legends: [] };
this.subs.add( this.subs.add(
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => { this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
if (this.mapDiv && this.props.id === evt.payload) { if (this.mapDiv && this.props.id === evt.payload) {
@ -114,7 +115,7 @@ export class GeomapPanel extends Component<Props, State> {
return true; // always? return true; // always?
} }
/** This funciton will actually update the JSON model */ /** This function will actually update the JSON model */
private doOptionsUpdate(selected: number) { private doOptionsUpdate(selected: number) {
const { options, onOptionsChange } = this.props; const { options, onOptionsChange } = this.props;
const layers = this.layers; const layers = this.layers;
@ -124,7 +125,7 @@ export class GeomapPanel extends Component<Props, State> {
layers: layers.slice(1).map((v) => v.options), layers: layers.slice(1).map((v) => v.options),
}); });
// Notify the the panel editor // Notify the panel editor
if (this.panelContext.onInstanceStateChange) { if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({ this.panelContext.onInstanceStateChange({
map: this.map, map: this.map,
@ -133,6 +134,8 @@ export class GeomapPanel extends Component<Props, State> {
actions: this.actions, actions: this.actions,
}); });
} }
this.setState({ legends: this.getLegends() });
} }
getNextLayerName = () => { getNextLayerName = () => {
@ -308,6 +311,8 @@ export class GeomapPanel extends Component<Props, State> {
actions: this.actions, actions: this.actions,
}); });
} }
this.setState({ legends: this.getLegends() });
}; };
clearTooltip = () => { clearTooltip = () => {
@ -447,6 +452,9 @@ export class GeomapPanel extends Component<Props, State> {
return false; return false;
} }
// Just to trigger a state update
this.setState({ legends: [] });
this.layers = layers; this.layers = layers;
this.doOptionsUpdate(layerIndex); this.doOptionsUpdate(layerIndex);
return true; return true;
@ -481,6 +489,7 @@ export class GeomapPanel extends Component<Props, State> {
if (!options.name) { if (!options.name) {
options.name = this.getNextLayerName(); options.name = this.getNextLayerName();
} }
const UID = options.name; const UID = options.name;
const state: MapLayerState<any> = { const state: MapLayerState<any> = {
// UID, // unique name when added to the map (it may change and will need special handling) // UID, // unique name when added to the map (it may change and will need special handling)
@ -496,6 +505,7 @@ export class GeomapPanel extends Component<Props, State> {
this.updateLayer(UID, cfg); this.updateLayer(UID, cfg);
}, },
}; };
this.byName.set(UID, state); this.byName.set(UID, state);
(state.layer as any).__state = state; (state.layer as any).__state = state;
return state; return state;
@ -597,15 +607,26 @@ export class GeomapPanel extends Component<Props, State> {
this.setState({ topRight }); this.setState({ topRight });
} }
getLegends() {
const legends: ReactNode[] = [];
for (const state of this.layers) {
if (state.handler.legend) {
legends.push(<div key={state.options.name}>{state.handler.legend}</div>);
}
}
return legends;
}
render() { render() {
const { ttip, ttipOpen, topRight, bottomLeft } = this.state; const { ttip, ttipOpen, topRight, legends } = this.state;
return ( return (
<> <>
<Global styles={this.globalCSS} /> <Global styles={this.globalCSS} />
<div className={this.style.wrap} onMouseLeave={this.clearTooltip}> <div className={this.style.wrap} onMouseLeave={this.clearTooltip}>
<div className={this.style.map} ref={this.initMapRef}></div> <div className={this.style.map} ref={this.initMapRef}></div>
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} /> <GeomapOverlay bottomLeft={legends} topRight={topRight} />
</div> </div>
<GeomapTooltip ttip={ttip} isOpen={ttipOpen} onClose={this.tooltipPopupClosed} /> <GeomapTooltip ttip={ttip} isOpen={ttipOpen} onClose={this.tooltipPopupClosed} />
</> </>

View File

@ -4,7 +4,7 @@ import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/O
import { defaultMarkersConfig } from '../layers/data/markersLayer'; import { defaultMarkersConfig } from '../layers/data/markersLayer';
import { hasAlphaPanels } from 'app/core/config'; import { hasAlphaPanels } from 'app/core/config';
import { MapLayerState } from '../types'; import { MapLayerState } from '../types';
import { get as lodashGet } from 'lodash'; import { get as lodashGet, isEqual } from 'lodash';
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
import { addLocationFields } from 'app/features/geo/editor/locationEditor'; import { addLocationFields } from 'app/features/geo/editor/locationEditor';
@ -93,12 +93,14 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
// TODO -- add opacity check // TODO -- add opacity check
} }
if (!isEqual(opts.category, ['Base layer'])) {
builder.addBooleanSwitch({ builder.addBooleanSwitch({
path: 'tooltip', path: 'tooltip',
name: 'Display tooltip', name: 'Display tooltip',
description: 'Show the tooltip for layer', description: 'Show the tooltip for layer',
defaultValue: true, defaultValue: true,
}); });
}
}, },
}; };
} }

View File

@ -5,28 +5,55 @@ import { css } from '@emotion/css';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionSupplier } from 'app/features/dimensions'; import { DimensionSupplier } from 'app/features/dimensions';
import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils'; import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils';
import { getMinMaxAndDelta } from '../../../../../../../packages/grafana-data/src/field/scale'; import { getMinMaxAndDelta } from '@grafana/data/src/field/scale';
import SVG from 'react-inlinesvg';
import { StyleConfigState } from '../../style/types';
export interface MarkersLegendProps { export interface MarkersLegendProps {
color?: DimensionSupplier<string>;
size?: DimensionSupplier<number>; size?: DimensionSupplier<number>;
layerName?: string;
styleConfig?: StyleConfigState;
} }
export function MarkersLegend(props: MarkersLegendProps) { export function MarkersLegend(props: MarkersLegendProps) {
const { color } = props; const { layerName, styleConfig } = props;
const theme = useTheme2(); const theme = useTheme2();
const style = getStyles(theme);
if (!color || (!color.field && color.fixed)) { if (!styleConfig) {
return <></>;
}
const { color, opacity} = styleConfig?.base ?? {};
const symbol = styleConfig?.config.symbol?.fixed;
const colorField = styleConfig.dims?.color?.field;
if (color && symbol && !colorField) {
return (
<div className={style.infoWrap}>
<div className={style.fixedColorContainer}>
<SVG
src={`public/${symbol}`}
className={style.legendSymbol}
title={'Symbol'}
style={{ fill: color, opacity: opacity }}
/>
<span>{layerName}</span>
</div>
</div>
)
}
if (!colorField) {
return <></>; return <></>;
} }
const style = getStyles(theme); const fmt = (v: any) => `${formattedValueToString(colorField.display!(v))}`;
const fmt = (v: any) => `${formattedValueToString(color.field!.display!(v))}`; const colorMode = getFieldColorModeForField(colorField);
const colorMode = getFieldColorModeForField(color!.field!);
if (colorMode.isContinuous && colorMode.getColors) { if (colorMode.isContinuous && colorMode.getColors) {
const colors = colorMode.getColors(config.theme2); const colors = colorMode.getColors(config.theme2);
const colorRange = getMinMaxAndDelta(color.field!); const colorRange = getMinMaxAndDelta(colorField);
// TODO: explore showing mean on the gradiant scale // TODO: explore showing mean on the gradiant scale
// const stats = reduceField({ // const stats = reduceField({
// field: color.field!, // field: color.field!,
@ -40,7 +67,7 @@ export function MarkersLegend(props: MarkersLegendProps) {
return ( return (
<> <>
<Label>{color?.field?.name}</Label> <Label>{colorField?.name}</Label>
<div <div
className={style.gradientContainer} className={style.gradientContainer}
style={{ backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}` }} style={{ backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}` }}
@ -52,12 +79,12 @@ export function MarkersLegend(props: MarkersLegendProps) {
); );
} }
const thresholds = color.field?.config?.thresholds; const thresholds = colorField?.config?.thresholds;
if (!thresholds || thresholds.steps.length < 2) { if (!thresholds || thresholds.steps.length < 2) {
return <div></div>; // don't show anything in the legend return <div></div>; // don't show anything in the legend
} }
const items = getThresholdItems(color.field!.config, config.theme2); const items = getThresholdItems(colorField!.config, config.theme2);
return ( return (
<div className={style.infoWrap}> <div className={style.infoWrap}>
<div className={style.legend}> <div className={style.legend}>
@ -95,6 +122,16 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
legendItem: css` legendItem: css`
white-space: nowrap; white-space: nowrap;
`, `,
fixedColorContainer: css`
min-width: 80px;
font-size: ${theme.typography.bodySmall.fontSize};
`,
legendSymbol: css`
height: 10px;
width: 10px;
margin: auto;
margin-right: 4px;
`,
gradientContainer: css` gradientContainer: css`
min-width: 200px; min-width: 200px;
display: flex; display: flex;

View File

@ -56,7 +56,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
/** /**
* Function that configures transformation and returns a transformer * Function that configures transformation and returns a transformer
* @param map
* @param options * @param options
* @param theme
*/ */
create: async (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2) => { create: async (map: Map, options: MapLayerOptions<MarkersConfig>, theme: GrafanaTheme2) => {
// Assert default values // Assert default values
@ -137,8 +139,9 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
// Post updates to the legend component // Post updates to the legend component
if (legend) { if (legend) {
legendProps.next({ legendProps.next({
color: style.dims?.color, styleConfig: style,
size: style.dims?.size, size: style.dims?.size,
layerName: options.name,
}); });
} }

View File

@ -22,7 +22,7 @@ export const defaultBaseLayer: MapLayerRegistryItem = {
if (serverLayerType) { if (serverLayerType) {
const layer = geomapLayerRegistry.getIfExists(serverLayerType); const layer = geomapLayerRegistry.getIfExists(serverLayerType);
if (!layer) { if (!layer) {
throw new Error('Invalid basemap configuraiton on server'); throw new Error('Invalid basemap configuration on server');
} }
return layer.create(map, config.geomapDefaultBaseLayerConfig!, theme); return layer.create(map, config.geomapDefaultBaseLayerConfig!, theme);
} }