Geomap: simplify styles (#41842)
This commit is contained in:
@@ -16,10 +16,10 @@ import { getScaledDimension, getColorDimension, getTextDimension } from 'app/fea
|
|||||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||||
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
import { MarkersLegend, MarkersLegendProps } from './MarkersLegend';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject } from 'rxjs';
|
||||||
import { FeaturesStylesBuilderConfig, getFeatures } from '../../utils/getFeatures';
|
import { getFeatures } from '../../utils/getFeatures';
|
||||||
import { getMarkerMaker } from '../../style/markers';
|
import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types';
|
||||||
import { defaultStyleConfig, StyleConfig } from '../../style/types';
|
|
||||||
import { StyleEditor } from './StyleEditor';
|
import { StyleEditor } from './StyleEditor';
|
||||||
|
import { getStyleConfigState } from '../../style/utils';
|
||||||
|
|
||||||
// Configuration options for Circle overlays
|
// Configuration options for Circle overlays
|
||||||
export interface MarkersConfig {
|
export interface MarkersConfig {
|
||||||
@@ -73,9 +73,11 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = config.style ?? defaultStyleConfig;
|
// Set the default style
|
||||||
const hasTextLabel = Boolean(style.text?.fixed || style.text?.field);
|
const style = await getStyleConfigState(config.style);
|
||||||
const markerMaker = await getMarkerMaker(style.symbol?.fixed, hasTextLabel);
|
if (!style.fields) {
|
||||||
|
vectorLayer.setStyle(style.maker(style.base));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init: () => vectorLayer,
|
init: () => vectorLayer,
|
||||||
@@ -94,24 +96,21 @@ export const markersLayer: MapLayerRegistryItem<MarkersConfig> = {
|
|||||||
continue; // ???
|
continue; // ???
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorDim = getColorDimension(frame, style.color ?? defaultStyleConfig.color, theme);
|
if (style.fields) {
|
||||||
const sizeDim = getScaledDimension(frame, style.size ?? defaultStyleConfig.size);
|
const dims: StyleDimensions = {};
|
||||||
let textDim = undefined;
|
if (style.fields.color) {
|
||||||
if (style?.text && (style.text.field || style.text.fixed)) {
|
dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme);
|
||||||
textDim = getTextDimension(frame, style.text);
|
}
|
||||||
|
if (style.fields.size) {
|
||||||
|
dims.size = getScaledDimension(frame, style.config.size ?? defaultStyleConfig.size);
|
||||||
|
}
|
||||||
|
if (style.fields.text) {
|
||||||
|
dims.text = getTextDimension(frame, style.config.text!);
|
||||||
|
}
|
||||||
|
style.dims = dims;
|
||||||
}
|
}
|
||||||
const opacity = style?.opacity ?? defaultStyleConfig.opacity;
|
|
||||||
|
|
||||||
const featureDimensionConfig: FeaturesStylesBuilderConfig = {
|
const frameFeatures = getFeatures(frame, info, style);
|
||||||
colorDim: colorDim,
|
|
||||||
sizeDim: sizeDim,
|
|
||||||
textDim: textDim,
|
|
||||||
textConfig: style?.textConfig,
|
|
||||||
opacity: opacity,
|
|
||||||
styleMaker: markerMaker,
|
|
||||||
};
|
|
||||||
|
|
||||||
const frameFeatures = getFeatures(frame, info, featureDimensionConfig);
|
|
||||||
|
|
||||||
if (frameFeatures) {
|
if (frameFeatures) {
|
||||||
features.push(...frameFeatures);
|
features.push(...frameFeatures);
|
||||||
@@ -120,8 +119,8 @@ 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: colorDim,
|
color: style.dims?.color,
|
||||||
size: sizeDim,
|
size: style.dims?.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break; // Only the first frame for now!
|
break; // Only the first frame for now!
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ColorDimensionConfig,
|
ColorDimensionConfig,
|
||||||
|
DimensionSupplier,
|
||||||
ResourceDimensionConfig,
|
ResourceDimensionConfig,
|
||||||
ResourceDimensionMode,
|
ResourceDimensionMode,
|
||||||
ScaleDimensionConfig,
|
ScaleDimensionConfig,
|
||||||
@@ -93,6 +94,28 @@ export interface StyleConfigValues {
|
|||||||
textConfig?: TextStyleConfig;
|
textConfig?: TextStyleConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** When the style depends on a field */
|
||||||
|
export interface StyleConfigFields {
|
||||||
|
color?: string;
|
||||||
|
size?: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StyleDimensions {
|
||||||
|
color?: DimensionSupplier<string>;
|
||||||
|
size?: DimensionSupplier<number>;
|
||||||
|
text?: DimensionSupplier<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StyleConfigState {
|
||||||
|
config: StyleConfig;
|
||||||
|
hasText?: boolean;
|
||||||
|
base: StyleConfigValues;
|
||||||
|
fields?: StyleConfigFields;
|
||||||
|
dims?: StyleDimensions;
|
||||||
|
maker: StyleMaker;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given values create a style
|
* Given values create a style
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ResourceDimensionMode } from 'app/features/dimensions';
|
||||||
|
import { StyleConfig } from './types';
|
||||||
|
import { getStyleConfigState } from './utils';
|
||||||
|
|
||||||
|
describe('style utils', () => {
|
||||||
|
it('should fill in default values', async () => {
|
||||||
|
const cfg: StyleConfig = {
|
||||||
|
color: {
|
||||||
|
field: 'Price',
|
||||||
|
fixed: 'dark-green',
|
||||||
|
},
|
||||||
|
opacity: 0.4,
|
||||||
|
size: {
|
||||||
|
field: 'Count',
|
||||||
|
fixed: 5,
|
||||||
|
max: 15,
|
||||||
|
min: 2,
|
||||||
|
},
|
||||||
|
symbol: {
|
||||||
|
fixed: 'img/icons/marker/star.svg',
|
||||||
|
mode: ResourceDimensionMode.Fixed, // 'fixed',
|
||||||
|
},
|
||||||
|
textConfig: {
|
||||||
|
fontSize: 12,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
// textAlign: 'center',
|
||||||
|
// textBaseline: 'middle',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = await getStyleConfigState(cfg);
|
||||||
|
state.config = null as any; // not interesting in the snapshot
|
||||||
|
expect(state.hasText).toBe(false);
|
||||||
|
expect(state).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"base": Object {
|
||||||
|
"color": "#37872D",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"opacity": 0.4,
|
||||||
|
"rotation": 0,
|
||||||
|
"size": 5,
|
||||||
|
},
|
||||||
|
"config": null,
|
||||||
|
"fields": Object {
|
||||||
|
"color": "Price",
|
||||||
|
"size": "Count",
|
||||||
|
},
|
||||||
|
"hasText": false,
|
||||||
|
"maker": [Function],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,64 @@
|
|||||||
import { StyleConfig } from './types';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { TextDimensionMode } from 'app/features/dimensions';
|
||||||
|
import { getMarkerMaker } from './markers';
|
||||||
|
import { defaultStyleConfig, StyleConfig, StyleConfigFields, StyleConfigState } from './types';
|
||||||
|
|
||||||
|
/** Indicate if the style wants to show text values */
|
||||||
|
export function styleUsesText(config: StyleConfig): boolean {
|
||||||
|
const { text } = config;
|
||||||
|
if (!text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (text.mode === TextDimensionMode.Fixed && text.fixed?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (text.mode === TextDimensionMode.Field && text.field?.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** Return a distinct list of fields used to dynamically change the style */
|
/** Return a distinct list of fields used to dynamically change the style */
|
||||||
export function getDependantFields(config: StyleConfig): Set<string> | undefined {
|
export async function getStyleConfigState(cfg?: StyleConfig): Promise<StyleConfigState> {
|
||||||
const fields = new Set<string>();
|
if (!cfg) {
|
||||||
|
cfg = defaultStyleConfig;
|
||||||
|
}
|
||||||
|
const hasText = styleUsesText(cfg);
|
||||||
|
const fields: StyleConfigFields = {};
|
||||||
|
const maker = await getMarkerMaker(cfg.symbol?.fixed, hasText);
|
||||||
|
const state: StyleConfigState = {
|
||||||
|
config: cfg, // raw values
|
||||||
|
hasText,
|
||||||
|
fields,
|
||||||
|
base: {
|
||||||
|
color: config.theme2.visualization.getColorByName(cfg.color?.fixed ?? defaultStyleConfig.color.fixed),
|
||||||
|
opacity: cfg.opacity ?? defaultStyleConfig.opacity,
|
||||||
|
lineWidth: cfg.lineWidth ?? 1,
|
||||||
|
size: cfg.size?.fixed ?? defaultStyleConfig.size.fixed,
|
||||||
|
rotation: 0, // dynamic will follow path
|
||||||
|
},
|
||||||
|
maker,
|
||||||
|
};
|
||||||
|
|
||||||
if (config.color?.field) {
|
if (cfg.color?.field?.length) {
|
||||||
fields.add(config.color.field);
|
fields.color = cfg.color.field;
|
||||||
}
|
}
|
||||||
if (config.size?.field) {
|
if (cfg.size?.field?.length) {
|
||||||
fields.add(config.size.field);
|
fields.size = cfg.size.field;
|
||||||
}
|
|
||||||
if (config.text?.field) {
|
|
||||||
fields.add(config.text.field);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
if (hasText) {
|
||||||
|
state.base.text = cfg.text?.fixed;
|
||||||
|
state.base.textConfig = cfg.textConfig ?? defaultStyleConfig.textConfig;
|
||||||
|
|
||||||
|
if (cfg.text?.field?.length) {
|
||||||
|
fields.text = cfg.text.field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the fields if possible
|
||||||
|
if (!Object.keys(fields).length) {
|
||||||
|
state.fields = undefined;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,20 @@
|
|||||||
import { DataFrame } from '@grafana/data';
|
import { DataFrame } from '@grafana/data';
|
||||||
import { DimensionSupplier } from 'app/features/dimensions';
|
|
||||||
import { Feature } from 'ol';
|
import { Feature } from 'ol';
|
||||||
import { Point } from 'ol/geom';
|
import { Point } from 'ol/geom';
|
||||||
import { StyleMaker, TextStyleConfig } from '../style/types';
|
import { StyleConfigState } from '../style/types';
|
||||||
import { LocationInfo } from './location';
|
import { LocationInfo } from './location';
|
||||||
|
|
||||||
export interface FeaturesStylesBuilderConfig {
|
|
||||||
colorDim: DimensionSupplier<string>;
|
|
||||||
sizeDim: DimensionSupplier<number>;
|
|
||||||
opacity: number;
|
|
||||||
styleMaker: StyleMaker;
|
|
||||||
textDim?: DimensionSupplier<string>;
|
|
||||||
textConfig?: TextStyleConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getFeatures = (
|
export const getFeatures = (
|
||||||
frame: DataFrame,
|
frame: DataFrame,
|
||||||
info: LocationInfo,
|
info: LocationInfo,
|
||||||
config: FeaturesStylesBuilderConfig
|
style: StyleConfigState
|
||||||
): Array<Feature<Point>> | undefined => {
|
): Array<Feature<Point>> | undefined => {
|
||||||
const features: Array<Feature<Point>> = [];
|
const features: Array<Feature<Point>> = [];
|
||||||
const opacity = config.opacity;
|
const { dims } = style;
|
||||||
|
const values = { ...style.base };
|
||||||
|
|
||||||
// Map each data value into new points
|
// Map each data value into new points
|
||||||
for (let i = 0; i < frame.length; i++) {
|
for (let i = 0; i < frame.length; i++) {
|
||||||
// Get the color for the feature based on color scheme
|
|
||||||
const color = config.colorDim.get(i);
|
|
||||||
|
|
||||||
// Get the size for the feature based on size dimension
|
|
||||||
const size = config.sizeDim.get(i);
|
|
||||||
|
|
||||||
// Get the text for the feature based on text dimension
|
|
||||||
const text = config?.textDim ? config?.textDim.get(i) : undefined;
|
|
||||||
|
|
||||||
// Get the textConfig
|
|
||||||
const textConfig = config?.textConfig;
|
|
||||||
|
|
||||||
// Create a new Feature for each point returned from dataFrameToPoints
|
// Create a new Feature for each point returned from dataFrameToPoints
|
||||||
const dot = new Feature(info.points[i]);
|
const dot = new Feature(info.points[i]);
|
||||||
dot.setProperties({
|
dot.setProperties({
|
||||||
@@ -43,7 +22,20 @@ export const getFeatures = (
|
|||||||
rowIndex: i,
|
rowIndex: i,
|
||||||
});
|
});
|
||||||
|
|
||||||
dot.setStyle(config.styleMaker({ color, size, text, opacity, textConfig }));
|
// Update values used in dynamic styles
|
||||||
|
if (dims) {
|
||||||
|
if (dims.color) {
|
||||||
|
values.color = dims.color.get(i);
|
||||||
|
}
|
||||||
|
if (dims.size) {
|
||||||
|
values.size = dims.size.get(i);
|
||||||
|
}
|
||||||
|
if (dims.text) {
|
||||||
|
values.text = dims.text.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
dot.setStyle(style.maker(values));
|
||||||
|
}
|
||||||
features.push(dot);
|
features.push(dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user