diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index b1b7b0c541..94ba13df59 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -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(); ref2 = React.createRef(); ref3 = React.createRef(); - 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, - data: any, - onCursorUpdate?: CursorUpdateListener, - timeSeries: boolean = false, -) { - return ( -
- + - d.toFixed(2)} /> - } + `GA: ${d.toFixed(2)}`} /> + `GB: ${d.toFixed(2)}`} /> - - + [d[0], -d[1]])} + xAccessor={0} + yAccessors={[1]} + stackAccessors={[0]} + /> + [d[0], -d[1]])} xAccessor={0} yAccessors={[1]} stackAccessors={[0]} /> -
- ); + ); + } } diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index f459d66ffb..d35bedad2f 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -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'; @@ -16,9 +16,22 @@ 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, specs: YBasicSeriesSpec[], @@ -26,71 +39,98 @@ export function mergeYDomain( ): 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(([groupId, groupSpecs]) => { + const customDomain = domainsByGroupId.get(groupId); + return mergeYDomainForGroup(dataSeries, groupId, groupSpecs, customDomain); + }); + + const globalGroupIds: Set = specs.reduce>((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, + 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( diff --git a/src/chart_types/xy_chart/specs/area_series.tsx b/src/chart_types/xy_chart/specs/area_series.tsx index 75d599b672..5151a27917 100644 --- a/src/chart_types/xy_chart/specs/area_series.tsx +++ b/src/chart_types/xy_chart/specs/area_series.tsx @@ -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'; @@ -10,7 +10,7 @@ type AreaSpecProps = SpecProps & AreaSeriesSpec; export class AreaSeriesSpecComponent extends PureComponent { static defaultProps: Partial = { seriesType: 'area', - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, xAccessor: 'x', diff --git a/src/chart_types/xy_chart/specs/axis.tsx b/src/chart_types/xy_chart/specs/axis.tsx index db31e68b25..d195bb0e7a 100644 --- a/src/chart_types/xy_chart/specs/axis.tsx +++ b/src/chart_types/xy_chart/specs/axis.tsx @@ -1,6 +1,6 @@ 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'; @@ -8,7 +8,7 @@ type AxisSpecProps = SpecProps & AxisSpecType; class AxisSpec extends PureComponent { static defaultProps: Partial = { - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), hide: false, showOverlappingTicks: false, showOverlappingLabels: false, diff --git a/src/chart_types/xy_chart/specs/bar_series.tsx b/src/chart_types/xy_chart/specs/bar_series.tsx index 791e073677..91c81ad013 100644 --- a/src/chart_types/xy_chart/specs/bar_series.tsx +++ b/src/chart_types/xy_chart/specs/bar_series.tsx @@ -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'; @@ -10,7 +10,7 @@ type BarSpecProps = SpecProps & BarSeriesSpec; export class BarSeriesSpecComponent extends PureComponent { static defaultProps: Partial = { seriesType: 'bar', - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, xAccessor: 'x', diff --git a/src/chart_types/xy_chart/specs/histogram_bar_series.tsx b/src/chart_types/xy_chart/specs/histogram_bar_series.tsx index 64d6374891..70f5621a6c 100644 --- a/src/chart_types/xy_chart/specs/histogram_bar_series.tsx +++ b/src/chart_types/xy_chart/specs/histogram_bar_series.tsx @@ -1,6 +1,6 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; -import { HistogramBarSeriesSpec } from '../utils/specs'; +import { HistogramBarSeriesSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; import { getGroupId } from '../../../utils/ids'; import { ScaleType } from '../../../utils/scales/scales'; import { SpecProps } from '../../../specs/specs_parser'; @@ -10,7 +10,7 @@ type HistogramBarSpecProps = SpecProps & HistogramBarSeriesSpec; export class HistogramBarSeriesSpecComponent extends PureComponent { static defaultProps: Partial = { seriesType: 'bar', - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, xAccessor: 'x', diff --git a/src/chart_types/xy_chart/specs/line_annotation.tsx b/src/chart_types/xy_chart/specs/line_annotation.tsx index 9d0b4e4d0a..518de5c72a 100644 --- a/src/chart_types/xy_chart/specs/line_annotation.tsx +++ b/src/chart_types/xy_chart/specs/line_annotation.tsx @@ -1,6 +1,6 @@ import { inject } from 'mobx-react'; import React, { createRef, CSSProperties, PureComponent } from 'react'; -import { LineAnnotationSpec } from '../utils/specs'; +import { LineAnnotationSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { getGroupId } from '../../../utils/ids'; import { SpecProps } from '../../../specs/specs_parser'; @@ -9,7 +9,7 @@ type LineAnnotationProps = SpecProps & LineAnnotationSpec; export class LineAnnotationSpecComponent extends PureComponent { static defaultProps: Partial = { - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), annotationType: 'line', style: DEFAULT_ANNOTATION_LINE_STYLE, hideLines: false, diff --git a/src/chart_types/xy_chart/specs/line_series.tsx b/src/chart_types/xy_chart/specs/line_series.tsx index ed9fd19544..acf5023080 100644 --- a/src/chart_types/xy_chart/specs/line_series.tsx +++ b/src/chart_types/xy_chart/specs/line_series.tsx @@ -1,6 +1,6 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; -import { HistogramModeAlignments, LineSeriesSpec } from '../utils/specs'; +import { HistogramModeAlignments, LineSeriesSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; import { getGroupId } from '../../../utils/ids'; import { ScaleType } from '../../../utils/scales/scales'; import { SpecProps } from '../../../specs/specs_parser'; @@ -10,7 +10,7 @@ type LineSpecProps = SpecProps & LineSeriesSpec; export class LineSeriesSpecComponent extends PureComponent { static defaultProps: Partial = { seriesType: 'line', - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, xAccessor: 'x', diff --git a/src/chart_types/xy_chart/specs/rect_annotation.tsx b/src/chart_types/xy_chart/specs/rect_annotation.tsx index 948a406904..f31788fc7b 100644 --- a/src/chart_types/xy_chart/specs/rect_annotation.tsx +++ b/src/chart_types/xy_chart/specs/rect_annotation.tsx @@ -1,6 +1,6 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; -import { RectAnnotationSpec } from '../utils/specs'; +import { RectAnnotationSpec, DEFAULT_GLOBAL_ID } from '../utils/specs'; import { getGroupId } from '../../../utils/ids'; import { SpecProps } from '../../../specs/specs_parser'; @@ -8,7 +8,7 @@ type RectAnnotationProps = SpecProps & RectAnnotationSpec; export class RectAnnotationSpecComponent extends PureComponent { static defaultProps: Partial = { - groupId: getGroupId('__global__'), + groupId: getGroupId(DEFAULT_GLOBAL_ID), annotationType: 'rectangle', zIndex: -1, }; diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index c988ae15e5..8ebcda6f9b 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -21,6 +21,7 @@ export type Rendering = 'canvas' | 'svg'; export type Color = string; export type StyleOverride = RecursivePartial | Color | null; export type StyleAccessor = (datum: RawDataSeriesDatum, geometryId: GeometryId) => StyleOverride; +export const DEFAULT_GLOBAL_ID = '__global__'; interface DomainMinInterval { /** Custom minInterval for the domain which will affect data bucket size. @@ -73,6 +74,8 @@ export interface SeriesSpec { * @default __global__ */ groupId: GroupId; + /** when using a different groupId this option will allow compute in the same domain of the global domain */ + useDefaultGroupDomain?: boolean; /** An array of data */ data: Datum[]; /** The type of series you are looking to render */ diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index 86350dc288..571e7d9e45 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -47,67 +47,7 @@ export class AreaGeometries extends React.PureComponent { - const { sharedStyle, highlightedLegendItem } = this.props; - const areas = this.props.areas.reduce<{ - stacked: AreaGeometry[]; - nonStacked: AreaGeometry[]; - }>( - (acc, area) => { - if (area.isStacked) { - acc.stacked.push(area); - } else { - acc.nonStacked.push(area); - } - return acc; - }, - - { stacked: [], nonStacked: [] }, - ); - - return [ - ...this.renderStackedAreas(areas.stacked, sharedStyle, highlightedLegendItem), - ...this.renderNonStackedAreas(areas.nonStacked, sharedStyle, highlightedLegendItem), - ]; - }; - renderStackedAreas = ( - areas: AreaGeometry[], - sharedStyle: SharedGeometryStyle, - highlightedLegendItem: LegendItem | null, - ): JSX.Element[] => { - const elements: JSX.Element[] = []; - areas.forEach((glyph) => { - const { seriesAreaStyle } = glyph; - if (seriesAreaStyle.visible) { - elements.push(this.renderArea(glyph, sharedStyle, highlightedLegendItem)); - } - }); - areas.forEach((glyph, i) => { - const { seriesAreaLineStyle } = glyph; - if (seriesAreaLineStyle.visible) { - elements.push(...this.renderAreaLines(glyph, i, sharedStyle, highlightedLegendItem)); - } - }); - areas.forEach((glyph, i) => { - const { seriesPointStyle, geometryId } = glyph; - if (seriesPointStyle.visible) { - const customOpacity = seriesPointStyle ? seriesPointStyle.opacity : undefined; - const geometryStyle = getGeometryStyle( - geometryId, - this.props.highlightedLegendItem, - sharedStyle, - customOpacity, - ); - const pointStyleProps = buildPointStyleProps(glyph.color, seriesPointStyle, geometryStyle); - elements.push(...this.renderPoints(glyph.points, i, pointStyleProps, glyph.geometryId)); - } - }); - return elements; - }; - renderNonStackedAreas = ( - areas: AreaGeometry[], - sharedStyle: SharedGeometryStyle, - highlightedLegendItem: LegendItem | null, - ): JSX.Element[] => { + const { sharedStyle, highlightedLegendItem, areas } = this.props; return areas.reduce((acc, glyph, i) => { const { seriesAreaLineStyle, seriesAreaStyle, seriesPointStyle, geometryId } = glyph; if (seriesAreaStyle.visible) {