Skip to content

Commit c244883

Browse files
[maps] show marker size in legend (#132549)
* [Maps] size legend * clean-up * refine spacing * clean up * more cleanup * use euiTheme for colors * fix jest test * do not show marker sizes for icons * remove lodash Co-authored-by: Kibana Machine <[email protected]>
1 parent d70ae0f commit c244883

File tree

4 files changed

+386
-9
lines changed

4 files changed

+386
-9
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { Component } from 'react';
9+
import { euiThemeVars } from '@kbn/ui-theme';
10+
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
11+
import { DynamicSizeProperty } from '../../properties/dynamic_size_property';
12+
13+
const FONT_SIZE = 10;
14+
const HALF_FONT_SIZE = FONT_SIZE / 2;
15+
const MIN_MARKER_DISTANCE = (FONT_SIZE + 2) / 2;
16+
17+
const EMPTY_VALUE = '';
18+
19+
interface Props {
20+
style: DynamicSizeProperty;
21+
}
22+
23+
interface State {
24+
label: string;
25+
}
26+
27+
export class MarkerSizeLegend extends Component<Props, State> {
28+
private _isMounted: boolean = false;
29+
30+
state: State = {
31+
label: EMPTY_VALUE,
32+
};
33+
34+
componentDidMount() {
35+
this._isMounted = true;
36+
this._loadLabel();
37+
}
38+
39+
componentDidUpdate() {
40+
this._loadLabel();
41+
}
42+
43+
componentWillUnmount() {
44+
this._isMounted = false;
45+
}
46+
47+
async _loadLabel() {
48+
const field = this.props.style.getField();
49+
if (!field) {
50+
return;
51+
}
52+
const label = await field.getLabel();
53+
if (this._isMounted && this.state.label !== label) {
54+
this.setState({ label });
55+
}
56+
}
57+
58+
_formatValue(value: string | number) {
59+
return value === EMPTY_VALUE ? value : this.props.style.formatField(value);
60+
}
61+
62+
_renderMarkers() {
63+
const fieldMeta = this.props.style.getRangeFieldMeta();
64+
const options = this.props.style.getOptions();
65+
if (!fieldMeta || !options) {
66+
return null;
67+
}
68+
69+
const circleStyle = {
70+
fillOpacity: 0,
71+
stroke: euiThemeVars.euiTextColor,
72+
strokeWidth: 1,
73+
};
74+
75+
const svgHeight = options.maxSize * 2 + HALF_FONT_SIZE + circleStyle.strokeWidth * 2;
76+
const circleCenterX = options.maxSize + circleStyle.strokeWidth;
77+
const circleBottomY = svgHeight - circleStyle.strokeWidth;
78+
79+
function makeMarker(radius: number, formattedValue: string | number) {
80+
const circleCenterY = circleBottomY - radius;
81+
const circleTopY = circleCenterY - radius;
82+
return (
83+
<g key={radius}>
84+
<line
85+
style={{ stroke: euiThemeVars.euiBorderColor }}
86+
x1={circleCenterX}
87+
y1={circleTopY}
88+
x2={circleCenterX * 2.25}
89+
y2={circleTopY}
90+
/>
91+
<text
92+
style={{ fontSize: FONT_SIZE, fill: euiThemeVars.euiTextColor }}
93+
x={circleCenterX * 2.25 + HALF_FONT_SIZE}
94+
y={circleTopY + HALF_FONT_SIZE}
95+
>
96+
{formattedValue}
97+
</text>
98+
<circle style={circleStyle} cx={circleCenterX} cy={circleCenterY} r={radius} />
99+
</g>
100+
);
101+
}
102+
103+
function getMarkerRadius(percentage: number) {
104+
const delta = options.maxSize - options.minSize;
105+
return percentage * delta + options.minSize;
106+
}
107+
108+
function getValue(percentage: number) {
109+
// Markers interpolated by area instead of radius to be more consistent with how the human eye+brain perceive shapes
110+
// and their visual relevance
111+
// This function mirrors output of maplibre expression created from DynamicSizeProperty.getMbSizeExpression
112+
const value = Math.pow(percentage * Math.sqrt(fieldMeta!.delta), 2) + fieldMeta!.min;
113+
return fieldMeta!.delta > 3 ? Math.round(value) : value;
114+
}
115+
116+
const markers = [];
117+
118+
if (fieldMeta.delta > 0) {
119+
const smallestMarker = makeMarker(options.minSize, this._formatValue(fieldMeta.min));
120+
markers.push(smallestMarker);
121+
122+
const markerDelta = options.maxSize - options.minSize;
123+
if (markerDelta > MIN_MARKER_DISTANCE * 3) {
124+
markers.push(makeMarker(getMarkerRadius(0.25), this._formatValue(getValue(0.25))));
125+
markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
126+
markers.push(makeMarker(getMarkerRadius(0.75), this._formatValue(getValue(0.75))));
127+
} else if (markerDelta > MIN_MARKER_DISTANCE) {
128+
markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
129+
}
130+
}
131+
132+
const largestMarker = makeMarker(options.maxSize, this._formatValue(fieldMeta.max));
133+
markers.push(largestMarker);
134+
135+
return (
136+
<svg height={svgHeight} xmlns="http://www.w3.org/2000/svg">
137+
{markers}
138+
</svg>
139+
);
140+
}
141+
142+
render() {
143+
return (
144+
<div>
145+
<EuiFlexGroup gutterSize="xs" justifyContent="spaceBetween">
146+
<EuiFlexItem grow={false}>
147+
<EuiToolTip
148+
position="top"
149+
title={this.props.style.getDisplayStyleName()}
150+
content={this.state.label}
151+
>
152+
<EuiText className="eui-textTruncate" size="xs" style={{ maxWidth: '180px' }}>
153+
<small>
154+
<strong>{this.state.label}</strong>
155+
</small>
156+
</EuiText>
157+
</EuiToolTip>
158+
</EuiFlexItem>
159+
</EuiFlexGroup>
160+
{this._renderMarkers()}
161+
</div>
162+
);
163+
}
164+
}

x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property/__snapshots__/dynamic_size_property.test.tsx.snap

Lines changed: 169 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)