Skip to content

Commit 5ab46ca

Browse files
authored
feat: compute global y domain on multiple groups (#348)
This commit add a new property for the series: useDefaultGroupDomain that allows to split series by groupId but keeps and compute the new groupId domain merging it with the global one fix #169, fix #185
1 parent 0247b4a commit 5ab46ca

File tree

11 files changed

+163
-209
lines changed

11 files changed

+163
-209
lines changed

.playground/playgroud.tsx

Lines changed: 42 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,66 +9,23 @@ import {
99
Position,
1010
ScaleType,
1111
Settings,
12-
BarSeries,
13-
LineAnnotation,
14-
getAnnotationId,
15-
AnnotationDomainTypes,
12+
AreaSeries,
13+
getGroupId,
1614
} from '../src';
1715
import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';
18-
import { CursorEvent } from '../src/specs/settings';
19-
import { CursorUpdateListener } from '../src/chart_types/xy_chart/store/chart_state';
20-
import { Icon } from '../src/components/icons/icon';
21-
2216
export class Playground extends React.Component {
2317
ref1 = React.createRef<Chart>();
2418
ref2 = React.createRef<Chart>();
2519
ref3 = React.createRef<Chart>();
2620

27-
onCursorUpdate: CursorUpdateListener = (event?: CursorEvent) => {
28-
this.ref1.current!.dispatchExternalCursorEvent(event);
29-
this.ref2.current!.dispatchExternalCursorEvent(event);
30-
this.ref3.current!.dispatchExternalCursorEvent(event);
31-
};
32-
3321
render() {
3422
return (
35-
<>
36-
{renderChart(
37-
'1',
38-
this.ref1,
39-
KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 15),
40-
this.onCursorUpdate,
41-
true,
42-
)}
43-
{renderChart(
44-
'2',
45-
this.ref2,
46-
KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 15),
47-
this.onCursorUpdate,
48-
true,
49-
)}
50-
{renderChart('3', this.ref3, KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(15, 30), this.onCursorUpdate)}
51-
</>
52-
);
53-
}
54-
}
55-
56-
function renderChart(
57-
key: string,
58-
ref: React.RefObject<Chart>,
59-
data: any,
60-
onCursorUpdate?: CursorUpdateListener,
61-
timeSeries: boolean = false,
62-
) {
63-
return (
64-
<div key={key} className="chart">
65-
<Chart ref={ref}>
23+
<Chart>
6624
<Settings
6725
tooltip={{ type: 'vertical' }}
6826
debug={false}
6927
legendPosition={Position.Right}
7028
showLegend={true}
71-
onCursorUpdate={onCursorUpdate}
7229
rotation={0}
7330
/>
7431
<Axis
@@ -77,41 +34,55 @@ function renderChart(
7734
position={Position.Bottom}
7835
tickFormat={niceTimeFormatter([1555819200000, 1555905600000])}
7936
/>
80-
<Axis id={getAxisId('count')} title="count" position={Position.Left} tickFormat={(d) => d.toFixed(2)} />
81-
<LineAnnotation
82-
annotationId={getAnnotationId('annotation1')}
83-
domainType={AnnotationDomainTypes.XDomain}
84-
dataValues={[
85-
{
86-
dataValue: KIBANA_METRICS.metrics.kibana_os_load[1].data[5][0],
87-
details: 'tooltip 1',
88-
},
89-
{
90-
dataValue: KIBANA_METRICS.metrics.kibana_os_load[1].data[9][0],
91-
details: 'tooltip 2',
92-
},
93-
]}
94-
hideLinesTooltips={true}
95-
marker={<Icon type="alert" />}
37+
<Axis id={getAxisId('A axis')} title="A" position={Position.Left} tickFormat={(d) => `GA: ${d.toFixed(2)}`} />
38+
<Axis
39+
id={getAxisId('B axis')}
40+
groupId={getGroupId('aaa')}
41+
title="B"
42+
hide={true}
43+
position={Position.Left}
44+
tickFormat={(d) => `GB: ${d.toFixed(2)}`}
9645
/>
97-
<BarSeries
98-
id={getSpecId('dataset A with long title')}
99-
xScaleType={timeSeries ? ScaleType.Time : ScaleType.Linear}
46+
<AreaSeries
47+
id={getSpecId('dataset A1')}
48+
xScaleType={ScaleType.Linear}
10049
yScaleType={ScaleType.Linear}
101-
data={data}
50+
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50)}
10251
xAccessor={0}
10352
yAccessors={[1]}
53+
stackAccessors={[0]}
10454
/>
105-
<BarSeries
106-
id={getSpecId('dataset B')}
55+
<AreaSeries
56+
id={getSpecId('dataset A2')}
57+
xScaleType={ScaleType.Linear}
58+
yScaleType={ScaleType.Linear}
59+
data={KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 50)}
60+
xAccessor={0}
61+
yAccessors={[1]}
62+
stackAccessors={[0]}
63+
/>
64+
<AreaSeries
65+
id={getSpecId('dataset B1')}
66+
groupId={getGroupId('aaa')}
67+
useDefaultGroupDomain={true}
68+
xScaleType={ScaleType.Time}
69+
yScaleType={ScaleType.Linear}
70+
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50).map((d) => [d[0], -d[1]])}
71+
xAccessor={0}
72+
yAccessors={[1]}
73+
stackAccessors={[0]}
74+
/>
75+
<AreaSeries
76+
id={getSpecId('dataset B2')}
77+
groupId={getGroupId('aaa')}
10778
xScaleType={ScaleType.Time}
10879
yScaleType={ScaleType.Linear}
109-
data={KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 15)}
80+
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50).map((d) => [d[0], -d[1]])}
11081
xAccessor={0}
11182
yAccessors={[1]}
11283
stackAccessors={[0]}
11384
/>
11485
</Chart>
115-
</div>
116-
);
86+
);
87+
}
11788
}

src/chart_types/xy_chart/domains/y_domain.ts

Lines changed: 103 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BasicSeriesSpec, DomainRange } from '../utils/specs';
2-
import { GroupId, SpecId } from '../../../utils/ids';
1+
import { BasicSeriesSpec, DomainRange, DEFAULT_GLOBAL_ID } from '../utils/specs';
2+
import { GroupId, SpecId, getGroupId } from '../../../utils/ids';
33
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
44
import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_utils';
55
import { BaseDomain } from './domain';
@@ -16,81 +16,121 @@ export type YDomain = BaseDomain & {
1616
};
1717
export type YBasicSeriesSpec = Pick<
1818
BasicSeriesSpec,
19-
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'styleAccessor'
19+
| 'id'
20+
| 'seriesType'
21+
| 'yScaleType'
22+
| 'groupId'
23+
| 'stackAccessors'
24+
| 'yScaleToDataExtent'
25+
| 'styleAccessor'
26+
| 'useDefaultGroupDomain'
2027
> & { stackAsPercentage?: boolean };
2128

29+
interface GroupSpecs {
30+
isPercentageStack: boolean;
31+
stacked: YBasicSeriesSpec[];
32+
nonStacked: YBasicSeriesSpec[];
33+
}
34+
2235
export function mergeYDomain(
2336
dataSeries: Map<SpecId, RawDataSeries[]>,
2437
specs: YBasicSeriesSpec[],
2538
domainsByGroupId: Map<GroupId, DomainRange>,
2639
): YDomain[] {
2740
// group specs by group ids
2841
const specsByGroupIds = splitSpecsByGroupId(specs);
29-
3042
const specsByGroupIdsEntries = [...specsByGroupIds.entries()];
43+
const globalId = getGroupId(DEFAULT_GLOBAL_ID);
3144

32-
const yDomains = specsByGroupIdsEntries.map(
33-
([groupId, groupSpecs]): YDomain => {
34-
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
35-
const { isPercentageStack } = groupSpecs;
36-
37-
let domain: number[];
38-
if (isPercentageStack) {
39-
domain = computeContinuousDataDomain([0, 1], identity);
40-
} else {
41-
// compute stacked domain
42-
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
43-
return spec.yScaleToDataExtent;
44-
});
45-
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
46-
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);
47-
48-
// compute non stacked domain
49-
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
50-
return spec.yScaleToDataExtent;
51-
});
52-
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
53-
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);
54-
55-
// merge stacked and non stacked domain together
56-
domain = computeContinuousDataDomain(
57-
[...stackedDomain, ...nonStackedDomain],
58-
identity,
59-
isStackedScaleToExtent || isNonStackedScaleToExtent,
60-
);
61-
62-
const [computedDomainMin, computedDomainMax] = domain;
63-
64-
const customDomain = domainsByGroupId.get(groupId);
65-
66-
if (customDomain && isCompleteBound(customDomain)) {
67-
// Don't need to check min > max because this has been validated on axis domain merge
68-
domain = [customDomain.min, customDomain.max];
69-
} else if (customDomain && isLowerBound(customDomain)) {
70-
if (customDomain.min > computedDomainMax) {
71-
throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`);
72-
}
73-
74-
domain = [customDomain.min, computedDomainMax];
75-
} else if (customDomain && isUpperBound(customDomain)) {
76-
if (computedDomainMin > customDomain.max) {
77-
throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`);
78-
}
79-
80-
domain = [computedDomainMin, customDomain.max];
81-
}
82-
}
45+
const yDomains = specsByGroupIdsEntries.map<YDomain>(([groupId, groupSpecs]) => {
46+
const customDomain = domainsByGroupId.get(groupId);
47+
return mergeYDomainForGroup(dataSeries, groupId, groupSpecs, customDomain);
48+
});
49+
50+
const globalGroupIds: Set<GroupId> = specs.reduce<Set<GroupId>>((acc, { groupId, useDefaultGroupDomain }) => {
51+
if (groupId !== globalId && useDefaultGroupDomain) {
52+
acc.add(groupId);
53+
}
54+
return acc;
55+
}, new Set());
56+
globalGroupIds.add(globalId);
57+
58+
const globalYDomains = yDomains.filter((domain) => globalGroupIds.has(domain.groupId));
59+
let globalYDomain = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER];
60+
globalYDomains.forEach((domain) => {
61+
globalYDomain = [Math.min(globalYDomain[0], domain.domain[0]), Math.max(globalYDomain[1], domain.domain[1])];
62+
});
63+
return yDomains.map((domain) => {
64+
if (globalGroupIds.has(domain.groupId)) {
8365
return {
84-
type: 'yDomain',
85-
isBandScale: false,
86-
scaleType: groupYScaleType,
87-
groupId,
88-
domain,
66+
...domain,
67+
domain: globalYDomain,
8968
};
90-
},
91-
);
69+
}
70+
return domain;
71+
});
72+
}
73+
74+
function mergeYDomainForGroup(
75+
dataSeries: Map<SpecId, RawDataSeries[]>,
76+
groupId: GroupId,
77+
groupSpecs: GroupSpecs,
78+
customDomain?: DomainRange,
79+
): YDomain {
80+
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
81+
const { isPercentageStack } = groupSpecs;
82+
83+
let domain: number[];
84+
if (isPercentageStack) {
85+
domain = computeContinuousDataDomain([0, 1], identity);
86+
} else {
87+
// compute stacked domain
88+
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
89+
return spec.yScaleToDataExtent;
90+
});
91+
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
92+
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);
9293

93-
return yDomains;
94+
// compute non stacked domain
95+
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
96+
return spec.yScaleToDataExtent;
97+
});
98+
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
99+
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);
100+
101+
// merge stacked and non stacked domain together
102+
domain = computeContinuousDataDomain(
103+
[...stackedDomain, ...nonStackedDomain],
104+
identity,
105+
isStackedScaleToExtent || isNonStackedScaleToExtent,
106+
);
107+
108+
const [computedDomainMin, computedDomainMax] = domain;
109+
110+
if (customDomain && isCompleteBound(customDomain)) {
111+
// Don't need to check min > max because this has been validated on axis domain merge
112+
domain = [customDomain.min, customDomain.max];
113+
} else if (customDomain && isLowerBound(customDomain)) {
114+
if (customDomain.min > computedDomainMax) {
115+
throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`);
116+
}
117+
118+
domain = [customDomain.min, computedDomainMax];
119+
} else if (customDomain && isUpperBound(customDomain)) {
120+
if (computedDomainMin > customDomain.max) {
121+
throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`);
122+
}
123+
124+
domain = [computedDomainMin, customDomain.max];
125+
}
126+
}
127+
return {
128+
type: 'yDomain',
129+
isBandScale: false,
130+
scaleType: groupYScaleType,
131+
groupId: groupId,
132+
domain,
133+
};
94134
}
95135

96136
export function getDataSeriesOnGroup(

src/chart_types/xy_chart/specs/area_series.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { inject } from 'mobx-react';
22
import { PureComponent } from 'react';
3-
import { AreaSeriesSpec, HistogramModeAlignments } from '../utils/specs';
3+
import { AreaSeriesSpec, HistogramModeAlignments, DEFAULT_GLOBAL_ID } from '../utils/specs';
44
import { getGroupId } from '../../../utils/ids';
55
import { ScaleType } from '../../../utils/scales/scales';
66
import { SpecProps } from '../../../specs/specs_parser';
@@ -10,7 +10,7 @@ type AreaSpecProps = SpecProps & AreaSeriesSpec;
1010
export class AreaSeriesSpecComponent extends PureComponent<AreaSpecProps> {
1111
static defaultProps: Partial<AreaSpecProps> = {
1212
seriesType: 'area',
13-
groupId: getGroupId('__global__'),
13+
groupId: getGroupId(DEFAULT_GLOBAL_ID),
1414
xScaleType: ScaleType.Ordinal,
1515
yScaleType: ScaleType.Linear,
1616
xAccessor: 'x',

src/chart_types/xy_chart/specs/axis.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { inject } from 'mobx-react';
22
import { PureComponent } from 'react';
3-
import { AxisSpec as AxisSpecType, Position } from '../utils/specs';
3+
import { AxisSpec as AxisSpecType, Position, DEFAULT_GLOBAL_ID } from '../utils/specs';
44
import { getGroupId } from '../../../utils/ids';
55
import { SpecProps } from '../../../specs/specs_parser';
66

77
type AxisSpecProps = SpecProps & AxisSpecType;
88

99
class AxisSpec extends PureComponent<AxisSpecProps> {
1010
static defaultProps: Partial<AxisSpecProps> = {
11-
groupId: getGroupId('__global__'),
11+
groupId: getGroupId(DEFAULT_GLOBAL_ID),
1212
hide: false,
1313
showOverlappingTicks: false,
1414
showOverlappingLabels: false,

src/chart_types/xy_chart/specs/bar_series.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { inject } from 'mobx-react';
22
import { PureComponent } from 'react';
3-
import { BarSeriesSpec } from '../utils/specs';
3+
import { BarSeriesSpec, DEFAULT_GLOBAL_ID } from '../utils/specs';
44
import { getGroupId } from '../../../utils/ids';
55
import { ScaleType } from '../../../utils/scales/scales';
66
import { SpecProps } from '../../../specs/specs_parser';
@@ -10,7 +10,7 @@ type BarSpecProps = SpecProps & BarSeriesSpec;
1010
export class BarSeriesSpecComponent extends PureComponent<BarSpecProps> {
1111
static defaultProps: Partial<BarSpecProps> = {
1212
seriesType: 'bar',
13-
groupId: getGroupId('__global__'),
13+
groupId: getGroupId(DEFAULT_GLOBAL_ID),
1414
xScaleType: ScaleType.Ordinal,
1515
yScaleType: ScaleType.Linear,
1616
xAccessor: 'x',

0 commit comments

Comments
 (0)