[maps] fix size legend does not indicate when min or max clamped by std range (#156927)

Fixes https://github.com/elastic/kibana/issues/156907 and
https://github.com/elastic/kibana/issues/133810

Display `>` when max is clamped by standard deviation
<img width="200" alt="Screen Shot 2023-05-05 at 3 33 11 PM"
src="https://user-images.githubusercontent.com/373691/236572440-a0395094-5a70-45f8-b64a-dd4ecdc1412a.png">

Bottom label is not cut off when pixel size is 1
<img width="200" alt="Screen Shot 2023-05-05 at 3 33 18 PM"
src="https://user-images.githubusercontent.com/373691/236572444-51b5af9e-fa31-4033-a671-0a9642d10e3e.png">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-05-30 07:28:45 -06:00 committed by GitHub
parent 094b62a6d6
commit ff8cebb407
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1485 additions and 347 deletions

View file

@ -0,0 +1,958 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should invert legend 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={55}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="7"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={40}
y2={40}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={61.25}
y={45}
/>
<circle
cx={25}
cy={47}
r={7}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="15.5"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={23}
y2={23}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="14.25KB"
x={61.25}
y={28}
/>
<circle
cx={25}
cy={38.5}
r={15.5}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="24"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0KB"
x={61.25}
y={11}
/>
<circle
cx={25}
cy={30}
r={24}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render legend 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={71}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="7"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={33}
x2={74.25}
y1={56}
y2={56}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0KB"
x={79.25}
y={61}
/>
<circle
cx={33}
cy={63}
r={7}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="13.25"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={33}
x2={74.25}
y1={43.5}
y2={43.5}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="1.188KB"
x={79.25}
y={48.5}
/>
<circle
cx={33}
cy={56.75}
r={13.25}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="19.5"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={33}
x2={74.25}
y1={31}
y2={31}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="4.75KB"
x={79.25}
y={36}
/>
<circle
cx={33}
cy={50.5}
r={19.5}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="25.75"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={33}
x2={74.25}
y1={18.5}
y2={18.5}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="10.688KB"
x={79.25}
y={23.5}
/>
<circle
cx={33}
cy={44.25}
r={25.75}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="32"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={33}
x2={74.25}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={79.25}
y={11}
/>
<circle
cx={33}
cy={38}
r={32}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render legend with 2 markers when size difference does not provide enough vertical space for more labels 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={37}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="7"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={16}
x2={36}
y1={22}
y2={22}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0KB"
x={41}
y={27}
/>
<circle
cx={16}
cy={29}
r={7}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="15"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={16}
x2={36}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={41}
y={11}
/>
<circle
cx={16}
cy={21}
r={15}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render legend with 3 markers when size difference does not provide enough vertical space for more labels 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={55}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="7"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={40}
y2={40}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0KB"
x={61.25}
y={45}
/>
<circle
cx={25}
cy={47}
r={7}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="15.5"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={23}
y2={23}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="4.75KB"
x={61.25}
y={28}
/>
<circle
cx={25}
cy={38.5}
r={15.5}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="24"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={25}
x2={56.25}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={61.25}
y={11}
/>
<circle
cx={25}
cy={30}
r={24}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render legend with only max marker when size difference does not provide enough vertical space for more labels 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={29}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="11"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={12}
x2={27}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={32}
y={11}
/>
<circle
cx={12}
cy={17}
r={11}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render legend without label cutoff when min size is 1 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={21}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="1"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={8}
x2={18}
y1={18}
y2={18}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0KB"
x={23}
y={21}
/>
<circle
cx={8}
cy={19}
r={1}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="7"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={8}
x2={18}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="19KB"
x={23}
y={11}
/>
<circle
cx={8}
cy={13}
r={7}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;
exports[`Should render max label with std clamp notification 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="bytes"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
bytes
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={29}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="11"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={12}
x2={27}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="> 16KB"
x={32}
y={11}
/>
<circle
cx={12}
cy={17}
r={11}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
</div>
`;

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should render legend 1`] = `
<RangedStyleLegendRow
fieldLabel="bytes"
header={
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "1px",
"width": "12px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "2px",
"width": "12px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "3px",
"width": "12px",
}
}
/>
</EuiFlexItem>
</React.Fragment>
</EuiFlexGroup>
}
invert={false}
maxLabel="19KB"
minLabel="0KB"
propertyLabel="Border width"
/>
`;

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export function getMaxLabel(
isFieldMetaEnabled: boolean,
isMaxOutsideStdRange: boolean,
max: number | string
) {
return isFieldMetaEnabled && isMaxOutsideStdRange ? `> ${max}` : max;
}
export function getMinLabel(
isFieldMetaEnabled: boolean,
isMinOutsideStdRange: boolean,
min: number | string
) {
return isFieldMetaEnabled && isMinOutsideStdRange ? `< ${min}` : min;
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { MarkerSizeLegend } from './marker_size_legend';
export { OrdinalLegend } from './ordinal_legend';

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ReactNode } from 'react';
export interface Marker {
svg: ReactNode;
textY: number;
}
export class MarkerList {
private readonly _minFontDistance;
private readonly _maxMarker;
private readonly _markers: Marker[] = [];
constructor(fontSize: number, maxMarker: Marker) {
this._minFontDistance = fontSize * 0.85;
this._maxMarker = maxMarker;
}
push(marker: Marker) {
if (marker.textY - this._maxMarker.textY < this._minFontDistance) {
return;
}
if (this._markers.length === 0) {
this._markers.push(marker);
return;
}
// only push marker when there is enough vertical space to display text without collisions
const prevMarker = this._markers[this._markers.length - 1];
if (prevMarker.textY - marker.textY > this._minFontDistance) {
this._markers.push(marker);
}
}
getMarkers() {
const svgs = this._markers.map((marker: Marker) => {
return marker.svg;
});
return [...svgs, this._maxMarker.svg];
}
}

View file

@ -0,0 +1,214 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { FIELD_ORIGIN } from '../../../../../../../common/constants';
import type { DynamicSizeProperty } from '../../../properties/dynamic_size_property';
import type { IField } from '../../../../../fields/field';
import { MarkerSizeLegend } from './marker_size_legend';
const dynamicSizeOptions = {
minSize: 7,
maxSize: 32,
field: {
name: 'bytes',
origin: FIELD_ORIGIN.SOURCE,
},
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
invert: false,
};
const mockStyle = {
formatField: (value: number) => {
return `${value * 0.001}KB`;
},
getDisplayStyleName: () => {
return 'Symbol size';
},
getField: () => {
return {
getLabel: () => {
return 'bytes';
},
} as unknown as IField;
},
getOptions: () => {
return dynamicSizeOptions;
},
getRangeFieldMeta: () => {
return {
min: 0,
max: 19000,
delta: 19000,
};
},
isFieldMetaEnabled: () => {
return true;
},
} as unknown as DynamicSizeProperty;
test('Should render legend', async () => {
const component = shallow(<MarkerSizeLegend style={mockStyle} />);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render legend with 3 markers when size difference does not provide enough vertical space for more labels', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
maxSize: 24,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render legend with 2 markers when size difference does not provide enough vertical space for more labels', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
maxSize: 15,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render legend with only max marker when size difference does not provide enough vertical space for more labels', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
maxSize: 11,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render legend without label cutoff when min size is 1', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
minSize: 1,
maxSize: 7,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render max label with std clamp notification', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
maxSize: 11,
};
},
getRangeFieldMeta: () => {
return {
min: 0,
max: 16000,
delta: 16000,
isMaxOutsideStdRange: true,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should invert legend', async () => {
const component = shallow(
<MarkerSizeLegend
style={
{
...mockStyle,
getOptions: () => {
return {
...dynamicSizeOptions,
maxSize: 24,
invert: true,
};
},
} as unknown as DynamicSizeProperty
}
/>
);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});

View file

@ -9,13 +9,14 @@ import React, { Component } from 'react';
import _ from 'lodash';
import { euiThemeVars } from '@kbn/ui-theme';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import { RangeFieldMeta } from '../../../../../../common/descriptor_types';
import { DynamicSizeProperty } from '../../properties/dynamic_size_property';
import { RightAlignedText } from './right_aligned_text';
import { RangeFieldMeta } from '../../../../../../../common/descriptor_types';
import { DynamicSizeProperty } from '../../../properties/dynamic_size_property';
import { RightAlignedText } from '../right_aligned_text';
import { getMaxLabel, getMinLabel } from './get_ordinal_label';
import { type Marker, MarkerList } from './marker_list';
const FONT_SIZE = 10;
const HALF_FONT_SIZE = FONT_SIZE / 2;
const MIN_MARKER_DISTANCE = (FONT_SIZE + 2) / 2;
const EMPTY_VALUE = '';
@ -103,29 +104,34 @@ export class MarkerSizeLegend extends Component<Props, State> {
const circleCenterX = options.maxSize + circleStyle.strokeWidth;
const circleBottomY = svgHeight - circleStyle.strokeWidth;
const makeMarker = (radius: number, formattedValue: string | number) => {
const makeMarker = (radius: number, formattedValue: string | number): Marker => {
const circleCenterY = circleBottomY - radius;
const circleTopY = circleCenterY - radius;
const textOffset = this.state.maxLabelWidth + HALF_FONT_SIZE;
return (
<g key={radius}>
<line
style={{ stroke: euiThemeVars.euiBorderColor }}
x1={circleCenterX}
y1={circleTopY}
x2={circleCenterX * 2.25}
y2={circleTopY}
/>
<RightAlignedText
setWidth={this._onRightAlignedWidthChange}
style={{ fontSize: FONT_SIZE, fill: euiThemeVars.euiTextColor }}
x={circleCenterX * 2.25 + textOffset}
y={circleTopY + HALF_FONT_SIZE}
value={formattedValue}
/>
<circle style={circleStyle} cx={circleCenterX} cy={circleCenterY} r={radius} />
</g>
);
const rawTextY = circleTopY + HALF_FONT_SIZE;
const textY = rawTextY > svgHeight ? svgHeight : rawTextY;
return {
svg: (
<g key={radius}>
<line
style={{ stroke: euiThemeVars.euiBorderColor }}
x1={circleCenterX}
y1={circleTopY}
x2={circleCenterX * 2.25}
y2={circleTopY}
/>
<RightAlignedText
setWidth={this._onRightAlignedWidthChange}
style={{ fontSize: FONT_SIZE, fill: euiThemeVars.euiTextColor }}
x={circleCenterX * 2.25 + textOffset}
y={textY}
value={formattedValue}
/>
<circle style={circleStyle} cx={circleCenterX} cy={circleCenterY} r={radius} />
</g>
),
textY,
};
};
function getMarkerRadius(percentage: number) {
@ -142,34 +148,33 @@ export class MarkerSizeLegend extends Component<Props, State> {
return fieldMeta!.delta > 3 ? Math.round(value) : value;
}
const markers = [];
const maxLabel = getMaxLabel(
this.props.style.isFieldMetaEnabled(),
Boolean(fieldMeta.isMaxOutsideStdRange),
this._formatValue(fieldMeta.max)
);
const minLabel = getMinLabel(
this.props.style.isFieldMetaEnabled(),
Boolean(fieldMeta.isMinOutsideStdRange),
this._formatValue(fieldMeta.min)
);
const markerList = new MarkerList(
FONT_SIZE,
makeMarker(options.maxSize, invert ? minLabel : maxLabel)
);
if (fieldMeta.delta > 0) {
const smallestMarker = makeMarker(
options.minSize,
this._formatValue(invert ? fieldMeta.max : fieldMeta.min)
);
markers.push(smallestMarker);
const markerDelta = options.maxSize - options.minSize;
if (markerDelta > MIN_MARKER_DISTANCE * 3) {
markers.push(makeMarker(getMarkerRadius(0.25), this._formatValue(getValue(0.25))));
markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
markers.push(makeMarker(getMarkerRadius(0.75), this._formatValue(getValue(0.75))));
} else if (markerDelta > MIN_MARKER_DISTANCE) {
markers.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
}
markerList.push(makeMarker(options.minSize, invert ? maxLabel : minLabel));
markerList.push(makeMarker(getMarkerRadius(0.25), this._formatValue(getValue(0.25))));
markerList.push(makeMarker(getMarkerRadius(0.5), this._formatValue(getValue(0.5))));
markerList.push(makeMarker(getMarkerRadius(0.75), this._formatValue(getValue(0.75))));
}
const largestMarker = makeMarker(
options.maxSize,
this._formatValue(invert ? fieldMeta.min : fieldMeta.max)
);
markers.push(largestMarker);
return (
<svg height={svgHeight} xmlns="http://www.w3.org/2000/svg">
{markers}
{markerList.getMarkers()}
</svg>
);
}

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { FIELD_ORIGIN, VECTOR_STYLES } from '../../../../../../../common/constants';
import type { DynamicSizeProperty } from '../../../properties/dynamic_size_property';
import type { IField } from '../../../../../fields/field';
import { OrdinalLegend } from './ordinal_legend';
const dynamicSizeOptions = {
minSize: 1,
maxSize: 10,
field: {
name: 'bytes',
origin: FIELD_ORIGIN.SOURCE,
},
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
invert: false,
};
const mockStyle = {
formatField: (value: number) => {
return `${value * 0.001}KB`;
},
getDisplayStyleName: () => {
return 'Border width';
},
getField: () => {
return {
getLabel: () => {
return 'bytes';
},
} as unknown as IField;
},
getOptions: () => {
return dynamicSizeOptions;
},
getRangeFieldMeta: () => {
return {
min: 0,
max: 19000,
delta: 19000,
};
},
getStyleName: () => {
return VECTOR_STYLES.LINE_WIDTH;
},
isFieldMetaEnabled: () => {
return true;
},
} as unknown as DynamicSizeProperty;
test('Should render legend', async () => {
const component = shallow(<OrdinalLegend style={mockStyle} />);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});

View file

@ -8,10 +8,11 @@
import React, { Component, Fragment } from 'react';
import _ from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row';
import { VECTOR_STYLES } from '../../../../../../common/constants';
import { CircleIcon } from './circle_icon';
import { IDynamicStyleProperty } from '../../properties/dynamic_style_property';
import { RangedStyleLegendRow } from '../../../../components/ranged_style_legend_row';
import { VECTOR_STYLES } from '../../../../../../../common/constants';
import { CircleIcon } from '../circle_icon';
import { IDynamicStyleProperty } from '../../../properties/dynamic_style_property';
import { getMaxLabel, getMinLabel } from './get_ordinal_label';
function getLineWidthIcons() {
const defaultStyle = {
@ -130,12 +131,18 @@ export class OrdinalLegend extends Component<Props, State> {
let maxLabel: string | number = EMPTY_VALUE;
if (fieldMeta) {
const min = this._formatValue(_.get(fieldMeta, 'min', EMPTY_VALUE));
minLabel =
this.props.style.isFieldMetaEnabled() && fieldMeta.isMinOutsideStdRange ? `< ${min}` : min;
minLabel = getMinLabel(
this.props.style.isFieldMetaEnabled(),
Boolean(fieldMeta.isMinOutsideStdRange),
min
);
const max = this._formatValue(_.get(fieldMeta, 'max', EMPTY_VALUE));
maxLabel =
this.props.style.isFieldMetaEnabled() && fieldMeta.isMaxOutsideStdRange ? `> ${max}` : max;
maxLabel = getMaxLabel(
this.props.style.isFieldMetaEnabled(),
Boolean(fieldMeta.isMaxOutsideStdRange),
max
);
}
const options = this.props.style.getOptions();

View file

@ -1,236 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderLegendDetailRow Should render icon size scale 1`] = `
exports[`renderLegendDetailRow Should render marker size legend for icon size property 1`] = `
<div>
<EuiFlexGroup
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiToolTip
content="foobar_label"
delay="regular"
display="inlineBlock"
position="top"
title="Symbol size"
>
<EuiText
className="eui-textTruncate"
size="xs"
style={
Object {
"maxWidth": "180px",
}
}
>
<small>
<strong>
foobar_label
</strong>
</small>
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
<svg
height={27}
xmlns="http://www.w3.org/2000/svg"
>
<g
key="0"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={11}
x2={24.75}
y1={26}
y2={26}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="0_format"
x={29.75}
y={31}
/>
<circle
cx={11}
cy={26}
r={0}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="5"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={11}
x2={24.75}
y1={16}
y2={16}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="25_format"
x={29.75}
y={21}
/>
<circle
cx={11}
cy={21}
r={5}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
<g
key="10"
>
<line
style={
Object {
"stroke": "#d3dae6",
}
}
x1={11}
x2={24.75}
y1={6}
y2={6}
/>
<RightAlignedText
setWidth={[Function]}
style={
Object {
"fill": "#343741",
"fontSize": 10,
}
}
value="100_format"
x={29.75}
y={11}
/>
<circle
cx={11}
cy={16}
r={10}
style={
Object {
"fillOpacity": 0,
"stroke": "#343741",
"strokeWidth": 1,
}
}
/>
</g>
</svg>
mockMarkerSizeLegend
</div>
`;
exports[`renderLegendDetailRow Should render line width simple range 1`] = `
<RangedStyleLegendRow
fieldLabel="foobar_label"
header={
<EuiFlexGroup
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "1px",
"width": "12px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "2px",
"width": "12px",
}
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule
margin="xs"
/>
</EuiFlexItem>
</React.Fragment>
<React.Fragment>
<EuiFlexItem
grow={false}
>
<CircleIcon
style={
Object {
"fill": "none",
"stroke": "grey",
"strokeWidth": "3px",
"width": "12px",
}
}
/>
</EuiFlexItem>
</React.Fragment>
</EuiFlexGroup>
}
invert={false}
maxLabel="100_format"
minLabel="0_format"
propertyLabel="Border width"
/>
exports[`renderLegendDetailRow Should render ordinal legend for line width style property 1`] = `
<div>
mockMarkerSizeLegend
</div>
`;

View file

@ -11,6 +11,15 @@ jest.mock('../../components/vector_style_editor', () => ({
},
}));
jest.mock('../../components/legend/size', () => ({
MarkerSizeLegend: () => {
return <div>mockMarkerSizeLegend</div>;
},
OrdinalLegend: () => {
return <div>mockMarkerSizeLegend</div>;
},
}));
import React from 'react';
import { shallow } from 'enzyme';
@ -20,95 +29,37 @@ import { IField } from '../../../../fields/field';
import { IVectorLayer } from '../../../../layers/vector_layer';
describe('renderLegendDetailRow', () => {
test('Should render line width simple range', async () => {
const field = {
getLabel: async () => {
return 'foobar_label';
},
getName: () => {
return 'foodbar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromEs: () => {
return true;
},
supportsFieldMetaFromLocalData: () => {
return true;
},
} as unknown as IField;
test('Should render ordinal legend for line width style property', () => {
const sizeProp = new DynamicSizeProperty(
{ minSize: 0, maxSize: 10, fieldMetaOptions: { isEnabled: true } },
VECTOR_STYLES.LINE_WIDTH,
field,
{} as unknown as IField,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
sizeProp.getRangeFieldMeta = () => {
return {
min: 0,
max: 100,
delta: 100,
};
};
const legendRow = sizeProp.renderLegendDetailRow();
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
test('Should render icon size scale', async () => {
const field = {
getLabel: async () => {
return 'foobar_label';
},
getName: () => {
return 'foodbar';
},
getOrigin: () => {
return FIELD_ORIGIN.SOURCE;
},
supportsFieldMetaFromEs: () => {
return true;
},
supportsFieldMetaFromLocalData: () => {
return true;
},
} as unknown as IField;
test('Should render marker size legend for icon size property', () => {
const sizeProp = new DynamicSizeProperty(
{ minSize: 0, maxSize: 10, fieldMetaOptions: { isEnabled: true } },
VECTOR_STYLES.ICON_SIZE,
field,
{} as unknown as IField,
{} as unknown as IVectorLayer,
() => {
return (value: RawValue) => value + '_format';
},
false
);
sizeProp.getRangeFieldMeta = () => {
return {
min: 0,
max: 100,
delta: 100,
};
};
const legendRow = sizeProp.renderLegendDetailRow();
const component = shallow(legendRow);
// Ensure all promises resolve
await new Promise((resolve) => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -8,8 +8,7 @@
import React from 'react';
import type { Map as MbMap } from '@kbn/mapbox-gl';
import { DynamicStyleProperty } from '../dynamic_style_property';
import { OrdinalLegend } from '../../components/legend/ordinal_legend';
import { MarkerSizeLegend } from '../../components/legend/marker_size_legend';
import { MarkerSizeLegend, OrdinalLegend } from '../../components/legend/size';
import { makeMbClampedNumberExpression } from '../../style_util';
import {
FieldFormatter,