Skip to content

Commit

Permalink
feat: compute global y domain on multiple groups (#348)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
markov00 authored Aug 26, 2019
1 parent 0247b4a commit 5ab46ca
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 209 deletions.
113 changes: 42 additions & 71 deletions .playground/playgroud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,66 +9,23 @@ import {
Position,
ScaleType,
Settings,
BarSeries,
LineAnnotation,
getAnnotationId,
AnnotationDomainTypes,
AreaSeries,
getGroupId,
} from '../src';
import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';
import { CursorEvent } from '../src/specs/settings';
import { CursorUpdateListener } from '../src/chart_types/xy_chart/store/chart_state';
import { Icon } from '../src/components/icons/icon';

export class Playground extends React.Component {
ref1 = React.createRef<Chart>();
ref2 = React.createRef<Chart>();
ref3 = React.createRef<Chart>();

onCursorUpdate: CursorUpdateListener = (event?: CursorEvent) => {
this.ref1.current!.dispatchExternalCursorEvent(event);
this.ref2.current!.dispatchExternalCursorEvent(event);
this.ref3.current!.dispatchExternalCursorEvent(event);
};

render() {
return (
<>
{renderChart(
'1',
this.ref1,
KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 15),
this.onCursorUpdate,
true,
)}
{renderChart(
'2',
this.ref2,
KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 15),
this.onCursorUpdate,
true,
)}
{renderChart('3', this.ref3, KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(15, 30), this.onCursorUpdate)}
</>
);
}
}

function renderChart(
key: string,
ref: React.RefObject<Chart>,
data: any,
onCursorUpdate?: CursorUpdateListener,
timeSeries: boolean = false,
) {
return (
<div key={key} className="chart">
<Chart ref={ref}>
<Chart>
<Settings
tooltip={{ type: 'vertical' }}
debug={false}
legendPosition={Position.Right}
showLegend={true}
onCursorUpdate={onCursorUpdate}
rotation={0}
/>
<Axis
Expand All @@ -77,41 +34,55 @@ function renderChart(
position={Position.Bottom}
tickFormat={niceTimeFormatter([1555819200000, 1555905600000])}
/>
<Axis id={getAxisId('count')} title="count" position={Position.Left} tickFormat={(d) => d.toFixed(2)} />
<LineAnnotation
annotationId={getAnnotationId('annotation1')}
domainType={AnnotationDomainTypes.XDomain}
dataValues={[
{
dataValue: KIBANA_METRICS.metrics.kibana_os_load[1].data[5][0],
details: 'tooltip 1',
},
{
dataValue: KIBANA_METRICS.metrics.kibana_os_load[1].data[9][0],
details: 'tooltip 2',
},
]}
hideLinesTooltips={true}
marker={<Icon type="alert" />}
<Axis id={getAxisId('A axis')} title="A" position={Position.Left} tickFormat={(d) => `GA: ${d.toFixed(2)}`} />
<Axis
id={getAxisId('B axis')}
groupId={getGroupId('aaa')}
title="B"
hide={true}
position={Position.Left}
tickFormat={(d) => `GB: ${d.toFixed(2)}`}
/>
<BarSeries
id={getSpecId('dataset A with long title')}
xScaleType={timeSeries ? ScaleType.Time : ScaleType.Linear}
<AreaSeries
id={getSpecId('dataset A1')}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
data={data}
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50)}
xAccessor={0}
yAccessors={[1]}
stackAccessors={[0]}
/>
<BarSeries
id={getSpecId('dataset B')}
<AreaSeries
id={getSpecId('dataset A2')}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
data={KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 50)}
xAccessor={0}
yAccessors={[1]}
stackAccessors={[0]}
/>
<AreaSeries
id={getSpecId('dataset B1')}
groupId={getGroupId('aaa')}
useDefaultGroupDomain={true}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50).map((d) => [d[0], -d[1]])}
xAccessor={0}
yAccessors={[1]}
stackAccessors={[0]}
/>
<AreaSeries
id={getSpecId('dataset B2')}
groupId={getGroupId('aaa')}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
data={KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 15)}
data={KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 50).map((d) => [d[0], -d[1]])}
xAccessor={0}
yAccessors={[1]}
stackAccessors={[0]}
/>
</Chart>
</div>
);
);
}
}
166 changes: 103 additions & 63 deletions src/chart_types/xy_chart/domains/y_domain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BasicSeriesSpec, DomainRange } from '../utils/specs';
import { GroupId, SpecId } from '../../../utils/ids';
import { BasicSeriesSpec, DomainRange, DEFAULT_GLOBAL_ID } from '../utils/specs';
import { GroupId, SpecId, getGroupId } from '../../../utils/ids';
import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales';
import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_utils';
import { BaseDomain } from './domain';
Expand All @@ -16,81 +16,121 @@ export type YDomain = BaseDomain & {
};
export type YBasicSeriesSpec = Pick<
BasicSeriesSpec,
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'styleAccessor'
| 'id'
| 'seriesType'
| 'yScaleType'
| 'groupId'
| 'stackAccessors'
| 'yScaleToDataExtent'
| 'styleAccessor'
| 'useDefaultGroupDomain'
> & { stackAsPercentage?: boolean };

interface GroupSpecs {
isPercentageStack: boolean;
stacked: YBasicSeriesSpec[];
nonStacked: YBasicSeriesSpec[];
}

export function mergeYDomain(
dataSeries: Map<SpecId, RawDataSeries[]>,
specs: YBasicSeriesSpec[],
domainsByGroupId: Map<GroupId, DomainRange>,
): YDomain[] {
// group specs by group ids
const specsByGroupIds = splitSpecsByGroupId(specs);

const specsByGroupIdsEntries = [...specsByGroupIds.entries()];
const globalId = getGroupId(DEFAULT_GLOBAL_ID);

const yDomains = specsByGroupIdsEntries.map(
([groupId, groupSpecs]): YDomain => {
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
const { isPercentageStack } = groupSpecs;

let domain: number[];
if (isPercentageStack) {
domain = computeContinuousDataDomain([0, 1], identity);
} else {
// compute stacked domain
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);

// compute non stacked domain
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);

// merge stacked and non stacked domain together
domain = computeContinuousDataDomain(
[...stackedDomain, ...nonStackedDomain],
identity,
isStackedScaleToExtent || isNonStackedScaleToExtent,
);

const [computedDomainMin, computedDomainMax] = domain;

const customDomain = domainsByGroupId.get(groupId);

if (customDomain && isCompleteBound(customDomain)) {
// Don't need to check min > max because this has been validated on axis domain merge
domain = [customDomain.min, customDomain.max];
} else if (customDomain && isLowerBound(customDomain)) {
if (customDomain.min > computedDomainMax) {
throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`);
}

domain = [customDomain.min, computedDomainMax];
} else if (customDomain && isUpperBound(customDomain)) {
if (computedDomainMin > customDomain.max) {
throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`);
}

domain = [computedDomainMin, customDomain.max];
}
}
const yDomains = specsByGroupIdsEntries.map<YDomain>(([groupId, groupSpecs]) => {
const customDomain = domainsByGroupId.get(groupId);
return mergeYDomainForGroup(dataSeries, groupId, groupSpecs, customDomain);
});

const globalGroupIds: Set<GroupId> = specs.reduce<Set<GroupId>>((acc, { groupId, useDefaultGroupDomain }) => {
if (groupId !== globalId && useDefaultGroupDomain) {
acc.add(groupId);
}
return acc;
}, new Set());
globalGroupIds.add(globalId);

const globalYDomains = yDomains.filter((domain) => globalGroupIds.has(domain.groupId));
let globalYDomain = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER];
globalYDomains.forEach((domain) => {
globalYDomain = [Math.min(globalYDomain[0], domain.domain[0]), Math.max(globalYDomain[1], domain.domain[1])];
});
return yDomains.map((domain) => {
if (globalGroupIds.has(domain.groupId)) {
return {
type: 'yDomain',
isBandScale: false,
scaleType: groupYScaleType,
groupId,
domain,
...domain,
domain: globalYDomain,
};
},
);
}
return domain;
});
}

function mergeYDomainForGroup(
dataSeries: Map<SpecId, RawDataSeries[]>,
groupId: GroupId,
groupSpecs: GroupSpecs,
customDomain?: DomainRange,
): YDomain {
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);
const { isPercentageStack } = groupSpecs;

let domain: number[];
if (isPercentageStack) {
domain = computeContinuousDataDomain([0, 1], identity);
} else {
// compute stacked domain
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);

return yDomains;
// compute non stacked domain
const isNonStackedScaleToExtent = groupSpecs.nonStacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const nonStackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.nonStacked);
const nonStackedDomain = computeYNonStackedDomain(nonStackedDataSeries, isNonStackedScaleToExtent);

// merge stacked and non stacked domain together
domain = computeContinuousDataDomain(
[...stackedDomain, ...nonStackedDomain],
identity,
isStackedScaleToExtent || isNonStackedScaleToExtent,
);

const [computedDomainMin, computedDomainMax] = domain;

if (customDomain && isCompleteBound(customDomain)) {
// Don't need to check min > max because this has been validated on axis domain merge
domain = [customDomain.min, customDomain.max];
} else if (customDomain && isLowerBound(customDomain)) {
if (customDomain.min > computedDomainMax) {
throw new Error(`custom yDomain for ${groupId} is invalid, custom min is greater than computed max`);
}

domain = [customDomain.min, computedDomainMax];
} else if (customDomain && isUpperBound(customDomain)) {
if (computedDomainMin > customDomain.max) {
throw new Error(`custom yDomain for ${groupId} is invalid, computed min is greater than custom max`);
}

domain = [computedDomainMin, customDomain.max];
}
}
return {
type: 'yDomain',
isBandScale: false,
scaleType: groupYScaleType,
groupId: groupId,
domain,
};
}

export function getDataSeriesOnGroup(
Expand Down
4 changes: 2 additions & 2 deletions src/chart_types/xy_chart/specs/area_series.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { inject } from 'mobx-react';
import { PureComponent } from 'react';
import { AreaSeriesSpec, HistogramModeAlignments } from '../utils/specs';
import { AreaSeriesSpec, HistogramModeAlignments, DEFAULT_GLOBAL_ID } from '../utils/specs';
import { getGroupId } from '../../../utils/ids';
import { ScaleType } from '../../../utils/scales/scales';
import { SpecProps } from '../../../specs/specs_parser';
Expand All @@ -10,7 +10,7 @@ type AreaSpecProps = SpecProps & AreaSeriesSpec;
export class AreaSeriesSpecComponent extends PureComponent<AreaSpecProps> {
static defaultProps: Partial<AreaSpecProps> = {
seriesType: 'area',
groupId: getGroupId('__global__'),
groupId: getGroupId(DEFAULT_GLOBAL_ID),
xScaleType: ScaleType.Ordinal,
yScaleType: ScaleType.Linear,
xAccessor: 'x',
Expand Down
4 changes: 2 additions & 2 deletions src/chart_types/xy_chart/specs/axis.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { inject } from 'mobx-react';
import { PureComponent } from 'react';
import { AxisSpec as AxisSpecType, Position } from '../utils/specs';
import { AxisSpec as AxisSpecType, Position, DEFAULT_GLOBAL_ID } from '../utils/specs';
import { getGroupId } from '../../../utils/ids';
import { SpecProps } from '../../../specs/specs_parser';

type AxisSpecProps = SpecProps & AxisSpecType;

class AxisSpec extends PureComponent<AxisSpecProps> {
static defaultProps: Partial<AxisSpecProps> = {
groupId: getGroupId('__global__'),
groupId: getGroupId(DEFAULT_GLOBAL_ID),
hide: false,
showOverlappingTicks: false,
showOverlappingLabels: false,
Expand Down
4 changes: 2 additions & 2 deletions src/chart_types/xy_chart/specs/bar_series.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { inject } from 'mobx-react';
import { PureComponent } from 'react';
import { BarSeriesSpec } from '../utils/specs';
import { BarSeriesSpec, DEFAULT_GLOBAL_ID } from '../utils/specs';
import { getGroupId } from '../../../utils/ids';
import { ScaleType } from '../../../utils/scales/scales';
import { SpecProps } from '../../../specs/specs_parser';
Expand All @@ -10,7 +10,7 @@ type BarSpecProps = SpecProps & BarSeriesSpec;
export class BarSeriesSpecComponent extends PureComponent<BarSpecProps> {
static defaultProps: Partial<BarSpecProps> = {
seriesType: 'bar',
groupId: getGroupId('__global__'),
groupId: getGroupId(DEFAULT_GLOBAL_ID),
xScaleType: ScaleType.Ordinal,
yScaleType: ScaleType.Linear,
xAccessor: 'x',
Expand Down
Loading

0 comments on commit 5ab46ca

Please sign in to comment.