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
display?: (v: number) => string;
hoverValue?: number;
useStopsPercentage?: boolean;
};
type HoverState = {
@ -19,8 +20,9 @@ type HoverState = {
};
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 [scaleHover, setScaleHover] = useState<HoverState>({ isShown: false, value: 0 });
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);
useEffect(() => {
setColors(getGradientStops({ colorArray: colorPalette }));
}, [colorPalette]);
setColors(getGradientStops({ colorArray: colorPalette, stops: GRADIENT_STOPS, useStopsPercentage }));
}, [colorPalette, useStopsPercentage]);
const onScaleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
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;
if (colorCount <= 20) {
if (useStopsPercentage && colorCount <= 20) {
const incr = (1 / colorCount) * 100;
let per = 0;
const stops: string[] = [];
@ -114,8 +124,6 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
scaleWrapper: css`
width: 100%;
max-width: 300px;
margin-left: 25px;
padding: 10px 0;
font-size: 11px;
opacity: 1;
`,

View File

@ -33,11 +33,12 @@ import { DebugOverlay } from './components/DebugOverlay';
import { getGlobalStyles } from './globalStyles';
import { Global } from '@emotion/react';
import { GeomapHoverPayload, GeomapLayerHover } from './event';
import { Subscription } from 'rxjs';
import { Subject, Subscription } from 'rxjs';
import { PanelEditExitedEvent } from 'app/types/events';
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
import { cloneDeep } from 'lodash';
import { GeomapTooltip } from './GeomapTooltip';
import { FeatureLike } from 'ol/Feature';
// Allows multiple panels to share the same view instance
let sharedView: View | undefined = undefined;
@ -313,7 +314,7 @@ export class GeomapPanel extends Component<Props, State> {
this.props.eventBus.publish(new DataHoverClearEvent());
});
// Notify the the panel editor
// Notify the panel editor
if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({
map: this.map,
@ -371,6 +372,7 @@ export class GeomapPanel extends Component<Props, State> {
this.map.forEachFeatureAtPixel(
pixel,
(feature, layer, geo) => {
const s: MapLayerState = (layer as any).__state;
//match hover layer to layer in layers
//check if the layer show tooltip is enabled
//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.rowIndex = ttip.rowIndex = props['rowIndex'];
}
if (s?.mouseEvents) {
s.mouseEvents.next(feature);
}
}
const s: MapLayerState = (layer as any).__state;
if (s) {
let h = layerLookup.get(s);
if (!h) {
@ -406,6 +411,14 @@ export class GeomapPanel extends Component<Props, State> {
this.props.eventBus.publish(this.hoverEvent);
this.setState({ ttip: { ...hoverPayload } });
if (!layers.length) {
// clear mouse events
this.layers.forEach((layer) => {
layer.mouseEvents.next(undefined);
});
}
return layers.length ? true : false;
};
@ -508,6 +521,7 @@ export class GeomapPanel extends Component<Props, State> {
options,
layer,
handler,
mouseEvents: new Subject<FeatureLike | undefined>(),
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 { formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
import { DataFrame, formattedValueToString, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { config } from 'app/core/config';
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 SVG from 'react-inlinesvg';
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 {
size?: DimensionSupplier<number>;
layerName?: string;
styleConfig?: StyleConfigState;
layer?: BaseLayer;
}
export function MarkersLegend(props: MarkersLegendProps) {
const { layerName, styleConfig } = props;
const { layerName, styleConfig, layer } = props;
const theme = useTheme2();
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) {
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}>
@ -48,7 +72,6 @@ export function MarkersLegend(props: MarkersLegendProps) {
return <></>;
}
const fmt = (v: any) => `${formattedValueToString(colorField.display!(v))}`;
const colorMode = getFieldColorModeForField(colorField);
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 (
<>
<Label>{colorField?.name}</Label>
<div
className={style.gradientContainer}
style={{ backgroundImage: `linear-gradient(to right, ${colors.map((c) => c).join(', ')}` }}
>
<div style={{ color: theme.colors.getContrastText(colors[0]) }}>{fmt(colorRange.min)}</div>
<div style={{ color: theme.colors.getContrastText(colors[colors.length - 1]) }}>{fmt(colorRange.max)}</div>
<div className={style.labelsWrapper}>
<Label>{layerName}</Label>
<Label>{colorField?.name}</Label>
</div>
<div className={style.colorScaleWrapper}>
<ColorScale hoverValue={hoverValue} colorPalette={colors} min={colorRange.min as number} max={colorRange.max as number} display={display} useStopsPercentage={false}/>
</div>
</>
);
@ -132,11 +155,13 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
margin: auto;
margin-right: 4px;
`,
gradientContainer: css`
colorScaleWrapper: css`
min-width: 200px;
display: flex;
justify-content: space-between;
font-size: ${theme.typography.bodySmall.fontSize};
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,
};
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 location = await getLocationMatchers(options.location);
const source = new FrameVectorSource(location);
@ -80,6 +74,12 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
source,
});
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
let legend: ReactNode = null;
if (config.showLegend) {
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
}
if (!style.fields) {
// Set a global style
vectorLayer.setStyle(style.maker(style.base));
@ -142,6 +142,7 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
styleConfig: style,
size: style.dims?.size,
layerName: options.name,
layer: vectorLayer,
});
}

View File

@ -5,6 +5,8 @@ import BaseLayer from 'ol/layer/Base';
import Units from 'ol/proj/Units';
import { StyleConfig } from './style/types';
import { MapCenterID } from './view';
import { Subject } from 'rxjs';
import { FeatureLike } from 'ol/Feature';
export interface ControlsOptions {
// Zoom (upper left)
@ -80,4 +82,5 @@ export interface MapLayerState<TConfig = any> extends LayerElement {
layer: BaseLayer; // the openlayers instance
onChange: (cfg: MapLayerOptions<TConfig>) => void;
isBasemap?: boolean;
mouseEvents: Subject<FeatureLike | undefined>;
}

View File

@ -18,7 +18,7 @@ import { quantizeScheme } from './palettes';
import { HeatmapHoverEvent, prepConfig } from './utils';
import { HeatmapHoverView } from './HeatmapHoverView';
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> {}
@ -113,7 +113,9 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
return (
<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>
);
};
@ -164,4 +166,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
closeButtonSpacer: css`
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 { colorSchemes, quantizeScheme } from './palettes';
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)
.useFieldConfig()