diff --git a/api/charts.api.md b/api/charts.api.md index 70ed2d1cce..82deef6b39 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -1680,7 +1680,9 @@ export type SeriesNameFn = (series: XYChartSeriesIdentifier, isTooltip: boolean) // @public (undocumented) export interface SeriesScales { timeZone?: string; + xNice?: boolean; xScaleType: XScaleType; + yNice?: boolean; // @deprecated yScaleToDataExtent?: boolean; yScaleType: ScaleContinuousType; diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png index f161bace35..570325f0d9 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-axes-custom-domain-visually-looks-correct-1-snap.png differ diff --git a/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts b/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts index 72e08d2985..8fbb8d6ef8 100644 --- a/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts +++ b/src/chart_types/heatmap/layout/viewmodel/viewmodel.ts @@ -110,15 +110,16 @@ export function shapeViewModel( let xValues = xDomain.domain as any[]; const timeScale = - xDomain.scaleType === ScaleType.Time + xDomain.type === ScaleType.Time ? new ScaleContinuous( { type: ScaleType.Time, domain: xDomain.domain, range: [0, chartDimensions.width], + nice: false, }, { - ticks: getTicks(chartDimensions.width, config.xAxisLabel), + desiredTickCount: getTicks(chartDimensions.width, config.xAxisLabel), timeZone: config.timeZone, }, ) @@ -315,7 +316,7 @@ export function shapeViewModel( * @param y */ const pickHighlightedArea: PickHighlightedArea = (x: Array, y: Array) => { - if (xDomain.scaleType !== ScaleType.Time) { + if (xDomain.type !== ScaleType.Time) { return null; } const [startValue, endValue] = x; diff --git a/src/chart_types/heatmap/specs/heatmap.ts b/src/chart_types/heatmap/specs/heatmap.ts index 0cdd896fe1..d399c46c46 100644 --- a/src/chart_types/heatmap/specs/heatmap.ts +++ b/src/chart_types/heatmap/specs/heatmap.ts @@ -29,6 +29,7 @@ import { Accessor, AccessorFn } from '../../../utils/accessor'; import { Color, Datum, RecursivePartial } from '../../../utils/common'; import { config } from '../layout/config/config'; import { Config } from '../layout/types/config_types'; +import { X_SCALE_DEFAULT } from './scale_defaults'; const defaultProps = { chartType: ChartType.Heatmap, @@ -38,7 +39,7 @@ const defaultProps = { colorScale: ScaleType.Linear, xAccessor: ({ x }: { x: string | number }) => x, yAccessor: ({ y }: { y: string | number }) => y, - xScaleType: ScaleType.Ordinal, + xScaleType: X_SCALE_DEFAULT.type, valueAccessor: ({ value }: { value: string | number }) => value, valueFormatter: (value: number) => `${value}`, xSortPredicate: Predicate.AlphaAsc, diff --git a/src/chart_types/heatmap/specs/scale_defaults.ts b/src/chart_types/heatmap/specs/scale_defaults.ts new file mode 100644 index 0000000000..4299532599 --- /dev/null +++ b/src/chart_types/heatmap/specs/scale_defaults.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; + +/** @internal */ +export const X_SCALE_DEFAULT = { + type: ScaleType.Ordinal, + nice: false, + desiredTickCount: 10, +}; diff --git a/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts b/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts index b569ec7b25..63e3c2c829 100644 --- a/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts +++ b/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts @@ -25,6 +25,8 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getAccessorValue } from '../../../../utils/accessor'; import { mergeXDomain } from '../../../xy_chart/domains/x_domain'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../../../xy_chart/scales/get_api_scales'; +import { X_SCALE_DEFAULT } from '../../specs/scale_defaults'; import { HeatmapTable } from './compute_chart_dimensions'; import { getHeatmapSpecSelector } from './get_heatmap_spec'; @@ -73,7 +75,16 @@ export const getHeatmapTableSelector = createCachedSelector( }, ); - resultData.xDomain = mergeXDomain([{ xScaleType: spec.xScaleType }], resultData.xValues, xDomain); + resultData.xDomain = mergeXDomain( + { + type: getXScaleTypeFromSpec(spec.xScaleType), + nice: getXNiceFromSpec(), + isBandScale: false, + desiredTickCount: X_SCALE_DEFAULT.desiredTickCount, + customDomain: xDomain, + }, + resultData.xValues, + ); // sort values by their predicates if (spec.xScaleType === ScaleType.Ordinal) { diff --git a/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts b/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts index 8b6e900943..7a2bccc20c 100644 --- a/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts +++ b/src/chart_types/heatmap/state/selectors/get_x_axis_right_overflow.ts @@ -33,7 +33,7 @@ import { getHeatmapTableSelector } from './get_heatmap_table'; export const getXAxisRightOverflow = createCachedSelector( [getHeatmapConfigSelector, getHeatmapTableSelector], ({ xAxisLabel: { fontSize, fontFamily, padding, formatter, width }, timeZone }, { xDomain }): number => { - if (xDomain.scaleType !== ScaleType.Time) { + if (xDomain.type !== ScaleType.Time) { return 0; } if (typeof width === 'number') { diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts index ee62c8f72b..311ef21748 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts @@ -18,9 +18,12 @@ */ import { ChartType } from '../..'; +import { MockGlobalSpec } from '../../../mocks/specs/specs'; +import { MockXDomain } from '../../../mocks/xy/domains'; import { ScaleType } from '../../../scales/constants'; import { SpecType } from '../../../specs/constants'; import { Dimensions } from '../../../utils/dimensions'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; import { computeSeriesDomains } from '../state/utils/utils'; import { computeXScale } from '../utils/scales'; import { BasicSeriesSpec, SeriesType } from '../utils/specs'; @@ -97,22 +100,35 @@ describe('Crosshair utils linear scale', () => { yScaleType: ScaleType.Linear, }; - const domainGroup = new Map([['group1', { fit: true }]]); - const barSeries = [barSeries1]; - const barSeriesDomains = computeSeriesDomains(barSeries, domainGroup); + const barSeriesDomains = computeSeriesDomains( + barSeries, + getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), + ); const multiBarSeries = [barSeries1, barSeries2]; - const multiBarSeriesDomains = computeSeriesDomains(multiBarSeries, domainGroup); + const multiBarSeriesDomains = computeSeriesDomains( + multiBarSeries, + getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), + ); const lineSeries = [lineSeries1]; - const lineSeriesDomains = computeSeriesDomains(lineSeries, domainGroup); + const lineSeriesDomains = computeSeriesDomains( + lineSeries, + getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), + ); const multiLineSeries = [lineSeries1, lineSeries2]; - const multiLineSeriesDomains = computeSeriesDomains(multiLineSeries, domainGroup); + const multiLineSeriesDomains = computeSeriesDomains( + multiLineSeries, + getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), + ); const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; - const mixedLinesBarsSeriesDomains = computeSeriesDomains(mixedLinesBars, domainGroup); + const mixedLinesBarsSeriesDomains = computeSeriesDomains( + mixedLinesBars, + getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), + ); const barSeriesScale = computeXScale({ xDomain: barSeriesDomains.xDomain, @@ -1456,13 +1472,11 @@ describe('Crosshair utils linear scale', () => { const chartDimensions: Dimensions = { top: 0, left: 0, width: 120, height: 120 }; test('cursor at begin of domain', () => { const barSeriesScaleLimited = computeXScale({ - xDomain: { + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0.5, 3.5], isBandScale: true, minInterval: 1, - scaleType: ScaleType.Linear, - type: 'xDomain', - }, + }), totalBarsInCluster: 1, range: [0, 120], }); @@ -1487,13 +1501,11 @@ describe('Crosshair utils linear scale', () => { }); test('cursor at end of domain', () => { const barSeriesScaleLimited = computeXScale({ - xDomain: { + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { domain: [-0.5, 2.5], isBandScale: true, minInterval: 1, - scaleType: ScaleType.Linear, - type: 'xDomain', - }, + }), totalBarsInCluster: barSeries.length, range: [0, 120], }); @@ -1518,13 +1530,11 @@ describe('Crosshair utils linear scale', () => { }); test('cursor at top begin of domain', () => { const barSeriesScaleLimited = computeXScale({ - xDomain: { + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0.5, 3.5], isBandScale: true, minInterval: 1, - scaleType: ScaleType.Linear, - type: 'xDomain', - }, + }), totalBarsInCluster: 1, range: [0, 120], }); @@ -1549,13 +1559,11 @@ describe('Crosshair utils linear scale', () => { }); test('cursor at top end of domain', () => { const barSeriesScaleLimited = computeXScale({ - xDomain: { + xDomain: MockXDomain.fromScaleType(ScaleType.Linear, { domain: [-0.5, 2.5], isBandScale: true, minInterval: 1, - scaleType: ScaleType.Linear, - type: 'xDomain', - }, + }), totalBarsInCluster: barSeries.length, range: [0, 120], }); diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts index 8e4ddd6412..7cd861b1d9 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts @@ -18,8 +18,10 @@ */ import { ChartType } from '../..'; +import { MockGlobalSpec } from '../../../mocks/specs/specs'; import { ScaleType } from '../../../scales/constants'; import { SpecType } from '../../../specs/constants'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; import { computeSeriesDomains } from '../state/utils/utils'; import { computeXScale } from '../utils/scales'; import { BasicSeriesSpec, SeriesType } from '../utils/specs'; @@ -96,22 +98,36 @@ describe('Crosshair utils ordinal scales', () => { yScaleType: ScaleType.Linear, }; - const domainGroup = new Map([['group1', { fit: true }]]); - const barSeries = [barSeries1]; - const barSeriesDomains = computeSeriesDomains(barSeries, domainGroup); + + const barSeriesDomains = computeSeriesDomains( + barSeries, + getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), + ); const multiBarSeries = [barSeries1, barSeries2]; - const multiBarSeriesDomains = computeSeriesDomains(multiBarSeries, domainGroup); + const multiBarSeriesDomains = computeSeriesDomains( + multiBarSeries, + getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), + ); const lineSeries = [lineSeries1]; - const lineSeriesDomains = computeSeriesDomains(lineSeries, domainGroup); + const lineSeriesDomains = computeSeriesDomains( + lineSeries, + getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), + ); const multiLineSeries = [lineSeries1, lineSeries2]; - const multiLineSeriesDomains = computeSeriesDomains(multiLineSeries, domainGroup); + const multiLineSeriesDomains = computeSeriesDomains( + multiLineSeries, + getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), + ); const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; - const mixedLinesBarsSeriesDomains = computeSeriesDomains(mixedLinesBars, domainGroup); + const mixedLinesBarsSeriesDomains = computeSeriesDomains( + mixedLinesBars, + getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), + ); const barSeriesScale = computeXScale({ xDomain: barSeriesDomains.xDomain, diff --git a/src/chart_types/xy_chart/domains/nice.ts b/src/chart_types/xy_chart/domains/nice.ts new file mode 100644 index 0000000000..665fb4d417 --- /dev/null +++ b/src/chart_types/xy_chart/domains/nice.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** @internal */ +export function areAllNiceDomain(nice: Array) { + return nice.length > 0 && nice.every((d) => d); +} diff --git a/src/chart_types/xy_chart/domains/types.ts b/src/chart_types/xy_chart/domains/types.ts index 1b00986de6..0819ab61ae 100644 --- a/src/chart_types/xy_chart/domains/types.ts +++ b/src/chart_types/xy_chart/domains/types.ts @@ -18,35 +18,31 @@ */ import { ScaleContinuousType } from '../../../scales'; -import { ScaleType } from '../../../scales/constants'; import { LogScaleOptions } from '../../../scales/scale_continuous'; import { OrdinalDomain, ContinuousDomain } from '../../../utils/domain'; import { GroupId } from '../../../utils/ids'; +import { XScaleType } from '../utils/specs'; /** @internal */ -export interface BaseDomain { - scaleType: typeof ScaleType.Ordinal | ScaleContinuousType; +export type XDomain = Pick & { + type: XScaleType; + nice: boolean; /* if the scale needs to be a band scale: used when displaying bars */ isBandScale: boolean; -} + /* the minimum interval of the scale if not-ordinal band-scale */ + minInterval: number; + /** if x domain is time, we should also specify the timezone */ + timeZone?: string; + domain: OrdinalDomain | ContinuousDomain; + desiredTickCount: number; +}; /** @internal */ -export type XDomain = BaseDomain & - Pick & { - type: 'xDomain'; - /* the minimum interval of the scale if not-ordinal band-scale */ - minInterval: number; - /** if x domain is time, we should also specify the timezone */ - timeZone?: string; - domain: OrdinalDomain | ContinuousDomain; - }; - -/** @internal */ -export type YDomain = BaseDomain & - LogScaleOptions & { - type: 'yDomain'; - isBandScale: false; - scaleType: ScaleContinuousType; - groupId: GroupId; - domain: ContinuousDomain; - }; +export type YDomain = LogScaleOptions & { + type: ScaleContinuousType; + nice: boolean; + isBandScale: false; + groupId: GroupId; + domain: ContinuousDomain; + desiredTickCount: number; +}; diff --git a/src/chart_types/xy_chart/domains/x_domain.test.ts b/src/chart_types/xy_chart/domains/x_domain.test.ts index a0f8b01c08..6ab4bd962a 100644 --- a/src/chart_types/xy_chart/domains/x_domain.test.ts +++ b/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -18,10 +18,12 @@ */ import { ChartType } from '../..'; -import { MockSeriesSpecs } from '../../../mocks/specs'; +import { MockGlobalSpec, MockSeriesSpec, MockSeriesSpecs } from '../../../mocks/specs'; import { ScaleType } from '../../../scales/constants'; import { SpecType, Direction, BinAgg } from '../../../specs/constants'; import { Logger } from '../../../utils/logger'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../scales/get_api_scales'; +import { getScaleConfigsFromSpecs } from '../state/selectors/get_api_scale_configs'; import { getDataSeriesFromSpecs } from '../utils/series'; import { BasicSeriesSpec, SeriesType } from '../utils/specs'; import { convertXScaleTypes, findMinInterval, mergeXDomain } from './x_domain'; @@ -33,16 +35,10 @@ jest.mock('../../../utils/logger', () => ({ })); describe('X Domain', () => { - test('Should return null when missing specs or specs types', () => { + test('Should return a default scale when missing specs or specs types', () => { const seriesSpecs: BasicSeriesSpec[] = []; const mainXScale = convertXScaleTypes(seriesSpecs); - expect(mainXScale).toBe(null); - }); - - test('should throw if we miss calling merge X domain without specs configured', () => { - expect(() => { - mergeXDomain([], new Set()); - }).toThrow(); + expect(mainXScale).not.toBeNull(); }); test('Should return correct scale type with single bar', () => { @@ -54,7 +50,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Linear, + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), isBandScale: true, }); }); @@ -68,7 +65,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Ordinal, + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), isBandScale: true, }); }); @@ -82,7 +80,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Linear, + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), isBandScale: false, }); }); @@ -96,7 +95,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Time, + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), isBandScale: false, timeZone: 'utc-3', }); @@ -116,7 +116,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Time, + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), isBandScale: false, timeZone: 'utc-3', }); @@ -136,7 +137,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Time, + type: getXScaleTypeFromSpec(ScaleType.Time), + nice: getXNiceFromSpec(), isBandScale: false, timeZone: 'utc', }); @@ -155,7 +157,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Ordinal, + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), isBandScale: false, }); }); @@ -172,7 +175,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Ordinal, + type: getXScaleTypeFromSpec(ScaleType.Ordinal), + nice: getXNiceFromSpec(), isBandScale: true, }); }); @@ -190,7 +194,8 @@ describe('X Domain', () => { ]; const mainXScale = convertXScaleTypes(seriesSpecs); expect(mainXScale).toEqual({ - scaleType: ScaleType.Linear, + type: getXScaleTypeFromSpec(ScaleType.Linear), + nice: getXNiceFromSpec(), isBandScale: true, }); }); @@ -229,16 +234,9 @@ describe('X Domain', () => { ], }; const specDataSeries: BasicSeriesSpec[] = [ds1, ds2]; + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - ); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge bar series correctly', () => { @@ -275,17 +273,9 @@ describe('X Domain', () => { ], }; const specDataSeries = [ds1, ds2]; - + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - ); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar series correctly', () => { @@ -322,21 +312,9 @@ describe('X Domain', () => { ], }; const specDataSeries = [ds1, ds2]; - + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - ); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar series correctly - 2', () => { @@ -375,19 +353,9 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - ); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar linear/bar ordinal series correctly', () => { @@ -424,21 +392,9 @@ describe('X Domain', () => { ], }; const specDataSeries = [ds1, ds2]; - + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Ordinal, - }, - ], - xValues, - ); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); @@ -483,28 +439,19 @@ describe('X Domain', () => { }; const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const getResult = () => - mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - customDomain, - ScaleType.Ordinal, - ); + const scalesConfig = getScaleConfigsFromSpecs( + [], + specDataSeries, + MockGlobalSpec.settings({ xDomain: customDomain }), + ); + + const getResult = () => mergeXDomain(scalesConfig.x, xValues, ScaleType.Ordinal); expect(getResult).not.toThrow(); const mergedDomain = getResult(); expect(mergedDomain.domain).toEqual([0, 'a', 2, 5, 7]); - expect(mergedDomain.scaleType).toEqual(ScaleType.Ordinal); + expect(mergedDomain.type).toEqual(ScaleType.Ordinal); }); test('Should merge multi bar/line ordinal series correctly', () => { @@ -543,19 +490,9 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Ordinal, - }, - ], - xValues, - ); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); test('Should merge multi bar/line time series correctly', () => { @@ -594,19 +531,9 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Bar, - xScaleType: ScaleType.Ordinal, - }, - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Time, - }, - ], - xValues, - ); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); test('Should merge multi lines series correctly', () => { @@ -645,19 +572,9 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Ordinal, - }, - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Linear, - }, - ], - xValues, - ); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); + + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); @@ -690,20 +607,9 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const { xValues } = getDataSeriesFromSpecs(specDataSeries); + const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain( - [ - { - seriesType: SeriesType.Area, - xScaleType: ScaleType.Linear, - }, - { - seriesType: SeriesType.Line, - xScaleType: ScaleType.Ordinal, - }, - ], - xValues, - ); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues); expect(mergedDomain.domain.length).toEqual(maxValues); }); test('should compute minInterval an ordered list of numbers', () => { @@ -714,7 +620,7 @@ describe('X Domain', () => { const minInterval = findMinInterval([2, 10, 3, 1, 5]); expect(minInterval).toBe(1); }); - test('should compute minInterval an list grether than 9', () => { + test('should compute minInterval an list greater than 9', () => { const minInterval = findMinInterval([0, 2, 4, 6, 8, 10, 20, 30, 40, 50, 80]); expect(minInterval).toBe(2); }); @@ -737,15 +643,19 @@ describe('X Domain', () => { test('should account for custom domain when merging a linear domain: complete bounded domain', () => { const xValues = new Set([1, 2, 3, 4, 5]); const xDomain = { min: 0, max: 3 }; - const specs: Pick[] = [ - { seriesType: SeriesType.Line, xScaleType: ScaleType.Linear }, - ]; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; - const basicMergedDomain = mergeXDomain(specs, xValues, xDomain); + const basicMergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); expect(basicMergedDomain.domain).toEqual([0, 3]); const arrayXDomain = [1, 2]; - let { domain } = mergeXDomain(specs, xValues, arrayXDomain); + let { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: arrayXDomain })).x, + xValues, + ); expect(domain).toEqual([1, 5]); const warnMessage = 'xDomain for continuous scale should be a DomainRange object, not an array'; expect(Logger.warn).toBeCalledWith(warnMessage); @@ -753,7 +663,10 @@ describe('X Domain', () => { (Logger.warn as jest.Mock).mockClear(); const invalidXDomain = { min: 10, max: 0 }; - domain = mergeXDomain(specs, xValues, invalidXDomain).domain; + domain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ).domain; expect(domain).toEqual([1, 5]); expect(Logger.warn).toBeCalledWith('custom xDomain is invalid, min is greater than max. Custom domain is ignored.'); }); @@ -761,15 +674,19 @@ describe('X Domain', () => { test('should account for custom domain when merging a linear domain: lower bounded domain', () => { const xValues = new Set([1, 2, 3, 4, 5]); const xDomain = { min: 0 }; - const specs: Pick[] = [ - { seriesType: SeriesType.Line, xScaleType: ScaleType.Linear }, - ]; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; - const mergedDomain = mergeXDomain(specs, xValues, xDomain); + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); expect(mergedDomain.domain).toEqual([0, 5]); const invalidXDomain = { min: 10 }; - const { domain } = mergeXDomain(specs, xValues, invalidXDomain); + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); expect(domain).toEqual([1, 5]); expect(Logger.warn).toBeCalledWith( 'custom xDomain is invalid, custom min is greater than computed max. Custom domain is ignored.', @@ -779,15 +696,19 @@ describe('X Domain', () => { test('should account for custom domain when merging a linear domain: upper bounded domain', () => { const xValues = new Set([1, 2, 3, 4, 5]); const xDomain = { max: 3 }; - const specs: Pick[] = [ - { seriesType: SeriesType.Line, xScaleType: ScaleType.Linear }, - ]; + const specs = [MockSeriesSpec.line({ xScaleType: ScaleType.Linear })]; - const mergedDomain = mergeXDomain(specs, xValues, xDomain); + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); expect(mergedDomain.domain).toEqual([1, 3]); const invalidXDomain = { max: -1 }; - const { domain } = mergeXDomain(specs, xValues, invalidXDomain); + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); expect(domain).toEqual([1, 5]); expect(Logger.warn).toBeCalledWith( 'custom xDomain is invalid, computed min is greater than custom max. Custom domain is ignored.', @@ -797,14 +718,18 @@ describe('X Domain', () => { test('should account for custom domain when merging an ordinal domain', () => { const xValues = new Set(['a', 'b', 'c', 'd']); const xDomain = ['a', 'b', 'c']; - const specs: Pick[] = [ - { seriesType: SeriesType.Bar, xScaleType: ScaleType.Ordinal }, - ]; - const basicMergedDomain = mergeXDomain(specs, xValues, xDomain); + const specs = [MockSeriesSpec.bar({ xScaleType: ScaleType.Ordinal })]; + const basicMergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); expect(basicMergedDomain.domain).toEqual(['a', 'b', 'c']); const objectXDomain = { max: 10, min: 0 }; - const { domain } = mergeXDomain(specs, xValues, objectXDomain); + const { domain } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: objectXDomain })).x, + xValues, + ); expect(domain).toEqual(['a', 'b', 'c', 'd']); const warnMessage = 'xDomain for ordinal scale should be an array of values, not a DomainRange object. xDomain is ignored.'; @@ -813,25 +738,32 @@ describe('X Domain', () => { describe('should account for custom minInterval', () => { const xValues = new Set([1, 2, 3, 4, 5]); - const specs: Pick[] = [ - { seriesType: SeriesType.Bar, xScaleType: ScaleType.Linear }, - ]; + const specs = [MockSeriesSpec.bar({ xScaleType: ScaleType.Linear })]; test('with valid minInterval', () => { const xDomain = { minInterval: 0.5 }; - const mergedDomain = mergeXDomain(specs, xValues, xDomain); + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + xValues, + ); expect(mergedDomain.minInterval).toEqual(0.5); }); test('with valid minInterval greater than computed minInterval for single datum set', () => { const xDomain = { minInterval: 10 }; - const mergedDomain = mergeXDomain(specs, new Set([5]), xDomain); + const mergedDomain = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, + new Set([5]), + ); expect(mergedDomain.minInterval).toEqual(10); }); test('with invalid minInterval greater than computed minInterval for multi data set', () => { const invalidXDomain = { minInterval: 10 }; - const { minInterval } = mergeXDomain(specs, xValues, invalidXDomain); + const { minInterval } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); expect(minInterval).toEqual(1); const expectedWarning = 'custom xDomain is invalid, custom minInterval is greater than computed minInterval. Using computed minInterval.'; @@ -840,7 +772,10 @@ describe('X Domain', () => { test('with invalid minInterval less than 0', () => { const invalidXDomain = { minInterval: -1 }; - const { minInterval } = mergeXDomain(specs, xValues, invalidXDomain); + const { minInterval } = mergeXDomain( + getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, + xValues, + ); expect(minInterval).toEqual(1); const expectedWarning = 'custom xDomain is invalid, custom minInterval is less than 0. Using computed minInterval.'; diff --git a/src/chart_types/xy_chart/domains/x_domain.ts b/src/chart_types/xy_chart/domains/x_domain.ts index 3a5b9a010f..97521a733b 100644 --- a/src/chart_types/xy_chart/domains/x_domain.ts +++ b/src/chart_types/xy_chart/domains/x_domain.ts @@ -23,50 +23,40 @@ import { ScaleType } from '../../../scales/constants'; import { compareByValueAsc, identity } from '../../../utils/common'; import { computeContinuousDataDomain, computeOrdinalDataDomain } from '../../../utils/domain'; import { Logger } from '../../../utils/logger'; +import { getXNiceFromSpec, getXScaleTypeFromSpec } from '../scales/get_api_scales'; +import { ScaleConfigs } from '../state/selectors/get_api_scale_configs'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; -import { BasicSeriesSpec, CustomXDomain, SeriesType, XScaleType } from '../utils/specs'; +import { BasicSeriesSpec, SeriesType, XScaleType } from '../utils/specs'; +import { areAllNiceDomain } from './nice'; import { XDomain } from './types'; /** * Merge X domain value between a set of chart specification. - * @param specs an array of [{ seriesType, xScaleType }] - * @param xValues a set of unique x values from all specs - * @param customXDomain if specified, a custom xDomain - * @param fallbackScale - * @returns a merged XDomain between all series. * @internal */ export function mergeXDomain( - specs: Optional, 'seriesType'>[], + { type, nice, isBandScale, timeZone, desiredTickCount, customDomain }: ScaleConfigs['x'], xValues: Set, - customXDomain?: CustomXDomain, fallbackScale?: XScaleType, ): XDomain { - const mainXScaleType = convertXScaleTypes(specs); - if (!mainXScaleType) { - throw new Error(`Cannot merge the domain. Missing X scale types ${JSON.stringify(specs)}`); - } - const values = [...xValues.values()]; let seriesXComputedDomains; let minInterval = 0; - if (mainXScaleType.scaleType === ScaleType.Ordinal || fallbackScale === ScaleType.Ordinal) { - if (mainXScaleType.scaleType !== ScaleType.Ordinal) { - Logger.warn( - `Each X value in a ${mainXScaleType.scaleType} x scale needs be be a number. Using ordinal x scale as fallback.`, - ); + if (type === ScaleType.Ordinal || fallbackScale === ScaleType.Ordinal) { + if (type !== ScaleType.Ordinal) { + Logger.warn(`Each X value in a ${type} x scale needs be be a number. Using ordinal x scale as fallback.`); } seriesXComputedDomains = computeOrdinalDataDomain(values, identity, false, true); - if (customXDomain) { - if (Array.isArray(customXDomain)) { - seriesXComputedDomains = customXDomain; + if (customDomain) { + if (Array.isArray(customDomain)) { + seriesXComputedDomains = customDomain; } else { if (fallbackScale === ScaleType.Ordinal) { Logger.warn(`xDomain ignored for fallback ordinal scale. Options to resolve: -1) Correct data to match ${mainXScaleType.scaleType} scale type (see previous warning) +1) Correct data to match ${type} scale type (see previous warning) 2) Change xScaleType to ordinal and set xDomain to Domain array`); } else { Logger.warn( @@ -76,39 +66,39 @@ export function mergeXDomain( } } } else { - seriesXComputedDomains = computeContinuousDataDomain(values, identity, mainXScaleType.scaleType, { + seriesXComputedDomains = computeContinuousDataDomain(values, identity, type, { fit: true, }); let customMinInterval: undefined | number; - if (customXDomain) { - if (Array.isArray(customXDomain)) { + if (customDomain) { + if (Array.isArray(customDomain)) { Logger.warn('xDomain for continuous scale should be a DomainRange object, not an array'); } else { - customMinInterval = customXDomain.minInterval; + customMinInterval = customDomain.minInterval; const [computedDomainMin, computedDomainMax] = seriesXComputedDomains; - if (isCompleteBound(customXDomain)) { - if (customXDomain.min > customXDomain.max) { + if (isCompleteBound(customDomain)) { + if (customDomain.min > customDomain.max) { Logger.warn('custom xDomain is invalid, min is greater than max. Custom domain is ignored.'); } else { - seriesXComputedDomains = [customXDomain.min, customXDomain.max]; + seriesXComputedDomains = [customDomain.min, customDomain.max]; } - } else if (isLowerBound(customXDomain)) { - if (customXDomain.min > computedDomainMax) { + } else if (isLowerBound(customDomain)) { + if (customDomain.min > computedDomainMax) { Logger.warn( 'custom xDomain is invalid, custom min is greater than computed max. Custom domain is ignored.', ); } else { - seriesXComputedDomains = [customXDomain.min, computedDomainMax]; + seriesXComputedDomains = [customDomain.min, computedDomainMax]; } - } else if (isUpperBound(customXDomain)) { - if (computedDomainMin > customXDomain.max) { + } else if (isUpperBound(customDomain)) { + if (computedDomainMin > customDomain.max) { Logger.warn( 'custom xDomain is invalid, computed min is greater than custom max. Custom domain is ignored.', ); } else { - seriesXComputedDomains = [computedDomainMin, customXDomain.max]; + seriesXComputedDomains = [computedDomainMin, customDomain.max]; } } } @@ -118,13 +108,14 @@ export function mergeXDomain( } return { - type: 'xDomain', - scaleType: fallbackScale ?? mainXScaleType.scaleType, - isBandScale: mainXScaleType.isBandScale, + type: fallbackScale ?? type, + nice, + isBandScale, domain: seriesXComputedDomains, minInterval, - timeZone: mainXScaleType.timeZone, - logBase: customXDomain && 'logBase' in customXDomain ? customXDomain.logBase : undefined, + timeZone, + logBase: customDomain && 'logBase' in customDomain ? customDomain.logBase : undefined, + desiredTickCount, }; } @@ -174,45 +165,58 @@ export function findMinInterval(xValues: number[]): number { /** * Convert the scale types of a set of specification to a generic one. - * If there are at least one `ordinal` scale type, the resulting scale is coerched to ordinal. - * If there are only `continuous` scale types, the resulting scale is coerched to linear. - * If there are only `time` scales, we coerch the timeZone to `utc` only if we have multiple + * If there are at least one `ordinal` scale type, the resulting scale is coerced to ordinal. + * If there are only `continuous` scale types, the resulting scale is coerced to linear. + * If there are only `time` scales, we coerce the timeZone to `utc` only if we have multiple * different timezones. - * @returns the coerched scale type, the timezone and a parameter that describe if its a bandScale or not + * @returns the coerced scale type, the timezone and a parameter that describe if its a bandScale or not * @internal */ export function convertXScaleTypes( - specs: Optional, 'seriesType'>[], + specs: Optional, 'seriesType'>[], ): { - scaleType: XScaleType; + type: XScaleType; + nice: boolean; isBandScale: boolean; timeZone?: string; -} | null { +} { const seriesTypes = new Set(); const scaleTypes = new Set(); const timeZones = new Set(); + const niceDomainConfigs: Array = []; specs.forEach((spec) => { + niceDomainConfigs.push(getXNiceFromSpec(spec.xNice)); seriesTypes.add(spec.seriesType); - scaleTypes.add(spec.xScaleType); + scaleTypes.add(getXScaleTypeFromSpec(spec.xScaleType)); if (spec.timeZone) { timeZones.add(spec.timeZone.toLowerCase()); } }); if (specs.length === 0 || seriesTypes.size === 0 || scaleTypes.size === 0) { - return null; + return { + type: ScaleType.Linear, + nice: true, + isBandScale: false, + }; } + const nice = areAllNiceDomain(niceDomainConfigs); const isBandScale = seriesTypes.has(SeriesType.Bar); if (scaleTypes.size === 1) { const scaleType = scaleTypes.values().next().value; - let timeZone: string | undefined; - if (scaleType === ScaleType.Time) { - timeZone = timeZones.size > 1 ? 'utc' : timeZones.values().next().value; - } - return { scaleType, isBandScale, timeZone }; + const timeZone = timeZones.size > 1 ? 'utc' : timeZones.values().next().value; + return { type: scaleType, nice, isBandScale, timeZone }; } if (scaleTypes.size > 1 && scaleTypes.has(ScaleType.Ordinal)) { - return { scaleType: ScaleType.Ordinal, isBandScale }; + return { + type: ScaleType.Ordinal, + nice, + isBandScale, + }; } - return { scaleType: ScaleType.Linear, isBandScale }; + return { + type: ScaleType.Linear, + nice, + isBandScale, + }; } diff --git a/src/chart_types/xy_chart/domains/y_domain.test.ts b/src/chart_types/xy_chart/domains/y_domain.test.ts index 81c90ecf0e..fdb3239f25 100644 --- a/src/chart_types/xy_chart/domains/y_domain.test.ts +++ b/src/chart_types/xy_chart/domains/y_domain.test.ts @@ -20,6 +20,7 @@ import { ChartType } from '../..'; import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; +import { MockYDomain } from '../../../mocks/xy/domains'; import { ScaleType } from '../../../scales/constants'; import { SpecType } from '../../../specs/constants'; import { Position } from '../../../utils/common'; @@ -84,13 +85,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: DEFAULT_GLOBAL_ID, domain: [2, 12], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); test('Should merge Y domain for zero baseline charts', () => { @@ -112,13 +111,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: DEFAULT_GLOBAL_ID, domain: [0, 12], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); test('Should merge Y domain different group', () => { @@ -148,20 +145,16 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [2, 12], - scaleType: ScaleType.Linear, isBandScale: false, - type: 'yDomain', - }, - { + }), + MockYDomain.fromScaleType(ScaleType.Log, { groupId: 'b', domain: [2, 10], - scaleType: ScaleType.Log, isBandScale: false, - type: 'yDomain', - }, + }), ]); }); test('Should merge Y domain same group all stacked', () => { @@ -186,13 +179,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 17], - scaleType: ScaleType.Linear, isBandScale: false, - type: 'yDomain', - }, + }), ]); }); test('Should merge Y domain same group partially stacked', () => { @@ -215,13 +206,11 @@ describe('Y Domain', () => { ); const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 12], - scaleType: ScaleType.Linear, isBandScale: false, - type: 'yDomain', - }, + }), ]); }); @@ -384,8 +373,10 @@ describe('Y Domain', () => { }); test('Should return a default Scale Linear for YScaleType when there are no specs', () => { - const specs: Pick[] = []; - expect(coerceYScaleTypes(specs)).toBe(ScaleType.Linear); + expect(coerceYScaleTypes([])).toEqual({ + nice: false, + type: ScaleType.Linear, + }); }); test('Should merge Y domain accounting for custom domain limits: complete bounded domain', () => { @@ -405,13 +396,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 20], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); test('Should merge Y domain accounting for custom domain limits: partial lower bounded domain', () => { @@ -431,13 +420,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 12], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); test('Should not merge Y domain with invalid custom domain limits: partial lower bounded domain', () => { @@ -480,13 +467,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [2, 20], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); test('Should not merge Y domain with invalid custom domain limits: partial upper bounded domain', () => { @@ -530,13 +515,11 @@ describe('Y Domain', () => { const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 1], - scaleType: ScaleType.Linear, isBandScale: false, - type: 'yDomain', - }, + }), ]); }); test('Should merge Y domain with as percentage regadless of custom domains', () => { @@ -558,13 +541,11 @@ describe('Y Domain', () => { ); const { yDomains } = computeSeriesDomainsSelector(store.getState()); expect(yDomains).toEqual([ - { - type: 'yDomain', + MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'a', domain: [0, 1], - scaleType: ScaleType.Linear, isBandScale: false, - }, + }), ]); }); }); diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index ea310c94b6..8053adf3ed 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -23,11 +23,13 @@ import { identity } from '../../../utils/common'; import { computeContinuousDataDomain, ContinuousDomain } from '../../../utils/domain'; import { GroupId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; +import { ScaleConfigs } from '../state/selectors/get_api_scale_configs'; import { getSpecDomainGroupId } from '../state/utils/spec'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; import { groupBy } from '../utils/group_data_series'; import { DataSeries } from '../utils/series'; import { BasicSeriesSpec, YDomainRange, SeriesType, StackMode } from '../utils/specs'; +import { areAllNiceDomain } from './nice'; import { YDomain } from './types'; /** @internal */ @@ -37,20 +39,16 @@ export type YBasicSeriesSpec = Pick< > & { stackMode?: StackMode; enableHistogramMode?: boolean }; /** @internal */ -export function mergeYDomain(dataSeries: DataSeries[], domainsByGroupId: Map): YDomain[] { +export function mergeYDomain(dataSeries: DataSeries[], yScaleAPIConfig: ScaleConfigs['y']): YDomain[] { const dataSeriesByGroupId = groupBy(dataSeries, ({ spec }) => getSpecDomainGroupId(spec), true); return dataSeriesByGroupId.reduce((acc, groupedDataSeries) => { - const [{ spec }] = groupedDataSeries; - const groupId = getSpecDomainGroupId(spec); - const stacked = groupedDataSeries.filter(({ isStacked, isFiltered }) => isStacked && !isFiltered); const nonStacked = groupedDataSeries.filter(({ isStacked, isFiltered }) => !isStacked && !isFiltered); - const customDomain = domainsByGroupId.get(groupId); const hasNonZeroBaselineTypes = groupedDataSeries.some( ({ seriesType, isFiltered }) => seriesType === SeriesType.Bar || (seriesType === SeriesType.Area && !isFiltered), ); - const domain = mergeYDomainForGroup(stacked, nonStacked, hasNonZeroBaselineTypes, customDomain); + const domain = mergeYDomainForGroup(stacked, nonStacked, hasNonZeroBaselineTypes, yScaleAPIConfig); if (!domain) { return acc; } @@ -62,22 +60,20 @@ function mergeYDomainForGroup( stacked: DataSeries[], nonStacked: DataSeries[], hasZeroBaselineSpecs: boolean, - customDomain?: YDomainRange, + yScaleConfig: ScaleConfigs['y'], ): YDomain | null { const dataSeries = [...stacked, ...nonStacked]; if (dataSeries.length === 0) { return null; } - const yScaleTypes = dataSeries.map(({ spec: { yScaleType } }) => ({ - yScaleType, - })); - const groupYScaleType = coerceYScaleTypes(yScaleTypes); + const [{ stackMode, spec }] = dataSeries; const groupId = getSpecDomainGroupId(spec); + const { customDomain, type, nice, desiredTickCount } = yScaleConfig[groupId]; let domain: ContinuousDomain; if (stackMode === StackMode.Percentage) { - domain = computeContinuousDataDomain([0, 1], identity, groupYScaleType, customDomain); + domain = computeContinuousDataDomain([0, 1], identity, type, customDomain); } else { // TODO remove when removing yScaleToDataExtent const newCustomDomain = customDomain ? { ...customDomain } : {}; @@ -87,18 +83,13 @@ function mergeYDomainForGroup( } // compute stacked domain - const stackedDomain = computeYDomain(stacked, hasZeroBaselineSpecs, groupYScaleType, newCustomDomain); + const stackedDomain = computeYDomain(stacked, hasZeroBaselineSpecs, type, newCustomDomain); // compute non stacked domain - const nonStackedDomain = computeYDomain(nonStacked, hasZeroBaselineSpecs, groupYScaleType, newCustomDomain); + const nonStackedDomain = computeYDomain(nonStacked, hasZeroBaselineSpecs, type, newCustomDomain); // merge stacked and non stacked domain together - domain = computeContinuousDataDomain( - [...stackedDomain, ...nonStackedDomain], - identity, - groupYScaleType, - newCustomDomain, - ); + domain = computeContinuousDataDomain([...stackedDomain, ...nonStackedDomain], identity, type, newCustomDomain); const [computedDomainMin, computedDomainMax] = domain; @@ -122,13 +113,14 @@ function mergeYDomainForGroup( } } return { - type: 'yDomain', + type, + nice, isBandScale: false, - scaleType: groupYScaleType, groupId, domain, logBase: customDomain?.logBase, logMinLimit: customDomain?.logMinLimit, + desiredTickCount, }; } @@ -228,19 +220,28 @@ export function isStackedSpec(spec: YBasicSeriesSpec, histogramEnabled: boolean) * @returns {ScaleContinuousType} * @internal */ -export function coerceYScaleTypes(scales: { yScaleType: ScaleContinuousType }[]): ScaleContinuousType { - const scaleTypes = new Set(); - scales.forEach(({ yScaleType }) => { - scaleTypes.add(yScaleType); - }); - return coerceYScale(scaleTypes); -} - -function coerceYScale(scaleTypes: Set): ScaleContinuousType { - if (scaleTypes.size === 1) { - const scales = scaleTypes.values(); - const { value } = scales.next(); - return value; - } - return ScaleType.Linear; +export function coerceYScaleTypes( + scales: Array<{ type: ScaleContinuousType; nice: boolean }>, +): { type: ScaleContinuousType; nice: boolean } { + const scaleCollection = scales.reduce<{ + types: Set; + nice: Array; + }>( + (acc, scale) => { + acc.types.add(scale.type); + acc.nice.push(scale.nice); + return acc; + }, + { + types: new Set(), + nice: [], + }, + ); + const nice = areAllNiceDomain(scaleCollection.nice); + return scaleCollection.types.size === 1 + ? { type: scaleCollection.types.values().next().value, nice } + : { + type: ScaleType.Linear, + nice, + }; } diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index 78180cd85e..a09da6d93c 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -24,6 +24,7 @@ import { SortSeriesByConfig, TickFormatterOptions } from '../../../specs'; import { Color } from '../../../utils/common'; import { BandedAccessorType } from '../../../utils/geometry'; import { getLegendCompareFn, SeriesCompareFn } from '../../../utils/series_sort'; +import { getXScaleTypeFromSpec } from '../scales/get_api_scales'; import { getAxesSpecForSpecId, getSpecsById } from '../state/utils/spec'; import { LastValues } from '../state/utils/types'; import { Y0_ACCESSOR_POSTFIX, Y1_ACCESSOR_POSTFIX } from '../tooltip/tooltip'; @@ -137,6 +138,7 @@ export function computeLegend( const lastValue = lastValues.get(key); const seriesIdentifier = getSeriesIdentifierFromDataSeries(series); + const xScaleType = getXScaleTypeFromSpec(spec.xScaleType); legendItems.push({ color, label: labelY1, @@ -145,7 +147,7 @@ export function computeLegend( isSeriesHidden, isItemHidden: hideInLegend, isToggleable: true, - defaultExtra: getLegendExtra(showLegendExtra, spec.xScaleType, formatter, 'y1', lastValue), + defaultExtra: getLegendExtra(showLegendExtra, xScaleType, formatter, 'y1', lastValue), path: [{ index: 0, value: seriesIdentifier.key }], keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], }); @@ -159,7 +161,7 @@ export function computeLegend( isSeriesHidden, isItemHidden: hideInLegend, isToggleable: true, - defaultExtra: getLegendExtra(showLegendExtra, spec.xScaleType, formatter, 'y0', lastValue), + defaultExtra: getLegendExtra(showLegendExtra, xScaleType, formatter, 'y0', lastValue), path: [{ index: 0, value: seriesIdentifier.key }], keys: [specId, spec.groupId, yAccessor, ...series.splitAccessors.values()], }); diff --git a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts index b0ffa7aa2e..5700b7c429 100644 --- a/src/chart_types/xy_chart/rendering/rendering.lines.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.lines.test.ts @@ -731,6 +731,7 @@ describe('Rendering points - line', () => { const axis = MockGlobalSpec.axis({ position: Position.Left, hide: true, domain: { max: 1 } }); const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); MockStore.addSpecs([pointSeriesSpec, axis, settings], store); + const { geometries: { lines }, geometriesIndex, diff --git a/src/chart_types/xy_chart/scales/get_api_scales.ts b/src/chart_types/xy_chart/scales/get_api_scales.ts new file mode 100644 index 0000000000..f43713c2c2 --- /dev/null +++ b/src/chart_types/xy_chart/scales/get_api_scales.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleContinuousType } from '../../../scales'; +import { BasicSeriesSpec, XScaleType } from '../utils/specs'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from './scale_defaults'; + +/** @internal */ +export function getXScaleTypeFromSpec(type?: BasicSeriesSpec['xScaleType']): XScaleType { + return type ?? X_SCALE_DEFAULT.type; +} + +/** @internal */ +export function getXNiceFromSpec(nice?: BasicSeriesSpec['xNice']): boolean { + return nice ?? X_SCALE_DEFAULT.nice; +} + +/** @internal */ +export function getYScaleTypeFromSpec(type?: BasicSeriesSpec['yScaleType']): ScaleContinuousType { + return type ?? Y_SCALE_DEFAULT.type; +} + +/** @internal */ +export function getYNiceFromSpec(nice?: BasicSeriesSpec['yNice']): boolean { + return nice ?? Y_SCALE_DEFAULT.nice; +} diff --git a/src/chart_types/xy_chart/scales/scale_defaults.ts b/src/chart_types/xy_chart/scales/scale_defaults.ts new file mode 100644 index 0000000000..7eb54d56c2 --- /dev/null +++ b/src/chart_types/xy_chart/scales/scale_defaults.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ScaleType } from '../../../scales/constants'; + +/** @internal */ +export const X_SCALE_DEFAULT = { + type: ScaleType.Ordinal, + nice: false, + desiredTickCount: 10, +}; + +/** @internal */ +export const Y_SCALE_DEFAULT = { + type: ScaleType.Linear, + nice: false, + desiredTickCount: 10, +}; diff --git a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts index 824d564fb5..f9ba585acf 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -24,8 +24,8 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { SeriesDomainsAndData } from '../utils/types'; import { computeSeriesDomains } from '../utils/utils'; +import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs'; import { getSeriesSpecsSelector, getSmallMultiplesIndexOrderSelector } from './get_specs'; -import { mergeYCustomDomainsByGroupIdSelector } from './merge_y_custom_domains'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; @@ -33,17 +33,16 @@ const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interacti export const computeSeriesDomainsSelector = createCachedSelector( [ getSeriesSpecsSelector, - mergeYCustomDomainsByGroupIdSelector, getDeselectedSeriesSelector, getSettingsSpecSelector, getSmallMultiplesIndexOrderSelector, + getScaleConfigsFromSpecsSelector, ], - (seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, settingsSpec, smallMultiples): SeriesDomainsAndData => { + (seriesSpecs, deselectedDataSeries, settingsSpec, smallMultiples, scaleConfigs): SeriesDomainsAndData => { return computeSeriesDomains( seriesSpecs, - customYDomainsByGroupId, + scaleConfigs, deselectedDataSeries, - settingsSpec.xDomain, settingsSpec.orderOrdinalBinsBy, smallMultiples, // @ts-ignore diff --git a/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts new file mode 100644 index 0000000000..c66aa1077d --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/get_api_scale_configs.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleContinuousType } from '../../../../scales'; +import { ScaleType } from '../../../../scales/constants'; +import { SettingsSpec } from '../../../../specs/settings'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { GroupId } from '../../../../utils/ids'; +import { convertXScaleTypes } from '../../domains/x_domain'; +import { coerceYScaleTypes } from '../../domains/y_domain'; +import { getYNiceFromSpec, getYScaleTypeFromSpec } from '../../scales/get_api_scales'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from '../../scales/scale_defaults'; +import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils'; +import { groupBy } from '../../utils/group_data_series'; +import { AxisSpec, BasicSeriesSpec, CustomXDomain, XScaleType, YDomainRange } from '../../utils/specs'; +import { isHorizontalRotation } from '../utils/common'; +import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; +import { mergeYCustomDomainsByGroupId } from './merge_y_custom_domains'; + +/** @internal */ +export type ScaleConfigBase = { + type: T; + nice: boolean; + desiredTickCount: number; + customDomain?: D; +}; +type XScaleConfigBase = ScaleConfigBase; +type YScaleConfigBase = ScaleConfigBase; + +/** @internal */ +export interface ScaleConfigs { + x: XScaleConfigBase & { + isBandScale: boolean; + timeZone?: string; + }; + y: Record; +} + +/** @internal */ +export const getScaleConfigsFromSpecsSelector = createCachedSelector( + [getAxisSpecsSelector, getSeriesSpecsSelector, getSettingsSpecSelector], + getScaleConfigsFromSpecs, +)(getChartIdSelector); + +/** @internal */ +export function getScaleConfigsFromSpecs( + axisSpecs: AxisSpec[], + seriesSpecs: BasicSeriesSpec[], + settingsSpec: SettingsSpec, +): ScaleConfigs { + const isHorizontalChart = isHorizontalRotation(settingsSpec.rotation); + + // x axis + const xAxes = axisSpecs.filter((d) => isHorizontalChart === isHorizontalAxis(d.position)); + const xTicks = xAxes.reduce((acc, { ticks = X_SCALE_DEFAULT.desiredTickCount }) => { + return Math.max(acc, ticks); + }, X_SCALE_DEFAULT.desiredTickCount); + + const xScaleConfig = convertXScaleTypes(seriesSpecs); + const x: ScaleConfigs['x'] = { + customDomain: settingsSpec.xDomain, + ...xScaleConfig, + desiredTickCount: xTicks, + }; + + // y axes + const scaleConfigsByGroupId = groupBy(seriesSpecs, ['groupId'], true).reduce< + Record + >((acc, series) => { + const yScaleTypes = series.map(({ yScaleType, yNice }) => ({ + nice: getYNiceFromSpec(yNice), + type: getYScaleTypeFromSpec(yScaleType), + })); + acc[series[0].groupId] = coerceYScaleTypes(yScaleTypes); + return acc; + }, {}); + + const customDomainByGroupId = mergeYCustomDomainsByGroupId(axisSpecs, settingsSpec.rotation); + + const yAxes = axisSpecs.filter((d) => isHorizontalChart === isVerticalAxis(d.position)); + const y = Object.keys(scaleConfigsByGroupId).reduce((acc, groupId) => { + const axis = yAxes.find((yAxis) => yAxis.groupId === groupId); + const desiredTickCount = axis?.ticks ?? Y_SCALE_DEFAULT.desiredTickCount; + const scaleConfig = scaleConfigsByGroupId[groupId]; + const customDomain = customDomainByGroupId.get(groupId); + if (!acc[groupId]) { + acc[groupId] = { + customDomain, + ...scaleConfig, + desiredTickCount, + }; + } + acc[groupId].desiredTickCount = Math.max(acc[groupId].desiredTickCount, desiredTickCount); + return acc; + }, {}); + return { x, y }; +} diff --git a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts index 5acc516aa4..865bc15b02 100644 --- a/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/merge_y_custom_domains.ts @@ -17,23 +17,11 @@ * under the License. */ -import createCachedSelector from 're-reselect'; - -import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { Rotation } from '../../../../utils/common'; import { GroupId } from '../../../../utils/ids'; import { isCompleteBound, isLowerBound, isUpperBound, isBounded } from '../../utils/axis_type_utils'; import { isYDomain } from '../../utils/axis_utils'; import { AxisSpec, YDomainRange } from '../../utils/specs'; -import { getAxisSpecsSelector } from './get_specs'; - -/** @internal */ -export const mergeYCustomDomainsByGroupIdSelector = createCachedSelector( - [getAxisSpecsSelector, getSettingsSpecSelector], - (axisSpecs, settingsSpec): Map => - mergeYCustomDomainsByGroupId(axisSpecs, settingsSpec ? settingsSpec.rotation : 0), -)(getChartIdSelector); /** @internal */ export function mergeYCustomDomainsByGroupId( diff --git a/src/chart_types/xy_chart/state/utils/utils.test.ts b/src/chart_types/xy_chart/state/utils/utils.test.ts index d751f1fee6..f167579b91 100644 --- a/src/chart_types/xy_chart/state/utils/utils.test.ts +++ b/src/chart_types/xy_chart/state/utils/utils.test.ts @@ -24,6 +24,7 @@ import { MockDataSeries } from '../../../../mocks/series/series'; import { MockSeriesSpec, MockGlobalSpec } from '../../../../mocks/specs'; import { MockStore } from '../../../../mocks/store'; import { SeededDataGenerator } from '../../../../mocks/utils'; +import { MockXDomain, MockYDomain } from '../../../../mocks/xy/domains'; import { ScaleContinuous } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; import { Spec } from '../../../../specs'; @@ -35,6 +36,7 @@ import { getSeriesIndex, XYChartSeriesIdentifier } from '../../utils/series'; import { BasicSeriesSpec, HistogramModeAlignments, SeriesColorAccessorFn } from '../../utils/specs'; import { computeSeriesDomainsSelector } from '../selectors/compute_series_domains'; import { computeSeriesGeometriesSelector } from '../selectors/compute_series_geometries'; +import { getScaleConfigsFromSpecs } from '../selectors/get_api_scale_configs'; import { computeSeriesDomains, computeXScaleOffset, @@ -77,33 +79,30 @@ describe('Chart State utils', () => { yAccessors: ['y'], data: BARCHART_1Y0G, }); - const domains = computeSeriesDomains([spec1, spec2], new Map()); - expect(domains.xDomain).toEqual({ - domain: [0, 3], - isBandScale: false, - scaleType: ScaleType.Linear, - minInterval: 1, - type: 'xDomain', - }); + const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig); + expect(domains.xDomain).toEqual( + MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 3], + isBandScale: false, + minInterval: 1, + }), + ); expect(domains.yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Log, { domain: [0, 10], - scaleType: ScaleType.Log, groupId: 'group1', isBandScale: false, - type: 'yDomain', logBase: undefined, logMinLimit: undefined, - }, - { + }), + MockYDomain.fromScaleType(ScaleType.Log, { domain: [0, 10], - scaleType: ScaleType.Log, groupId: 'group2', isBandScale: false, - type: 'yDomain', logBase: undefined, logMinLimit: undefined, - }, + }), ]); expect(domains.formattedDataSeries).toMatchSnapshot(); }); @@ -129,33 +128,30 @@ describe('Chart State utils', () => { stackAccessors: ['x'], data: BARCHART_1Y1G, }); - const domains = computeSeriesDomains([spec1, spec2], new Map()); - expect(domains.xDomain).toEqual({ - domain: [0, 3], - isBandScale: false, - scaleType: ScaleType.Linear, - minInterval: 1, - type: 'xDomain', - }); + const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig); + expect(domains.xDomain).toEqual( + MockXDomain.fromScaleType(ScaleType.Linear, { + domain: [0, 3], + isBandScale: false, + minInterval: 1, + }), + ); expect(domains.yDomains).toEqual([ - { + MockYDomain.fromScaleType(ScaleType.Log, { domain: [0, 5], - scaleType: ScaleType.Log, groupId: 'group1', isBandScale: false, - type: 'yDomain', logBase: undefined, logMinLimit: undefined, - }, - { + }), + MockYDomain.fromScaleType(ScaleType.Log, { domain: [0, 9], - scaleType: ScaleType.Log, groupId: 'group2', isBandScale: false, - type: 'yDomain', logBase: undefined, logMinLimit: undefined, - }, + }), ]); expect(domains.formattedDataSeries.filter(({ isStacked }) => isStacked)).toMatchSnapshot(); expect(domains.formattedDataSeries.filter(({ isStacked }) => !isStacked)).toMatchSnapshot(); @@ -764,7 +760,7 @@ describe('Chart State utils', () => { data: BARCHART_1Y1G, }); const histogramBar = MockSeriesSpec.histogramBar({ - id: 'histo', + id: 'histogram', groupId: 'group2', yScaleType: ScaleType.Log, xScaleType: ScaleType.Linear, diff --git a/src/chart_types/xy_chart/state/utils/utils.ts b/src/chart_types/xy_chart/state/utils/utils.ts index 39de5946be..6adeac5fb3 100644 --- a/src/chart_types/xy_chart/state/utils/utils.ts +++ b/src/chart_types/xy_chart/state/utils/utils.ts @@ -20,7 +20,7 @@ import { getPredicateFn, Predicate } from '../../../../common/predicate'; import { SeriesKey, SeriesIdentifier } from '../../../../common/series_id'; import { Scale } from '../../../../scales'; -import { CustomXDomain, SortSeriesByConfig } from '../../../../specs'; +import { SortSeriesByConfig } from '../../../../specs'; import { OrderBy } from '../../../../specs/settings'; import { mergePartial, Rotation, Color, isUniqueArray } from '../../../../utils/common'; import { CurveType } from '../../../../utils/curves'; @@ -62,9 +62,9 @@ import { Fit, FitConfig, isBubbleSeriesSpec, - YDomainRange, } from '../../utils/specs'; import { SmallMultipleScales } from '../selectors/compute_small_multiple_scales'; +import { ScaleConfigs } from '../selectors/get_api_scale_configs'; import { SmallMultiplesGroupBy } from '../selectors/get_specs'; import { isHorizontalRotation } from './common'; import { getSpecsById, getAxesSpecForSpecId, getSpecDomainGroupId } from './spec'; @@ -116,22 +116,12 @@ export function getCustomSeriesColors(dataSeries: DataSeries[]): Map, the custom X domain - * @param deselectedDataSeries is optional; if not supplied, - * @param customXDomain is optional; if not supplied, - * @param orderOrdinalBinsBy - * @param smallMultiples - * @param sortSeriesBy - * @returns `SeriesDomainsAndData` * @internal */ export function computeSeriesDomains( seriesSpecs: BasicSeriesSpec[], - customYDomainsByGroupId: Map = new Map(), + scaleConfigs: ScaleConfigs, deselectedDataSeries: SeriesIdentifier[] = [], - customXDomain?: CustomXDomain, orderOrdinalBinsBy?: OrderBy, smallMultiples?: SmallMultiplesGroupBy, sortSeriesBy?: SeriesCompareFn | SortSeriesByConfig, @@ -143,21 +133,21 @@ export function computeSeriesDomains( smallMultiples, ); // compute the x domain merging any custom domain - const xDomain = mergeXDomain(seriesSpecs, xValues, customXDomain, fallbackScale); + const xDomain = mergeXDomain(scaleConfigs.x, xValues, fallbackScale); // fill series with missing x values - const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.scaleType); + const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.type); const seriesSortFn = getRenderingCompareFn(sortSeriesBy, (a: SeriesIdentifier, b: SeriesIdentifier) => { return defaultXYSeriesSort(a as DataSeries, b as DataSeries); }); - const formattedDataSeries = getFormattedDataSeries(seriesSpecs, filledDataSeries, xValues, xDomain.scaleType).sort( + const formattedDataSeries = getFormattedDataSeries(seriesSpecs, filledDataSeries, xValues, xDomain.type).sort( seriesSortFn, ); // let's compute the yDomains after computing all stacked values - const yDomains = mergeYDomain(formattedDataSeries, customYDomainsByGroupId); + const yDomains = mergeYDomain(formattedDataSeries, scaleConfigs.y); // sort small multiples values const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index 88fe6976b2..04dafeed58 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -23,6 +23,7 @@ import moment from 'moment-timezone'; import { ChartType } from '../..'; import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs'; import { MockStore } from '../../../mocks/store/store'; +import { MockXDomain, MockYDomain } from '../../../mocks/xy/domains'; import { Scale } from '../../../scales'; import { ScaleType } from '../../../scales/constants'; import { SpecType } from '../../../specs/constants'; @@ -34,7 +35,6 @@ import { OrdinalDomain } from '../../../utils/domain'; import { AxisId, GroupId } from '../../../utils/ids'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { AxisStyle, TextOffset } from '../../../utils/themes/theme'; -import { XDomain, YDomain } from '../domains/types'; import { computeAxesGeometriesSelector } from '../state/selectors/compute_axes_geometries'; import { computeAxisTicksDimensionsSelector } from '../state/selectors/compute_axis_ticks_dimensions'; import { getScale, SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; @@ -189,21 +189,17 @@ describe('Axis computational utils', () => { [1, 1], ], }); - const xDomain: XDomain = { - type: 'xDomain', - scaleType: ScaleType.Linear, + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 1], isBandScale: false, minInterval: 0, - }; + }); - const yDomain: YDomain = { - scaleType: ScaleType.Linear, + const yDomain = MockYDomain.fromScaleType(ScaleType.Linear, { groupId: 'group_1', - type: 'yDomain', domain: [0, 1], isBandScale: false, - }; + }); const getSmScales = (smHDomain: OrdinalDomain = [], smVDomain: OrdinalDomain = []): SmallMultipleScales => ({ horizontal: getScale(smHDomain, chartDim.width), @@ -279,14 +275,12 @@ describe('Axis computational utils', () => { test('should compute axis dimensions with timeZone', () => { const bboxCalculator = new SvgTextBBoxCalculator(); - const xDomain: XDomain = { - type: 'xDomain', - scaleType: ScaleType.Time, + const xDomain = MockXDomain.fromScaleType(ScaleType.Time, { domain: [1551438000000, 1551441300000], isBandScale: false, minInterval: 0, timeZone: 'utc', - }; + }); let axisDimensions = computeAxisTicksDimensions( xAxisWithTime, xDomain, @@ -419,20 +413,18 @@ describe('Axis computational utils', () => { { label: '0.7', position: 30 + rotationalOffset, value: 0.7 }, { label: '0.8', position: 20 + rotationalOffset, value: 0.8 }, { label: '0.9', position: 10 + rotationalOffset, value: 0.9 }, - { label: '1', position: 0 + rotationalOffset, value: 1 }, + { label: '1', position: rotationalOffset, value: 1 }, ]; expect(axisPositions).toEqual(expectedAxisPositions); }); test('should extend ticks to domain + minInterval in histogram mode for linear scale', () => { const enableHistogramMode = true; - const xBandDomain: XDomain = { - type: 'xDomain', - scaleType: ScaleType.Linear, + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 100], isBandScale: true, minInterval: 10, - }; + }); const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); const histogramAxisPositions = getAvailableTicks( horizontalAxisSpec, @@ -448,13 +440,11 @@ describe('Axis computational utils', () => { test('should extend ticks to domain + minInterval in histogram mode for time scale', () => { const enableHistogramMode = true; - const xBandDomain: XDomain = { - type: 'xDomain', - scaleType: ScaleType.Time, + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Time, { domain: [1560438420000, 1560438510000], isBandScale: true, minInterval: 90000, - }; + }); const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); const histogramAxisPositions = getAvailableTicks( horizontalAxisSpec, @@ -487,13 +477,11 @@ describe('Axis computational utils', () => { test('should extend ticks to domain + minInterval in histogram mode for a scale with single datum', () => { const enableHistogramMode = true; - const xBandDomain: XDomain = { - type: 'xDomain', - scaleType: ScaleType.Time, + const xBandDomain = MockXDomain.fromScaleType(ScaleType.Time, { domain: [1560438420000, 1560438420000], // a single datum scale will have the same value for domain start & end isBandScale: true, minInterval: 90000, - }; + }); const xScale = getScaleForAxisSpec(horizontalAxisSpec, xBandDomain, [yDomain], 1, 0, 100, 0); const histogramAxisPositions = getAvailableTicks( horizontalAxisSpec, @@ -1523,13 +1511,11 @@ describe('Axis computational utils', () => { style, tickFormat: formatter, }; - const xDomainTime: XDomain = { - type: 'xDomain', + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { isBandScale: false, domain: [1547190000000, 1547622000000], minInterval: 86400000, - scaleType: ScaleType.Time, - }; + }); const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); const offset = 0; const tickFormatOption = { timeZone: 'utc+1' }; @@ -1557,14 +1543,12 @@ describe('Axis computational utils', () => { tickFormat: (d, options) => DateTime.fromMillis(d, { setZone: true, zone: options?.timeZone ?? 'utc+1' }).toFormat('HH:mm'), }; - const xDomainTime: XDomain = { - type: 'xDomain', + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { isBandScale: false, timeZone: 'utc+1', domain: [1547190000000, 1547622000000], minInterval: 86400000, - scaleType: ScaleType.Time, - }; + }); const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); const offset = 0; const tickFormatOption = { timeZone: xDomainTime.timeZone }; @@ -1600,13 +1584,11 @@ describe('Axis computational utils', () => { style, tickFormat: formatter, }; - const xDomainTime: XDomain = { - type: 'xDomain', + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { isBandScale: false, domain: [1547190000000, 1547622000000], minInterval: 86400000, - scaleType: ScaleType.Time, - }; + }); const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); const offset = 0; const tickFormatOption = { timeZone: 'utc+1' }; @@ -1640,13 +1622,11 @@ describe('Axis computational utils', () => { style, tickFormat: formatter, }; - const xDomainTime: XDomain = { - type: 'xDomain', + const xDomainTime = MockXDomain.fromScaleType(ScaleType.Time, { isBandScale: false, domain: [1547190000000, 1547622000000], minInterval: 86400000, - scaleType: ScaleType.Time, - }; + }); const scale: Scale = computeXScale({ xDomain: xDomainTime, totalBarsInCluster: 0, range: [0, 603.5] }); const offset = 0; const tickFormatOption = { timeZone: 'utc+1' }; diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index a1a5aa4771..19c86316cd 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -177,7 +177,6 @@ export function getScaleForAxisSpec( const yScales = computeYScales({ yDomains, range, - ticks: axisSpec.ticks, integersOnly: axisSpec.integersOnly, }); if (yScales.has(axisSpec.groupId)) { @@ -191,7 +190,6 @@ export function getScaleForAxisSpec( range, barsPadding, enableHistogramMode, - ticks: axisSpec.ticks, integersOnly: axisSpec.integersOnly, }); } diff --git a/src/chart_types/xy_chart/utils/scales.test.ts b/src/chart_types/xy_chart/utils/scales.test.ts index b2b9c3a4d7..8750459551 100644 --- a/src/chart_types/xy_chart/utils/scales.test.ts +++ b/src/chart_types/xy_chart/utils/scales.test.ts @@ -17,26 +17,22 @@ * under the License. */ +import { MockXDomain } from '../../../mocks/xy/domains'; import { ScaleType } from '../../../scales/constants'; -import { XDomain } from '../domains/types'; import { computeXScale } from './scales'; describe('Series scales', () => { - const xDomainLinear: XDomain = { - type: 'xDomain', + const xDomainLinear = MockXDomain.fromScaleType(ScaleType.Linear, { isBandScale: true, domain: [0, 3], minInterval: 1, - scaleType: ScaleType.Linear, - }; + }); - const xDomainOrdinal: XDomain = { - type: 'xDomain', + const xDomainOrdinal = MockXDomain.fromScaleType(ScaleType.Ordinal, { isBandScale: true, domain: ['a', 'b'], minInterval: 1, - scaleType: ScaleType.Ordinal, - }; + }); test('should compute X Scale linear min, max with bands', () => { const scale = computeXScale({ xDomain: xDomainLinear, totalBarsInCluster: 1, range: [0, 120] }); @@ -64,17 +60,15 @@ describe('Series scales', () => { const minInterval = 1; test('should return extended domain & range when in histogram mode', () => { - const xDomainSingleValue: XDomain = { - type: 'xDomain', + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { isBandScale: true, domain: [singleDomainValue, singleDomainValue], minInterval, - scaleType: ScaleType.Linear, - }; + }); const enableHistogramMode = true; const scale = computeXScale({ - xDomain: xDomainSingleValue, + xDomain, totalBarsInCluster: 1, range: [0, maxRange], barsPadding: 0, @@ -87,17 +81,15 @@ describe('Series scales', () => { }); test('should return unextended domain & range when not in histogram mode', () => { - const xDomainSingleValue: XDomain = { - type: 'xDomain', + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { isBandScale: true, domain: [singleDomainValue, singleDomainValue], minInterval, - scaleType: ScaleType.Linear, - }; + }); const enableHistogramMode = false; const scale = computeXScale({ - xDomain: xDomainSingleValue, + xDomain, totalBarsInCluster: 1, range: [0, maxRange], barsPadding: 0, @@ -121,13 +113,11 @@ describe('Series scales', () => { }); describe('bandwidth when totalBarsInCluster is greater than 0 or less than 0', () => { - const xDomainLinear: XDomain = { - type: 'xDomain', + const xDomainLinear = MockXDomain.fromScaleType(ScaleType.Linear, { isBandScale: true, domain: [0, 3], minInterval: 1, - scaleType: ScaleType.Linear, - }; + }); const maxRange = 120; const scaleOver0 = computeXScale({ xDomain: xDomainLinear, diff --git a/src/chart_types/xy_chart/utils/scales.ts b/src/chart_types/xy_chart/utils/scales.ts index 985298067b..3c9125e89d 100644 --- a/src/chart_types/xy_chart/utils/scales.ts +++ b/src/chart_types/xy_chart/utils/scales.ts @@ -46,7 +46,6 @@ interface XScaleOptions { range: Range; barsPadding?: number; enableHistogramMode?: boolean; - ticks?: number; integersOnly?: boolean; logBase?: LogBase; logMinLimit?: number; @@ -54,17 +53,14 @@ interface XScaleOptions { /** * Compute the x scale used to align geometries to the x axis. - * @param xDomain the x domain - * @param totalBarsInCluster the total number of grouped series - * @param axisLength the length of the x axis * @internal */ export function computeXScale(options: XScaleOptions): Scale { - const { xDomain, totalBarsInCluster, range, barsPadding, enableHistogramMode, ticks, integersOnly } = options; - const { scaleType, minInterval, domain, isBandScale, timeZone, logBase } = xDomain; + const { xDomain, totalBarsInCluster, range, barsPadding, enableHistogramMode, integersOnly } = options; + const { type, nice, minInterval, domain, isBandScale, timeZone, logBase, desiredTickCount } = xDomain; const rangeDiff = Math.abs(range[1] - range[0]); const isInverse = range[1] < range[0]; - if (scaleType === ScaleType.Ordinal) { + if (type === ScaleType.Ordinal) { const dividend = totalBarsInCluster > 0 ? totalBarsInCluster : 1; const bandwidth = rangeDiff / (domain.length * dividend); return new ScaleBand(domain, range, bandwidth, barsPadding); @@ -77,15 +73,16 @@ export function computeXScale(options: XScaleOptions): Scale { const adjustedDomain = [domainMin, adjustedDomainMax]; const intervalCount = (adjustedDomain[1] - adjustedDomain[0]) / minInterval; - const intervalCountOffest = isSingleValueHistogram ? 0 : 1; - const bandwidth = rangeDiff / (intervalCount + intervalCountOffest); + const intervalCountOffset = isSingleValueHistogram ? 0 : 1; + const bandwidth = rangeDiff / (intervalCount + intervalCountOffset); const { start, end } = getBandScaleRange(isInverse, isSingleValueHistogram, range[0], range[1], bandwidth); - const scale = new ScaleContinuous( + return new ScaleContinuous( { - type: scaleType, + type, domain: adjustedDomain, range: [start, end], + nice, }, { bandwidth: totalBarsInCluster > 0 ? bandwidth / totalBarsInCluster : bandwidth, @@ -93,23 +90,21 @@ export function computeXScale(options: XScaleOptions): Scale { timeZone, totalBarsInCluster, barsPadding, - ticks, + desiredTickCount, isSingleValueHistogram, logBase, }, ); - - return scale; } return new ScaleContinuous( - { type: scaleType, domain, range }, + { type, domain, range, nice }, { bandwidth: 0, minInterval, timeZone, totalBarsInCluster, barsPadding, - ticks, + desiredTickCount, integersOnly, logBase, }, @@ -119,35 +114,31 @@ export function computeXScale(options: XScaleOptions): Scale { interface YScaleOptions { yDomains: YDomain[]; range: Range; - ticks?: number; integersOnly?: boolean; } /** * Compute the y scales, one per groupId for the y axis. - * @param yDomains the y domains - * @param axisLength the axisLength of the y axis * @internal */ export function computeYScales(options: YScaleOptions): Map { - const yScales: Map = new Map(); - const { yDomains, range, ticks, integersOnly } = options; - yDomains.forEach(({ scaleType: type, domain, groupId, logBase, logMinLimit }) => { + const { yDomains, range, integersOnly } = options; + return yDomains.reduce((yScales, { type, nice, desiredTickCount, domain, groupId, logBase, logMinLimit }) => { const yScale = new ScaleContinuous( { type, domain, range, + nice, }, { - ticks, + desiredTickCount, integersOnly, logBase, logMinLimit, }, ); yScales.set(groupId, yScale); - }); - - return yScales; + return yScales; + }, new Map()); } diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index d3f6575b46..6d48c8201c 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -28,6 +28,7 @@ import { GroupId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { ColorConfig } from '../../../utils/themes/theme'; import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec } from '../domains/y_domain'; +import { X_SCALE_DEFAULT } from '../scales/scale_defaults'; import { SmallMultiplesGroupBy } from '../state/selectors/get_specs'; import { applyFitFunctionToDataSeries } from './fit_function_utils'; import { groupBy } from './group_data_series'; @@ -455,7 +456,7 @@ export function getDataSeriesFromSpecs( // keep the user order for ordinal scales xValues, ...smallMultipleUniqueValues, - fallbackScale: !isOrdinalScale && !isNumberArray ? ScaleType.Ordinal : undefined, + fallbackScale: !isOrdinalScale && !isNumberArray ? X_SCALE_DEFAULT.type : undefined, }; } diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index c8c0c6a053..313651a868 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -266,11 +266,11 @@ interface DomainBase { /** * Custom minInterval for the domain which will affect data bucket size. * The minInterval cannot be greater than the computed minimum interval between any two adjacent data points. - * Further, if you specify a custom numeric minInterval for a timeseries, please note that due to the restriction + * Further, if you specify a custom numeric minInterval for a time-series, please note that due to the restriction * above, the specified numeric minInterval will be interpreted as a fixed interval. - * This means that, for example, if you have yearly timeseries data that ranges from 2016 to 2019 and you manually + * This means that, for example, if you have yearly time-series data that ranges from 2016 to 2019 and you manually * compute the interval between 2016 and 2017, you'll have 366 days due to 2016 being a leap year. This will not - * be a valid interval because it is greater than the computed minInterval of 365 days betwen the other years. + * be a valid interval because it is greater than the computed minInterval of 365 days between the other years. */ minInterval?: number; } @@ -462,6 +462,11 @@ export interface SeriesScales { * @defaultValue `ordinal` {@link (ScaleType:type) | ScaleType.Ordinal} */ xScaleType: XScaleType; + /** + * Extends the x domain so that it starts and ends on nice round values. + * @defaultValue `false` + */ + xNice?: boolean; /** * If using a ScaleType.Time this timezone identifier is required to * compute a nice set of xScale ticks. Can be any IANA zone supported by @@ -474,6 +479,11 @@ export interface SeriesScales { * @defaultValue `linear` {@link (ScaleType:type) | ScaleType.Linear} */ yScaleType: ScaleContinuousType; + /** + * Extends the y domain so that it starts and ends on nice round values. + * @defaultValue `false` + */ + yNice?: boolean; /** * if true, the min y value is set to the minimum domain value, 0 otherwise * @deprecated use `domain.fit` instead diff --git a/src/mocks/xy/domains.ts b/src/mocks/xy/domains.ts new file mode 100644 index 0000000000..5d3f752d36 --- /dev/null +++ b/src/mocks/xy/domains.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { XDomain, YDomain } from '../../chart_types/xy_chart/domains/types'; +import { + getXNiceFromSpec, + getXScaleTypeFromSpec, + getYNiceFromSpec, + getYScaleTypeFromSpec, +} from '../../chart_types/xy_chart/scales/get_api_scales'; +import { X_SCALE_DEFAULT, Y_SCALE_DEFAULT } from '../../chart_types/xy_chart/scales/scale_defaults'; +import { DEFAULT_GLOBAL_ID, XScaleType } from '../../chart_types/xy_chart/utils/specs'; +import { ScaleContinuousType } from '../../scales'; +import { ScaleType } from '../../scales/constants'; +import { mergePartial, RecursivePartial } from '../../utils/common'; + +/** @internal */ +export class MockXDomain { + private static readonly base: XDomain = { + ...X_SCALE_DEFAULT, + isBandScale: X_SCALE_DEFAULT.type !== ScaleType.Ordinal, + minInterval: 0, + timeZone: undefined, + domain: [0, 1], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockXDomain.base, partial, { mergeOptionalPartialValues: true }); + } + + static fromScaleType(scaleType: XScaleType, partial?: RecursivePartial) { + return mergePartial(MockXDomain.base, partial, { mergeOptionalPartialValues: true }, [ + { + type: getXScaleTypeFromSpec(scaleType), + nice: getXNiceFromSpec(), + }, + ]); + } +} + +/** @internal */ +export class MockYDomain { + private static readonly base: YDomain = { + ...Y_SCALE_DEFAULT, + isBandScale: false, + groupId: DEFAULT_GLOBAL_ID, + domain: [0, 1], + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockYDomain.base, partial, { mergeOptionalPartialValues: true }); + } + + static fromScaleType(scaleType: ScaleContinuousType, partial?: RecursivePartial) { + return mergePartial(MockYDomain.base, partial, { mergeOptionalPartialValues: true }, [ + { + type: getYScaleTypeFromSpec(scaleType), + nice: getYNiceFromSpec(), + }, + ]); + } +} diff --git a/src/scales/scale_continuous.test.ts b/src/scales/scale_continuous.test.ts index 84345fe663..1c66b51ed3 100644 --- a/src/scales/scale_continuous.test.ts +++ b/src/scales/scale_continuous.test.ts @@ -20,8 +20,8 @@ import { DateTime, Settings } from 'luxon'; import { ScaleContinuous, ScaleBand } from '.'; -import { XDomain } from '../chart_types/xy_chart/domains/types'; import { computeXScale } from '../chart_types/xy_chart/utils/scales'; +import { MockXDomain } from '../mocks/xy/domains'; import { ContinuousDomain, Range } from '../utils/domain'; import { LOG_MIN_ABS_DOMAIN, ScaleType } from './constants'; import { limitLogScaleDomain } from './scale_continuous'; @@ -101,13 +101,11 @@ describe('Scale Continuous', () => { }); test('invert with step x value on linear band scale', () => { const data = [0, 1, 2]; - const xDomain: XDomain = { + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 2], isBandScale: true, minInterval: 1, - scaleType: ScaleType.Linear, - type: 'xDomain', - }; + }); const scaleLinear = computeXScale({ xDomain, totalBarsInCluster: 1, range: [0, 119], barsPadding: 0 }); expect(scaleLinear.bandwidth).toBe(119 / 3); @@ -142,13 +140,11 @@ describe('Scale Continuous', () => { test('can get the right x value on linear scale with band', () => { const data = [0, 10, 20, 50, 90]; - const xDomain: XDomain = { + const xDomain = MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 100], isBandScale: true, minInterval: 10, - scaleType: ScaleType.Linear, - type: 'xDomain', - }; + }); // we tweak the maxRange removing the bandwidth to correctly compute // a band linear scale in computeXScale const scaleLinear = computeXScale({ xDomain, totalBarsInCluster: 1, range: [0, 109], barsPadding: 0 }); diff --git a/src/scales/scale_continuous.ts b/src/scales/scale_continuous.ts index 8fd8f3f12f..c265ee150e 100644 --- a/src/scales/scale_continuous.ts +++ b/src/scales/scale_continuous.ts @@ -27,6 +27,7 @@ import { ScaleLogarithmic, ScalePower, ScaleTime, + ScaleContinuousNumeric, } from 'd3-scale'; import { $Values, Required } from 'utility-types'; @@ -152,6 +153,7 @@ interface ScaleData { domain: any[]; /** The data output range */ range: Range; + nice?: boolean; } /** @@ -207,7 +209,7 @@ type ScaleOptions = Required & { * The approximated number of ticks. * @defaultValue 10 */ - ticks: number; + desiredTickCount: number; /** * true if the scale was adjusted to fit one single value histogram */ @@ -224,7 +226,7 @@ const defaultScaleOptions: ScaleOptions = { timeZone: 'utc', totalBarsInCluster: 1, barsPadding: 0, - ticks: 10, + desiredTickCount: 10, isSingleValueHistogram: false, integersOnly: false, logBase: LogBase.Common, @@ -263,21 +265,22 @@ export class ScaleContinuous implements Scale { private readonly d3Scale: D3Scale; - constructor(scaleData: ScaleData, options?: Partial) { - const { type, domain, range } = scaleData; + constructor( + { type = ScaleType.Linear, domain = [0, 1], range = [0, 1], nice = false }: ScaleData, + options?: Partial, + ) { const { bandwidth, minInterval, timeZone, totalBarsInCluster, barsPadding, - ticks, + desiredTickCount, isSingleValueHistogram, integersOnly, logBase, logMinLimit, } = mergePartial(defaultScaleOptions, options, { mergeOptionalPartialValues: true }); - this.d3Scale = SCALES[type](); if (type === ScaleType.Log) { @@ -288,6 +291,10 @@ export class ScaleContinuous implements Scale { } this.d3Scale.domain(this.domain); + if (nice && type !== ScaleType.Time) { + (this.d3Scale as ScaleContinuousNumeric).domain(this.domain).nice(desiredTickCount); + this.domain = this.d3Scale.domain(); + } const safeBarPadding = maxValueWithUpperLimit(barsPadding, 0, 1); this.barsPadding = safeBarPadding; @@ -311,7 +318,7 @@ export class ScaleContinuous implements Scale { const shiftedDomainMax = endDomain.add(offset, 'minutes').valueOf(); const tzShiftedScale = scaleUtc().domain([shiftedDomainMin, shiftedDomainMax]); - const rawTicks = tzShiftedScale.ticks(ticks); + const rawTicks = tzShiftedScale.ticks(desiredTickCount); const timePerTick = (shiftedDomainMax - shiftedDomainMin) / rawTicks.length; const hasHourTicks = timePerTick < 1000 * 60 * 60 * 12; @@ -327,7 +334,7 @@ export class ScaleContinuous implements Scale { const intervalCount = Math.floor((this.domain[1] - this.domain[0]) / this.minInterval); this.tickValues = new Array(intervalCount + 1).fill(0).map((_, i) => this.domain[0] + i * this.minInterval); } else { - this.tickValues = this.getTicks(ticks, integersOnly); + this.tickValues = this.getTicks(desiredTickCount, integersOnly); } } } diff --git a/src/scales/scales.test.ts b/src/scales/scales.test.ts index bcb50ab5a5..9a8664ab0b 100644 --- a/src/scales/scales.test.ts +++ b/src/scales/scales.test.ts @@ -46,7 +46,12 @@ describe('Scale Test', () => { const data = [0, 10]; const minRange = 0; const maxRange = 100; - const linearScale = new ScaleContinuous({ type: ScaleType.Linear, domain: data, range: [minRange, maxRange] }); + const linearScale = new ScaleContinuous({ + type: ScaleType.Linear, + domain: data, + range: [minRange, maxRange], + nice: false, + }); const { domain, range } = linearScale; expect(domain).toEqual([0, 10]); expect(range).toEqual([minRange, maxRange]); @@ -66,7 +71,12 @@ describe('Scale Test', () => { const data = [date1, date3]; const minRange = 0; const maxRange = 100; - const timeScale = new ScaleContinuous({ type: ScaleType.Time, domain: data, range: [minRange, maxRange] }); + const timeScale = new ScaleContinuous({ + type: ScaleType.Time, + domain: data, + range: [minRange, maxRange], + nice: false, + }); const { domain, range } = timeScale; expect(domain).toEqual([date1, date3]); expect(range).toEqual([minRange, maxRange]); @@ -81,7 +91,12 @@ describe('Scale Test', () => { const data = [1, 10]; const minRange = 0; const maxRange = 100; - const logScale = new ScaleContinuous({ type: ScaleType.Log, domain: data, range: [minRange, maxRange] }); + const logScale = new ScaleContinuous({ + type: ScaleType.Log, + domain: data, + range: [minRange, maxRange], + nice: false, + }); const { domain, range } = logScale; expect(domain).toEqual([1, 10]); expect(range).toEqual([minRange, maxRange]); @@ -94,7 +109,12 @@ describe('Scale Test', () => { const data = [0, 10]; const minRange = 0; const maxRange = 100; - const logScale = new ScaleContinuous({ type: ScaleType.Log, domain: data, range: [minRange, maxRange] }); + const logScale = new ScaleContinuous({ + type: ScaleType.Log, + domain: data, + range: [minRange, maxRange], + nice: false, + }); const { domain, range } = logScale; expect(domain).toEqual([1, 10]); expect(range).toEqual([minRange, maxRange]); @@ -107,7 +127,12 @@ describe('Scale Test', () => { const data = [0, 10]; const minRange = 0; const maxRange = 100; - const sqrtScale = new ScaleContinuous({ type: ScaleType.Sqrt, domain: data, range: [minRange, maxRange] }); + const sqrtScale = new ScaleContinuous({ + type: ScaleType.Sqrt, + domain: data, + range: [minRange, maxRange], + nice: false, + }); const { domain, range } = sqrtScale; expect(domain).toEqual([0, 10]); expect(range).toEqual([minRange, maxRange]); @@ -165,7 +190,7 @@ describe('Scale Test', () => { const maxRange = 120; const bandwidth = maxRange / 3; const linearScale = new ScaleContinuous( - { type: ScaleType.Linear, domain: domainLinear, range: [minRange, maxRange - bandwidth] }, // we currently limit the range like that a band linear scale + { type: ScaleType.Linear, domain: domainLinear, range: [minRange, maxRange - bandwidth], nice: false }, // we currently limit the range like that a band linear scale { bandwidth, minInterval: 1 }, ); const ordinalScale = new ScaleBand(domainOrdinal, [minRange, maxRange]); @@ -188,6 +213,7 @@ describe('Scale Test', () => { type: ScaleType.Linear, domain: dataLinear, range: [minRange, maxRange - bandwidth], + nice: false, }, // we currently limit the range like that a band linear scale { bandwidth, minInterval: 1 }, ); diff --git a/stories/axes/8_custom_domain.tsx b/stories/axes/8_custom_domain.tsx index 3fe1134241..06c083ec9b 100644 --- a/stories/axes/8_custom_domain.tsx +++ b/stories/axes/8_custom_domain.tsx @@ -20,73 +20,139 @@ import { boolean, number } from '@storybook/addon-knobs'; import React from 'react'; -import { Axis, BarSeries, Chart, LineSeries, Position, ScaleType, Settings } from '../../src'; +import { Axis, BarSeries, Chart, LIGHT_THEME, LineSeries, Position, ScaleType, Settings } from '../../src'; export const Example = () => { - const leftDomain = { - min: number('left min', 0), - max: number('left max', 7), + const customXDomain = boolean('customize X domain', true, 'X axis'); + const customBarYDomain = boolean('customize Y domain', true, 'Bar'); + const customLineYDomain = boolean('customize Y domain', true, 'Line'); + const options = { + range: true, + min: -10, + max: 20, + step: 0.1, + }; + const barDomain = { + min: number('Bar min', -5, options, 'Bar'), + max: number('Bar max', 7, options, 'Bar'), }; - const rightDomain1 = { - min: number('right1 min', 0), - max: number('right1 max', 10), + const lineDomain = { + min: number('Line min', 0, options, 'Line'), + max: number('Line max', 10, options, 'Line'), }; - const rightDomain2 = { - min: number('right2 min', 0), - max: number('right2 max', 10), + const ticksOptions = { + range: true, + min: 1, + max: 15, + step: 1, }; + const barTicks = number('Bar ticks', 4, ticksOptions, 'Bar'); + const lineTicks = number('Line ticks', 10, ticksOptions, 'Line'); + const xOptions = { + range: true, + min: 0, + max: 6, + step: 1, + }; const xDomain = { - min: number('xDomain min', 0), - max: number('xDomain max', 3), + min: number('X min', 0, xOptions, 'X axis'), + max: number('X max', 3, xOptions, 'X axis'), }; + + const showBars = boolean('show bars', true, 'Bar'); + const niceDomainBar = boolean('nice domain', true, 'Bar'); + const niceDomainLine = boolean('nice domain', true, 'Line'); return ( - - + Number(d).toFixed(2)} - domain={leftDomain} - hide={boolean('hide left axis', false)} + id="bottom" + position={Position.Bottom} + title="X axis" + style={{ + tickLine: { + visible: true, + }, + }} /> Number(d).toFixed(2)} - domain={rightDomain1} + domain={customBarYDomain ? barDomain : undefined} + hide={boolean('Hide bar axis', false, 'Bar')} + ticks={barTicks} + style={{ + axisLine: { + stroke: LIGHT_THEME.colors.vizColors[0], + strokeWidth: 1.5, + }, + axisTitle: { + fill: LIGHT_THEME.colors.vizColors[0], + }, + tickLabel: { + fill: LIGHT_THEME.colors.vizColors[0], + }, + tickLine: { + stroke: LIGHT_THEME.colors.vizColors[0], + }, + }} /> Number(d).toFixed(2)} - domain={rightDomain2} - /> - + {showBars && ( + + )} ( +