mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Add color scale in legend (#47803)
Added color scale in geomap legend
This commit is contained in:
parent
516c8b60ee
commit
858a1bd24e
@ -11,6 +11,7 @@ type Props = {
|
|||||||
// Show a value as string -- when not defined, the raw values will not be shown
|
// Show a value as string -- when not defined, the raw values will not be shown
|
||||||
display?: (v: number) => string;
|
display?: (v: number) => string;
|
||||||
hoverValue?: number;
|
hoverValue?: number;
|
||||||
|
useStopsPercentage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HoverState = {
|
type HoverState = {
|
||||||
@ -19,8 +20,9 @@ type HoverState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LEFT_OFFSET = 2;
|
const LEFT_OFFSET = 2;
|
||||||
|
const GRADIENT_STOPS = 10;
|
||||||
|
|
||||||
export const ColorScale = ({ colorPalette, min, max, display, hoverValue }: Props) => {
|
export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => {
|
||||||
const [colors, setColors] = useState<string[]>([]);
|
const [colors, setColors] = useState<string[]>([]);
|
||||||
const [scaleHover, setScaleHover] = useState<HoverState>({ isShown: false, value: 0 });
|
const [scaleHover, setScaleHover] = useState<HoverState>({ isShown: false, value: 0 });
|
||||||
const [percent, setPercent] = useState<number | null>(null);
|
const [percent, setPercent] = useState<number | null>(null);
|
||||||
@ -29,8 +31,8 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue }: Prop
|
|||||||
const styles = getStyles(theme, colors);
|
const styles = getStyles(theme, colors);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColors(getGradientStops({ colorArray: colorPalette }));
|
setColors(getGradientStops({ colorArray: colorPalette, stops: GRADIENT_STOPS, useStopsPercentage }));
|
||||||
}, [colorPalette]);
|
}, [colorPalette, useStopsPercentage]);
|
||||||
|
|
||||||
const onScaleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
const onScaleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
const divOffset = event.nativeEvent.offsetX;
|
const divOffset = event.nativeEvent.offsetX;
|
||||||
@ -79,9 +81,17 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue }: Prop
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGradientStops = ({ colorArray, stops = 10 }: { colorArray: string[]; stops?: number }): string[] => {
|
const getGradientStops = ({
|
||||||
|
colorArray,
|
||||||
|
stops,
|
||||||
|
useStopsPercentage = true,
|
||||||
|
}: {
|
||||||
|
colorArray: string[];
|
||||||
|
stops: number;
|
||||||
|
useStopsPercentage?: boolean;
|
||||||
|
}): string[] => {
|
||||||
const colorCount = colorArray.length;
|
const colorCount = colorArray.length;
|
||||||
if (colorCount <= 20) {
|
if (useStopsPercentage && colorCount <= 20) {
|
||||||
const incr = (1 / colorCount) * 100;
|
const incr = (1 / colorCount) * 100;
|
||||||
let per = 0;
|
let per = 0;
|
||||||
const stops: string[] = [];
|
const stops: string[] = [];
|
||||||
@ -114,8 +124,6 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
|||||||
scaleWrapper: css`
|
scaleWrapper: css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
margin-left: 25px;
|
|
||||||
padding: 10px 0;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
`,
|
`,
|
@ -33,11 +33,12 @@ import { DebugOverlay } from './components/DebugOverlay';
|
|||||||
import { getGlobalStyles } from './globalStyles';
|
import { getGlobalStyles } from './globalStyles';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import { GeomapHoverPayload, GeomapLayerHover } from './event';
|
import { GeomapHoverPayload, GeomapLayerHover } from './event';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { PanelEditExitedEvent } from 'app/types/events';
|
import { PanelEditExitedEvent } from 'app/types/events';
|
||||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { GeomapTooltip } from './GeomapTooltip';
|
import { GeomapTooltip } from './GeomapTooltip';
|
||||||
|
import { FeatureLike } from 'ol/Feature';
|
||||||
|
|
||||||
// Allows multiple panels to share the same view instance
|
// Allows multiple panels to share the same view instance
|
||||||
let sharedView: View | undefined = undefined;
|
let sharedView: View | undefined = undefined;
|
||||||
@ -313,7 +314,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.props.eventBus.publish(new DataHoverClearEvent());
|
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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,
|
||||||
@ -371,6 +372,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.map.forEachFeatureAtPixel(
|
this.map.forEachFeatureAtPixel(
|
||||||
pixel,
|
pixel,
|
||||||
(feature, layer, geo) => {
|
(feature, layer, geo) => {
|
||||||
|
const s: MapLayerState = (layer as any).__state;
|
||||||
//match hover layer to layer in layers
|
//match hover layer to layer in layers
|
||||||
//check if the layer show tooltip is enabled
|
//check if the layer show tooltip is enabled
|
||||||
//then also pass the list of tooltip fields if exists
|
//then also pass the list of tooltip fields if exists
|
||||||
@ -382,9 +384,12 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
hoverPayload.data = ttip.data = frame as DataFrame;
|
hoverPayload.data = ttip.data = frame as DataFrame;
|
||||||
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s?.mouseEvents) {
|
||||||
|
s.mouseEvents.next(feature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const s: MapLayerState = (layer as any).__state;
|
|
||||||
if (s) {
|
if (s) {
|
||||||
let h = layerLookup.get(s);
|
let h = layerLookup.get(s);
|
||||||
if (!h) {
|
if (!h) {
|
||||||
@ -406,6 +411,14 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
this.props.eventBus.publish(this.hoverEvent);
|
this.props.eventBus.publish(this.hoverEvent);
|
||||||
|
|
||||||
this.setState({ ttip: { ...hoverPayload } });
|
this.setState({ ttip: { ...hoverPayload } });
|
||||||
|
|
||||||
|
if (!layers.length) {
|
||||||
|
// clear mouse events
|
||||||
|
this.layers.forEach((layer) => {
|
||||||
|
layer.mouseEvents.next(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return layers.length ? true : false;
|
return layers.length ? true : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -508,6 +521,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
options,
|
options,
|
||||||
layer,
|
layer,
|
||||||
handler,
|
handler,
|
||||||
|
mouseEvents: new Subject<FeatureLike | undefined>(),
|
||||||
|
|
||||||
getName: () => UID,
|
getName: () => UID,
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Label, stylesFactory, useTheme2, VizLegendItem } from '@grafana/ui';
|
import { Label, stylesFactory, useTheme2, VizLegendItem } from '@grafana/ui';
|
||||||
import { formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
|
import { DataFrame, formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { css } from '@emotion/css';
|
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';
|
||||||
@ -8,26 +8,50 @@ import { getThresholdItems } from 'app/plugins/panel/state-timeline/utils';
|
|||||||
import { getMinMaxAndDelta } from '@grafana/data/src/field/scale';
|
import { getMinMaxAndDelta } from '@grafana/data/src/field/scale';
|
||||||
import SVG from 'react-inlinesvg';
|
import SVG from 'react-inlinesvg';
|
||||||
import { StyleConfigState } from '../../style/types';
|
import { StyleConfigState } from '../../style/types';
|
||||||
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import BaseLayer from 'ol/layer/Base';
|
||||||
|
import { MapLayerState } from '../../types';
|
||||||
|
|
||||||
export interface MarkersLegendProps {
|
export interface MarkersLegendProps {
|
||||||
size?: DimensionSupplier<number>;
|
size?: DimensionSupplier<number>;
|
||||||
layerName?: string;
|
layerName?: string;
|
||||||
styleConfig?: StyleConfigState;
|
styleConfig?: StyleConfigState;
|
||||||
|
layer?: BaseLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkersLegend(props: MarkersLegendProps) {
|
export function MarkersLegend(props: MarkersLegendProps) {
|
||||||
const { layerName, styleConfig } = props;
|
const { layerName, styleConfig, layer } = props;
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const style = getStyles(theme);
|
const style = getStyles(theme);
|
||||||
|
|
||||||
|
const hoverEvent = useObservable(((layer as any)?.__state as MapLayerState)?.mouseEvents ?? of(undefined));
|
||||||
|
|
||||||
|
const colorField = styleConfig?.dims?.color?.field;
|
||||||
|
const hoverValue = useMemo(() => {
|
||||||
|
if (!colorField || !hoverEvent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = hoverEvent.getProperties();
|
||||||
|
const frame = props.frame as DataFrame;
|
||||||
|
|
||||||
|
if (!frame) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowIndex = props.rowIndex as number;
|
||||||
|
return colorField.values.get(rowIndex);
|
||||||
|
}, [hoverEvent, colorField]);
|
||||||
|
|
||||||
if (!styleConfig) {
|
if (!styleConfig) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { color, opacity} = styleConfig?.base ?? {};
|
const { color, opacity} = styleConfig?.base ?? {};
|
||||||
const symbol = styleConfig?.config.symbol?.fixed;
|
const symbol = styleConfig?.config.symbol?.fixed;
|
||||||
|
|
||||||
const colorField = styleConfig.dims?.color?.field;
|
|
||||||
|
|
||||||
if (color && symbol && !colorField) {
|
if (color && symbol && !colorField) {
|
||||||
return (
|
return (
|
||||||
<div className={style.infoWrap}>
|
<div className={style.infoWrap}>
|
||||||
@ -48,7 +72,6 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fmt = (v: any) => `${formattedValueToString(colorField.display!(v))}`;
|
|
||||||
const colorMode = getFieldColorModeForField(colorField);
|
const colorMode = getFieldColorModeForField(colorField);
|
||||||
|
|
||||||
if (colorMode.isContinuous && colorMode.getColors) {
|
if (colorMode.isContinuous && colorMode.getColors) {
|
||||||
@ -65,15 +88,15 @@ export function MarkersLegend(props: MarkersLegendProps) {
|
|||||||
// ]
|
// ]
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
const display = colorField.display ? (v: number) => formattedValueToString(colorField.display!(v)) : (v: number) => `${v}`;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Label>{colorField?.name}</Label>
|
<div className={style.labelsWrapper}>
|
||||||
<div
|
<Label>{layerName}</Label>
|
||||||
className={style.gradientContainer}
|
<Label>{colorField?.name}</Label>
|
||||||
style={{ backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}` }}
|
</div>
|
||||||
>
|
<div className={style.colorScaleWrapper}>
|
||||||
<div style={{ color: theme.colors.getContrastText(colors[0]) }}>{fmt(colorRange.min)}</div>
|
<ColorScale hoverValue={hoverValue} colorPalette={colors} min={colorRange.min as number} max={colorRange.max as number} display={display} useStopsPercentage={false}/>
|
||||||
<div style={{ color: theme.colors.getContrastText(colors[colors.length - 1]) }}>{fmt(colorRange.max)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -132,11 +155,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
`,
|
`,
|
||||||
gradientContainer: css`
|
colorScaleWrapper: css`
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
padding: ${theme.spacing(0, 0.5)};
|
padding: ${theme.spacing(0, 0.5)};
|
||||||
`,
|
`,
|
||||||
|
labelsWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`
|
||||||
}));
|
}));
|
||||||
|
@ -67,12 +67,6 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
...options?.config,
|
...options?.config,
|
||||||
};
|
};
|
||||||
|
|
||||||
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
|
|
||||||
let legend: ReactNode = null;
|
|
||||||
if (config.showLegend) {
|
|
||||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = await getStyleConfigState(config.style);
|
const style = await getStyleConfigState(config.style);
|
||||||
const location = await getLocationMatchers(options.location);
|
const location = await getLocationMatchers(options.location);
|
||||||
const source = new FrameVectorSource(location);
|
const source = new FrameVectorSource(location);
|
||||||
@ -80,6 +74,12 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
source,
|
source,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
|
||||||
|
let legend: ReactNode = null;
|
||||||
|
if (config.showLegend) {
|
||||||
|
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!style.fields) {
|
if (!style.fields) {
|
||||||
// Set a global style
|
// Set a global style
|
||||||
vectorLayer.setStyle(style.maker(style.base));
|
vectorLayer.setStyle(style.maker(style.base));
|
||||||
@ -142,6 +142,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
styleConfig: style,
|
styleConfig: style,
|
||||||
size: style.dims?.size,
|
size: style.dims?.size,
|
||||||
layerName: options.name,
|
layerName: options.name,
|
||||||
|
layer: vectorLayer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import BaseLayer from 'ol/layer/Base';
|
|||||||
import Units from 'ol/proj/Units';
|
import Units from 'ol/proj/Units';
|
||||||
import { StyleConfig } from './style/types';
|
import { StyleConfig } from './style/types';
|
||||||
import { MapCenterID } from './view';
|
import { MapCenterID } from './view';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { FeatureLike } from 'ol/Feature';
|
||||||
|
|
||||||
export interface ControlsOptions {
|
export interface ControlsOptions {
|
||||||
// Zoom (upper left)
|
// Zoom (upper left)
|
||||||
@ -80,4 +82,5 @@ export interface MapLayerState<TConfig = any> extends LayerElement {
|
|||||||
layer: BaseLayer; // the openlayers instance
|
layer: BaseLayer; // the openlayers instance
|
||||||
onChange: (cfg: MapLayerOptions<TConfig>) => void;
|
onChange: (cfg: MapLayerOptions<TConfig>) => void;
|
||||||
isBasemap?: boolean;
|
isBasemap?: boolean;
|
||||||
|
mouseEvents: Subject<FeatureLike | undefined>;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import { quantizeScheme } from './palettes';
|
|||||||
import { HeatmapHoverEvent, prepConfig } from './utils';
|
import { HeatmapHoverEvent, prepConfig } from './utils';
|
||||||
import { HeatmapHoverView } from './HeatmapHoverView';
|
import { HeatmapHoverView } from './HeatmapHoverView';
|
||||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
import { ColorScale } from './ColorScale';
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
|
|
||||||
interface HeatmapPanelProps extends PanelProps<PanelOptions> {}
|
interface HeatmapPanelProps extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
@ -113,7 +113,9 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout.Legend placement="bottom" maxHeight="20%">
|
<VizLayout.Legend placement="bottom" maxHeight="20%">
|
||||||
<ColorScale hoverValue={hoverValue} colorPalette={palette} min={min} max={max} display={info.display} />
|
<div className={styles.colorScaleWrapper}>
|
||||||
|
<ColorScale hoverValue={hoverValue} colorPalette={palette} min={min} max={max} display={info.display} />
|
||||||
|
</div>
|
||||||
</VizLayout.Legend>
|
</VizLayout.Legend>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -164,4 +166,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
closeButtonSpacer: css`
|
closeButtonSpacer: css`
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
`,
|
`,
|
||||||
|
colorScaleWrapper: css`
|
||||||
|
margin-left: 25px;
|
||||||
|
padding: 10px 0;
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@ import { heatmapChangedHandler } from './migrations';
|
|||||||
import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
|
import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper';
|
||||||
import { colorSchemes, quantizeScheme } from './palettes';
|
import { colorSchemes, quantizeScheme } from './palettes';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { ColorScale } from './ColorScale';
|
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
|
export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPanel)
|
||||||
.useFieldConfig()
|
.useFieldConfig()
|
||||||
|
Loading…
Reference in New Issue
Block a user