Geomap: Add color scale in legend (#47803)

Added color scale in geomap legend
This commit is contained in:
Adela Almasan 2022-04-18 09:19:20 -05:00 committed by GitHub
parent 516c8b60ee
commit 858a1bd24e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 35 deletions

View File

@ -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;
`, `,

View File

@ -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,

View File

@ -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;
`
})); }));

View File

@ -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,
}); });
} }

View File

@ -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>;
} }

View File

@ -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;
`,
}); });

View File

@ -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()