mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Display legend (#46886)
* Display legend for fixed colors and field; Hide tooltip on base layer;
This commit is contained in:
parent
b52794601d
commit
118b87ee8f
@ -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} />
|
||||||
</>
|
</>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user