diff --git a/src/lib/axes/axis_utils.test.ts b/src/lib/axes/axis_utils.test.ts index 003ab98b7a..e954a19a57 100644 --- a/src/lib/axes/axis_utils.test.ts +++ b/src/lib/axes/axis_utils.test.ts @@ -1,8 +1,8 @@ import { XDomain } from '../series/domains/x_domain'; import { YDomain } from '../series/domains/y_domain'; -import { Position } from '../series/specs'; +import { AxisSpec, DomainRange, Position } from '../series/specs'; import { LIGHT_THEME } from '../themes/light_theme'; -import { getAxisId, getGroupId } from '../utils/ids'; +import { getAxisId, getGroupId, GroupId } from '../utils/ids'; import { ScaleType } from '../utils/scales/scales'; import { centerRotationOrigin, @@ -14,15 +14,17 @@ import { getAxisTicksPositions, getHorizontalAxisGridLineProps, getHorizontalAxisTickLineProps, - getHorizontalDomain, getMaxBboxDimensions, getMinMaxRange, getScaleForAxisSpec, getTickLabelProps, getVerticalAxisGridLineProps, getVerticalAxisTickLineProps, - getVerticalDomain, getVisibleTicks, + isHorizontal, + isVertical, + isYDomain, + mergeDomainsByGroupId, } from './axis_utils'; import { CanvasTextBBoxCalculator } from './canvas_text_bbox_calculator'; import { SvgTextBBoxCalculator } from './svg_text_bbox_calculator'; @@ -71,7 +73,7 @@ describe('Axis computational utils', () => { maxLabelTextWidth: 10, maxLabelTextHeight: 10, }; - const verticalAxisSpec = { + const verticalAxisSpec: AxisSpec = { id: getAxisId('axis_1'), groupId: getGroupId('group_1'), hide: false, @@ -86,7 +88,7 @@ describe('Axis computational utils', () => { showGridLines: true, }; - const horizontalAxisSpec = { + const horizontalAxisSpec: AxisSpec = { id: getAxisId('axis_2'), groupId: getGroupId('group_1'), hide: false, @@ -149,6 +151,21 @@ describe('Axis computational utils', () => { bboxCalculator.destroy(); }); + test('should not compute axis dimensions when spec is configured to hide', () => { + const bboxCalculator = new CanvasTextBBoxCalculator(); + verticalAxisSpec.hide = true; + const axisDimensions = computeAxisTicksDimensions( + verticalAxisSpec, + xDomain, + [yDomain], + 1, + bboxCalculator, + 0, + axes, + ); + expect(axisDimensions).toBe(null); + }); + test('should compute dimensions for the bounding box containing a rotated label', () => { expect(computeRotatedLabelDimensions({ width: 1, height: 2 }, 0)).toEqual({ width: 1, @@ -914,13 +931,94 @@ describe('Axis computational utils', () => { expect(horizontalAxisGridLines).toEqual([25, 0, 25, 100]); }); - test('should return correct domain based on rotation', () => { - const chartRotation = 180; - expect(getHorizontalDomain(xDomain, [yDomain], chartRotation)).toEqual(xDomain); - expect(getVerticalDomain(xDomain, [yDomain], chartRotation)).toEqual([yDomain]); + test('should determine orientation of axis position', () => { + expect(isVertical(Position.Left)).toBe(true); + expect(isVertical(Position.Right)).toBe(true); + expect(isVertical(Position.Top)).toBe(false); + expect(isVertical(Position.Bottom)).toBe(false); + + expect(isHorizontal(Position.Left)).toBe(false); + expect(isHorizontal(Position.Right)).toBe(false); + expect(isHorizontal(Position.Top)).toBe(true); + expect(isHorizontal(Position.Bottom)).toBe(true); + }); + + test('should determine if axis belongs to yDomain', () => { + const verticalY = isYDomain(Position.Left, 0); + expect(verticalY).toBe(true); + + const verticalX = isYDomain(Position.Left, 90); + expect(verticalX).toBe(false); + + const horizontalX = isYDomain(Position.Top, 0); + expect(horizontalX).toBe(false); + + const horizontalY = isYDomain(Position.Top, 90); + expect(horizontalY).toBe(true); + }); + + test('should merge axis domains by group id', () => { + const groupId = getGroupId('group_1'); + const domainRange1 = { + min: 2, + max: 9, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = new Map(); + axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec); + + // Base case + const expectedSimpleMap = new Map(); + expectedSimpleMap.set(groupId, { min: 2, max: 9 }); + + const simpleDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0); + expect(simpleDomainsByGroupId).toEqual(expectedSimpleMap); + + // Multiple definitions for the same group + const domainRange2 = { + min: 0, + max: 7, + }; + + const altVerticalAxisSpec = { ...verticalAxisSpec, id: getAxisId('axis2') }; + + altVerticalAxisSpec.domain = domainRange2; + axesSpecs.set(altVerticalAxisSpec.id, altVerticalAxisSpec); + + const expectedMergedMap = new Map(); + expectedMergedMap.set(groupId, { min: 0, max: 9 }); + + const mergedDomainsByGroupId = mergeDomainsByGroupId(axesSpecs, 0); + expect(mergedDomainsByGroupId).toEqual(expectedMergedMap); + + // xDomain limit (bad config) + horizontalAxisSpec.domain = { + min: 5, + max: 15, + }; + axesSpecs.set(horizontalAxisSpec.id, horizontalAxisSpec); + + const attemptToMerge = () => { mergeDomainsByGroupId(axesSpecs, 0); }; + + expect(attemptToMerge).toThrowError('[Axis axis_2]: custom domain for xDomain should be defined in Settings'); + }); + + test('should throw on invalid domain', () => { + const domainRange1 = { + min: 9, + max: 2, + }; + + verticalAxisSpec.domain = domainRange1; + + const axesSpecs = new Map(); + axesSpecs.set(verticalAxisSpec.id, verticalAxisSpec); + + const attemptToMerge = () => { mergeDomainsByGroupId(axesSpecs, 0); }; + const expectedError = '[Axis axis_1]: custom domain is invalid, min is greater than max'; - const skewChartRotation = 45; - expect(getHorizontalDomain(xDomain, [yDomain], skewChartRotation)).toEqual([yDomain]); - expect(getVerticalDomain(xDomain, [yDomain], skewChartRotation)).toEqual(xDomain); + expect(attemptToMerge).toThrowError(expectedError); }); }); diff --git a/src/lib/axes/axis_utils.ts b/src/lib/axes/axis_utils.ts index 5e255a2879..ac81e3ce88 100644 --- a/src/lib/axes/axis_utils.ts +++ b/src/lib/axes/axis_utils.ts @@ -1,11 +1,11 @@ import { XDomain } from '../series/domains/x_domain'; import { YDomain } from '../series/domains/y_domain'; import { computeXScale, computeYScales } from '../series/scales'; -import { AxisSpec, Position, Rotation, TickFormatter } from '../series/specs'; +import { AxisSpec, DomainRange, Position, Rotation, TickFormatter } from '../series/specs'; import { AxisConfig, Theme } from '../themes/theme'; import { Dimensions, Margins } from '../utils/dimensions'; import { Domain } from '../utils/domain'; -import { AxisId } from '../utils/ids'; +import { AxisId, GroupId } from '../utils/ids'; import { Scale, ScaleType } from '../utils/scales/scales'; import { BBox, BBoxCalculator } from './bbox_calculator'; @@ -54,6 +54,10 @@ export function computeAxisTicksDimensions( chartRotation: Rotation, axisConfig: AxisConfig, ): AxisTicksDimensions | null { + if (axisSpec.hide) { + return null; + } + const scale = getScaleForAxisSpec( axisSpec, xDomain, @@ -80,6 +84,16 @@ export function computeAxisTicksDimensions( ...dimensions, }; } + +export function isYDomain(position: Position, chartRotation: Rotation): boolean { + const isStraightRotation = chartRotation === 0 || chartRotation === 180; + if (isVertical(position)) { + return isStraightRotation; + } + + return !isStraightRotation; +} + export function getScaleForAxisSpec( axisSpec: AxisSpec, xDomain: XDomain, @@ -89,9 +103,9 @@ export function getScaleForAxisSpec( minRange: number, maxRange: number, ): Scale | null { - const axisDomain = getAxisDomain(axisSpec.position, xDomain, yDomain, chartRotation); - // If axisDomain is an array of values, this is an array of YDomains - if (Array.isArray(axisDomain)) { + const axisIsYDomain = isYDomain(axisSpec.position, chartRotation); + + if (axisIsYDomain) { const yScales = computeYScales(yDomain, minRange, maxRange); if (yScales.has(axisSpec.groupId)) { return yScales.get(axisSpec.groupId)!; @@ -598,43 +612,6 @@ export function computeAxisGridLinePositions( return positions; } -export function getVerticalDomain( - xDomain: XDomain, - yDomain: YDomain[], - chartRotation: number, -): XDomain | YDomain[] { - if (chartRotation === 0 || chartRotation === 180) { - return yDomain; - } else { - return xDomain; - } -} - -export function getHorizontalDomain( - xDomain: XDomain, - yDomain: YDomain[], - chartRotation: number, -): XDomain | YDomain[] { - if (chartRotation === 0 || chartRotation === 180) { - return xDomain; - } else { - return yDomain; - } -} - -export function getAxisDomain( - position: Position, - xDomain: XDomain, - yDomain: YDomain[], - chartRotation: number, -): XDomain | YDomain[] { - if (!isHorizontal(position)) { - return getVerticalDomain(xDomain, yDomain, chartRotation); - } else { - return getHorizontalDomain(xDomain, yDomain, chartRotation); - } -} - export function isVertical(position: Position) { return position === Position.Left || position === Position.Right; } @@ -642,3 +619,45 @@ export function isVertical(position: Position) { export function isHorizontal(position: Position) { return !isVertical(position); } + +export function mergeDomainsByGroupId( + axesSpecs: Map, + chartRotation: Rotation, +): Map { + const domainsByGroupId = new Map(); + + axesSpecs.forEach((spec: AxisSpec, id: AxisId) => { + const { groupId, domain } = spec; + + if (!domain) { + return; + } + + const isAxisYDomain = isYDomain(spec.position, chartRotation); + + if (!isAxisYDomain) { + const errorMessage = `[Axis ${id}]: custom domain for xDomain should be defined in Settings`; + throw new Error(errorMessage); + } + + if (domain.min > domain.max) { + const errorMessage = `[Axis ${id}]: custom domain is invalid, min is greater than max`; + throw new Error(errorMessage); + } + + const prevGroupDomain = domainsByGroupId.get(groupId); + + if (prevGroupDomain) { + const mergedDomain = { + min: Math.min(domain.min, prevGroupDomain.min), + max: Math.max(domain.max, prevGroupDomain.max), + }; + + domainsByGroupId.set(groupId, mergedDomain); + } else { + domainsByGroupId.set(groupId, domain); + } + }); + + return domainsByGroupId; +} diff --git a/src/lib/series/domains/x_domain.test.ts b/src/lib/series/domains/x_domain.test.ts index d072ea7322..1a5bb20845 100644 --- a/src/lib/series/domains/x_domain.test.ts +++ b/src/lib/series/domains/x_domain.test.ts @@ -590,12 +590,12 @@ describe('X Domain', () => { specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); const { xValues } = getSplittedSeries(specDataSeries); + const mergedDomain = mergeXDomain( [ { seriesType: 'area', xScaleType: ScaleType.Linear, - xDomain: [0, 10], }, { seriesType: 'line', @@ -630,4 +630,36 @@ describe('X Domain', () => { const minInterval = findMinInterval([100]); expect(minInterval).toBe(1); }); + test('should account for custom domain when merging a linear domain', () => { + const xValues = new Set([1, 2, 3, 4, 5]); + const xDomain = { min: 0, max: 3 }; + const specs: Array> = + [{ seriesType: 'line', xScaleType: ScaleType.Linear }]; + + const basicMergedDomain = mergeXDomain(specs, xValues, xDomain); + expect(basicMergedDomain.domain).toEqual([0, 3]); + + const arrayXDomain = [1, 2]; + const attemptToMergeArrayDomain = () => { mergeXDomain(specs, xValues, arrayXDomain); }; + const errorMessage = 'xDomain for continuous scale should be a DomainRange object, not an array'; + expect(attemptToMergeArrayDomain).toThrowError(errorMessage); + + const invalidXDomain = { min: 10, max: 0 }; + const attemptToMerge = () => { mergeXDomain(specs, xValues, invalidXDomain); }; + expect(attemptToMerge).toThrowError('custom xDomain is invalid, min is greater than max'); + }); + + 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: Array> = + [{ seriesType: 'bar', xScaleType: ScaleType.Ordinal }]; + const basicMergedDomain = mergeXDomain(specs, xValues, xDomain); + expect(basicMergedDomain.domain).toEqual(['a', 'b', 'c']); + + const objectXDomain = { max: 10, min: 0 }; + const attemptToMerge = () => { mergeXDomain(specs, xValues, objectXDomain); }; + const errorMessage = 'xDomain for ordinal scale should be an array of values, not a DomainRange object'; + expect(attemptToMerge).toThrowError(errorMessage); + }); }); diff --git a/src/lib/series/domains/x_domain.ts b/src/lib/series/domains/x_domain.ts index 012258cd3c..6206833e2a 100644 --- a/src/lib/series/domains/x_domain.ts +++ b/src/lib/series/domains/x_domain.ts @@ -1,7 +1,7 @@ import { compareByValueAsc, identity } from '../../utils/commons'; -import { computeContinuousDataDomain, computeOrdinalDataDomain } from '../../utils/domain'; +import { computeContinuousDataDomain, computeOrdinalDataDomain, Domain } from '../../utils/domain'; import { ScaleType } from '../../utils/scales/scales'; -import { BasicSeriesSpec } from '../specs'; +import { BasicSeriesSpec, DomainRange } from '../specs'; import { BaseDomain } from './domain'; export type XDomain = BaseDomain & { @@ -16,8 +16,9 @@ export type XDomain = BaseDomain & { * Merge X domain value between a set of chart specification. */ export function mergeXDomain( - specs: Array>, + specs: Array>, xValues: Set, + xDomain?: DomainRange | Domain, ): XDomain { const mainXScaleType = convertXScaleTypes(specs); if (!mainXScaleType) { @@ -31,10 +32,28 @@ export function mergeXDomain( let minInterval = null; if (mainXScaleType.scaleType === ScaleType.Ordinal) { seriesXComputedDomains = computeOrdinalDataDomain(values, identity, false, true); + if (xDomain) { + if (Array.isArray(xDomain)) { + seriesXComputedDomains = xDomain; + } else { + throw new Error('xDomain for ordinal scale should be an array of values, not a DomainRange object'); + } + } } else { seriesXComputedDomains = computeContinuousDataDomain(values, identity, true); + if (xDomain) { + if (!Array.isArray(xDomain)) { + if (xDomain.min > xDomain.max) { + throw new Error('custom xDomain is invalid, min is greater than max'); + } + seriesXComputedDomains = [xDomain.min, xDomain.max]; + } else { + throw new Error('xDomain for continuous scale should be a DomainRange object, not an array'); + } + } minInterval = findMinInterval(values); } + return { type: 'xDomain', scaleType: mainXScaleType.scaleType, diff --git a/src/lib/series/domains/y_domain.test.ts b/src/lib/series/domains/y_domain.test.ts index 5e8b334e0e..25e8c84448 100644 --- a/src/lib/series/domains/y_domain.test.ts +++ b/src/lib/series/domains/y_domain.test.ts @@ -1,9 +1,15 @@ -import { getGroupId, getSpecId } from '../../utils/ids'; +import { getGroupId, getSpecId, GroupId } from '../../utils/ids'; import { ScaleType } from '../../utils/scales/scales'; import { RawDataSeries } from '../series'; -import { BasicSeriesSpec } from '../specs'; +import { BasicSeriesSpec, DomainRange } from '../specs'; import { BARCHART_1Y0G } from '../utils/test_dataset'; -import { mergeYDomain, splitSpecsByGroupId } from './y_domain'; +import { + coerceYScaleTypes, + getDataSeriesOnGroup, + mergeYDomain, + splitSpecsByGroupId, + YBasicSeriesSpec, +} from './y_domain'; describe('Y Domain', () => { test('Should merge Y domain', () => { @@ -32,7 +38,7 @@ describe('Y Domain', () => { stackAccessors: ['a'], yScaleToDataExtent: true, }, - ]); + ], new Map()); expect(mergedDomain).toEqual([ { type: 'yDomain', @@ -86,7 +92,7 @@ describe('Y Domain', () => { stackAccessors: ['a'], yScaleToDataExtent: true, }, - ]); + ], new Map()); expect(mergedDomain).toEqual([ { groupId: 'a', @@ -147,7 +153,7 @@ describe('Y Domain', () => { stackAccessors: ['a'], yScaleToDataExtent: true, }, - ]); + ], new Map()); expect(mergedDomain).toEqual([ { groupId: 'a', @@ -200,7 +206,7 @@ describe('Y Domain', () => { id: getSpecId('b'), yScaleToDataExtent: true, }, - ]); + ], new Map()); expect(mergedDomain).toEqual([ { groupId: 'a', @@ -254,7 +260,7 @@ describe('Y Domain', () => { id: getSpecId('b'), yScaleToDataExtent: true, }, - ]); + ], new Map()); expect(mergedDomain.length).toEqual(1); }); test('Should split specs by groupId, two groups, non stacked', () => { @@ -405,4 +411,82 @@ describe('Y Domain', () => { expect(groupValues[1].stacked).toEqual([spec3]); expect(groupValues[0].nonStacked).toEqual([]); }); + + test('Should return null for YScaleType when there are no specs', () => { + const specs: Array> = []; + expect(coerceYScaleTypes(specs)).toBe(null); + }); + + test('Should getDataSeriesOnGroup for matching specs', () => { + const dataSeries: RawDataSeries[] = [ + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y: 2 }, { x: 2, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 5 }], + }, + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y: 2 }, { x: 4, y: 7 }], + }, + ]; + const specDataSeries = new Map(); + specDataSeries.set(getSpecId('b'), dataSeries); + + const specs: YBasicSeriesSpec[] = [{ + seriesType: 'area', + yScaleType: ScaleType.Linear, + groupId: getGroupId('a'), + id: getSpecId('a'), + stackAccessors: ['a'], + yScaleToDataExtent: true, + }]; + + const rawDataSeries = getDataSeriesOnGroup(specDataSeries, specs); + expect(rawDataSeries).toEqual([]); + }); + test('Should merge Y domain accounting for custom domain limits', () => { + const groupId = getGroupId('a'); + + const dataSeries: RawDataSeries[] = [ + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y: 2 }, { x: 2, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 5 }], + }, + { + specId: getSpecId('a'), + key: [''], + seriesColorKey: '', + data: [{ x: 1, y: 2 }, { x: 4, y: 7 }], + }, + ]; + const specDataSeries = new Map(); + specDataSeries.set(getSpecId('a'), dataSeries); + const domainsByGroupId = new Map(); + domainsByGroupId.set(groupId, { min: 0, max: 20 }); + + const mergedDomain = mergeYDomain(specDataSeries, [ + { + seriesType: 'area', + yScaleType: ScaleType.Linear, + groupId, + id: getSpecId('a'), + stackAccessors: ['a'], + yScaleToDataExtent: true, + }, + ], domainsByGroupId); + expect(mergedDomain).toEqual([ + { + type: 'yDomain', + groupId, + domain: [0, 20], + scaleType: ScaleType.Linear, + isBandScale: false, + }, + ]); + }); }); diff --git a/src/lib/series/domains/y_domain.ts b/src/lib/series/domains/y_domain.ts index 89c9e1f2e6..cf98fd0711 100644 --- a/src/lib/series/domains/y_domain.ts +++ b/src/lib/series/domains/y_domain.ts @@ -4,7 +4,7 @@ import { computeContinuousDataDomain } from '../../utils/domain'; import { GroupId, SpecId } from '../../utils/ids'; import { ScaleContinuousType, ScaleType } from '../../utils/scales/scales'; import { RawDataSeries } from '../series'; -import { BasicSeriesSpec } from '../specs'; +import { BasicSeriesSpec, DomainRange } from '../specs'; import { BaseDomain } from './domain'; export type YDomain = BaseDomain & { @@ -18,7 +18,6 @@ export type YBasicSeriesSpec = Pick< | 'id' | 'seriesType' | 'yScaleType' - | 'yDomain' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' @@ -28,6 +27,7 @@ export type YBasicSeriesSpec = Pick< export function mergeYDomain( dataSeries: Map, specs: YBasicSeriesSpec[], + domainsByGroupId: Map, ): YDomain[] { // group specs by group ids const specsByGroupIds = splitSpecsByGroupId(specs); @@ -37,7 +37,7 @@ export function mergeYDomain( const yDomains = specsByGroupIdsEntries.map( ([groupId, groupSpecs]): YDomain => { - const groupYScaleType = coerchYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); + const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); if (groupYScaleType === null) { throw new Error(`Cannot merge ${groupId} domain. Missing Y scale types`); } @@ -66,12 +66,15 @@ export function mergeYDomain( isStackedScaleToExtent || isNonStackedScaleToExtent, ); + const customDomain = domainsByGroupId.get(groupId); + const domain = customDomain ? [customDomain.min, customDomain.max] : groupDomain; + return { type: 'yDomain', isBandScale: false, scaleType: groupYScaleType as ScaleContinuousType, groupId, - domain: groupDomain, + domain, }; }, ); @@ -79,7 +82,7 @@ export function mergeYDomain( return yDomains; } -function getDataSeriesOnGroup( +export function getDataSeriesOnGroup( dataSeries: Map, specs: YBasicSeriesSpec[], ): RawDataSeries[] { @@ -147,29 +150,30 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { } /** - * Coerch the scale types of a set of specification to a generic one. + * Coerce the scale types of a set of specification to a generic one. * If there is at least one bar series type, than the response will specity - * that the coerched scale is a `scaleBand` (each point needs to have a surrounding empty + * that the coerced scale is a `scaleBand` (each point needs to have a surrounding empty * space to draw the bar width). - * If there are multiple continuous scale types, is coerched to linear. - * If there are at least one Ordinal scale type, is coerched to ordinal. - * If none of the above, than coerch to the specified scale. + * If there are multiple continuous scale types, is coerced to linear. + * If there are at least one Ordinal scale type, is coerced to ordinal. + * If none of the above, than coerce to the specified scale. * @returns {ChartScaleType} */ -export function coerchYScaleTypes( +export function coerceYScaleTypes( specs: Array>, ): ScaleContinuousType | null { const scaleTypes = new Set(); specs.forEach((spec) => { scaleTypes.add(spec.yScaleType); }); - if (specs.length === 0 || scaleTypes.size === 0) { + + if (specs.length === 0) { return null; } - return coerchYScale(scaleTypes); + return coerceYScale(scaleTypes); } -function coerchYScale(scaleTypes: Set): ScaleContinuousType { +function coerceYScale(scaleTypes: Set): ScaleContinuousType { if (scaleTypes.size === 1) { const scales = scaleTypes.values(); const value = scales.next().value; diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index 0b231f712a..7ec411abc3 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -98,8 +98,17 @@ export function renderBars( specId: SpecId, seriesKey: any[], ): BarGeometry[] { - return dataset.map((datum, i) => { + const barGeometries: BarGeometry[] = []; + const xDomain = xScale.domain; + const xScaleType = xScale.type; + + dataset.forEach((datum, i) => { const { x, y0, y1 } = datum; + + if (xScaleType === ScaleType.Ordinal && !xDomain.includes(x)) { + return; + } + let height = 0; let y = 0; if (yScale.type === ScaleType.Log) { @@ -116,7 +125,7 @@ export function renderBars( height = yScale.scale(y0) - y; } - return { + const barGeometry = { x: xScale.scale(x) + xScale.bandwidth * orderIndex, y, // top most value width: xScale.bandwidth, @@ -132,7 +141,11 @@ export function renderBars( seriesKey, }, }; + + barGeometries.push(barGeometry); }); + + return barGeometries; } export function renderLine( diff --git a/src/lib/series/specs.ts b/src/lib/series/specs.ts index 34d281b5c8..9e86a68f71 100644 --- a/src/lib/series/specs.ts +++ b/src/lib/series/specs.ts @@ -1,6 +1,5 @@ import { GridLineConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; -import { Domain } from '../utils/domain'; import { AxisId, GroupId, SpecId } from '../utils/ids'; import { ScaleContinuousType, ScaleType } from '../utils/scales/scales'; import { CurveType } from './curves'; @@ -23,6 +22,11 @@ export interface GeomDatum { tooltipPosition: TooltipPosition; } +export interface DomainRange { + min: number; + max: number; +} + export interface SeriesSpec { /** The ID of the spec, generated via getSpecId method */ id: SpecId; @@ -32,10 +36,6 @@ export interface SeriesSpec { groupId: GroupId; /** An array of data */ data: Datum[]; - /** If specified, it constrant the x domain to these values */ - xDomain?: Domain; - /** If specified, it constrant the y Domain to these values */ - yDomain?: Domain; /** The type of series you are looking to render */ seriesType: 'bar' | 'line' | 'area' | 'basic'; /** Custom colors for series */ @@ -133,6 +133,8 @@ export interface AxisSpec { tickLabelRotation?: number; /** The axis title */ title?: string; + /** If specified, it constrains the domain for these values */ + domain?: DomainRange; } export type TickFormatter = (value: any) => string; diff --git a/src/lib/utils/dimensions.test.ts b/src/lib/utils/dimensions.test.ts index 137f80ca42..a235ef1fce 100644 --- a/src/lib/utils/dimensions.test.ts +++ b/src/lib/utils/dimensions.test.ts @@ -148,4 +148,47 @@ describe('Computed chart dimensions', () => { ); expect(chartDimensions).toMatchSnapshot(); }); + test('should not add space for axis when no spec for axis dimensions or axis is hidden', () => { + const axisDims = new Map(); + const axisSpecs = new Map(); + axisDims.set(getAxisId('foo'), axis1Dims); + axisSpecs.set(getAxisId('axis_1'), { + ...axisLeftSpec, + position: Position.Bottom, + }); + const chartDimensions = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisSpecs, + showLegend, + ); + + const expectedDims = { + height: 60, + width: 60, + left: 20, + top: 20, + }; + + expect(chartDimensions).toEqual(expectedDims); + + const hiddenAxisDims = new Map(); + const hiddenAxisSpecs = new Map(); + hiddenAxisDims.set(getAxisId('axis_1'), axis1Dims); + hiddenAxisSpecs.set(getAxisId('axis_1'), { + ...axisLeftSpec, + hide: true, + position: Position.Bottom, + }); + const hiddenAxisChartDimensions = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisSpecs, + showLegend, + ); + + expect(hiddenAxisChartDimensions).toEqual(expectedDims); + }); }); diff --git a/src/lib/utils/dimensions.ts b/src/lib/utils/dimensions.ts index e73bc2383e..c9fda2e3d5 100644 --- a/src/lib/utils/dimensions.ts +++ b/src/lib/utils/dimensions.ts @@ -44,7 +44,7 @@ export function computeChartDimensions( axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0 }, id) => { const axisSpec = axisSpecs.get(id); - if (!axisSpec) { + if (!axisSpec || axisSpec.hide) { return; } const { position, tickSize, tickPadding } = axisSpec; diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index fd0543c528..c982b44c88 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -1,8 +1,9 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; -import { Position, Rendering, Rotation } from '../lib/series/specs'; +import { DomainRange, Position, Rendering, Rotation } from '../lib/series/specs'; import { LIGHT_THEME } from '../lib/themes/light_theme'; import { Theme } from '../lib/themes/theme'; +import { Domain } from '../lib/utils/domain'; import { BrushEndListener, ChartStore, @@ -29,6 +30,7 @@ interface SettingSpecProps { onLegendItemClick?: LegendItemListener; onLegendItemPlusClick?: LegendItemListener; onLegendItemMinusClick?: LegendItemListener; + xDomain?: Domain | DomainRange; } function updateChartStore(props: SettingSpecProps) { @@ -50,6 +52,7 @@ function updateChartStore(props: SettingSpecProps) { onLegendItemMinusClick, onLegendItemPlusClick, debug, + xDomain, } = props; if (!chartStore) { return; @@ -62,6 +65,7 @@ function updateChartStore(props: SettingSpecProps) { chartStore.setShowLegend(showLegend); chartStore.legendPosition = legendPosition; + chartStore.xDomain = xDomain; if (onElementOver) { chartStore.setOnElementOverListener(onElementOver); diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 5ba087f6f7..d38f1afe89 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -5,6 +5,7 @@ import { AxisTicksDimensions, computeAxisTicksDimensions, getAxisTicksPositions, + mergeDomainsByGroupId, } from '../lib/axes/axis_utils'; import { CanvasTextBBoxCalculator } from '../lib/axes/canvas_text_bbox_calculator'; import { XDomain } from '../lib/series/domains/x_domain'; @@ -29,6 +30,7 @@ import { AxisSpec, BarSeriesSpec, BasicSeriesSpec, + DomainRange, LineSeriesSpec, Position, Rendering, @@ -38,6 +40,7 @@ import { formatTooltip } from '../lib/series/tooltip'; import { LIGHT_THEME } from '../lib/themes/light_theme'; import { Theme } from '../lib/themes/theme'; import { computeChartDimensions, Dimensions } from '../lib/utils/dimensions'; +import { Domain } from '../lib/utils/domain'; import { AxisId, GroupId, SpecId } from '../lib/utils/ids'; import { Scale, ScaleType } from '../lib/utils/scales/scales'; import { @@ -54,6 +57,7 @@ import { Transform, updateSelectedDataSeries, } from './utils'; + export interface TooltipPosition { top?: number; left?: number; @@ -128,6 +132,7 @@ export class ChartStore { seriesDomainsAndData?: SeriesDomainsAndData; // computed xScale?: Scale; yScales?: Map; + xDomain?: Domain | DomainRange; legendItems: LegendItem[] = []; highlightedLegendItemIndex: IObservableValue = observable.box(null); @@ -440,9 +445,16 @@ export class ChartStore { this.selectedDataSeries = null; } - // The second argument is optional; if not supplied, then all series will be factored into computations + const domainsByGroupId = mergeDomainsByGroupId(this.axesSpecs, this.chartRotation); + + // The last argument is optional; if not supplied, then all series will be factored into computations // Otherwise, selectedDataSeries is used to restrict the computation for just the selected series - const seriesDomains = computeSeriesDomains(this.seriesSpecs, this.selectedDataSeries); + const seriesDomains = computeSeriesDomains( + this.seriesSpecs, + domainsByGroupId, + this.xDomain, + this.selectedDataSeries, + ); this.seriesDomainsAndData = seriesDomains; // If this.selectedDataSeries is null, initialize with all series diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index b253316319..08ebaaada9 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -42,7 +42,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs); + const domains = computeSeriesDomains(specs, new Map(), undefined); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, @@ -98,7 +98,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs); + const domains = computeSeriesDomains(specs, new Map(), undefined); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, diff --git a/src/state/utils.ts b/src/state/utils.ts index 761e9bb1d7..aad3871c08 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -28,11 +28,13 @@ import { AreaSeriesSpec, AxisSpec, BasicSeriesSpec, + DomainRange, LineSeriesSpec, Rotation, } from '../lib/series/specs'; import { ColorConfig } from '../lib/themes/theme'; import { Dimensions } from '../lib/utils/dimensions'; +import { Domain } from '../lib/utils/domain'; import { AxisId, GroupId, SpecId } from '../lib/utils/ids'; import { Scale } from '../lib/utils/scales/scales'; @@ -106,6 +108,8 @@ export function getUpdatedCustomSeriesColors(seriesSpecs: Map, + domainsByGroupId: Map, + customXDomain?: DomainRange | Domain, selectedDataSeries?: DataSeriesColorsValues[] | null, ): { xDomain: XDomain; @@ -122,8 +126,10 @@ export function computeSeriesDomains( // console.log({ splittedSeries, xValues, seriesColors }); const splittedDataSeries = [...splittedSeries.values()]; const specsArray = [...seriesSpecs.values()]; - const xDomain = mergeXDomain(specsArray, xValues); - const yDomain = mergeYDomain(splittedSeries, specsArray); + + const xDomain = mergeXDomain(specsArray, xValues, customXDomain); + const yDomain = mergeYDomain(splittedSeries, specsArray, domainsByGroupId); + const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries); // tslint:disable-next-line:no-console // console.log({ formattedDataSeries, xDomain, yDomain }); diff --git a/stories/axis.tsx b/stories/axis.tsx index a2199e49f2..b664770f66 100644 --- a/stories/axis.tsx +++ b/stories/axis.tsx @@ -1,4 +1,4 @@ -import { boolean, number } from '@storybook/addon-knobs'; +import { array, boolean, number } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { @@ -154,24 +154,28 @@ storiesOf('Axis', module) position={Position.Bottom} title={'bottom'} showOverlappingTicks={true} + hide={boolean('hide botttom axis', false)} /> Number(d).toFixed(2)} + hide={boolean('hide left axis', false)} /> Number(d).toFixed(2)} + hide={boolean('hide right axis', false)} /> ); + }) + .add('customizing domain limits [mixed linear chart]', () => { + const leftDomain = { + min: number('left min', 0), + max: number('left max', 7), + }; + + const rightDomain1 = { + min: number('right1 min', 0), + max: number('right1 max', 10), + }; + + const rightDomain2 = { + min: number('right2 min', 0), + max: number('right2 max', 10), + }; + + const xDomain = { + min: number('xDomain min', 0), + max: number('xDomain max', 3), + }; + + return ( + + + + Number(d).toFixed(2)} + domain={leftDomain} + hide={boolean('hide left axis', false)} + /> + Number(d).toFixed(2)} + domain={rightDomain1} + /> + Number(d).toFixed(2)} + domain={rightDomain2} + /> + + + + ); + }) + .add('customizing domain limits [mixed ordinal & linear x domain]', () => { + const leftDomain = { + min: number('left min', 0), + max: number('left max', 7), + }; + + const right1Domain = { + min: number('right1 min', 0), + max: number('right1 max', 10), + }; + + const xDomain = array('xDomain', ['a', 'b', 'c', 'd', 0, 1, 2, 3]); + + return ( + + + + Number(d).toFixed(2)} + domain={leftDomain} + /> + Number(d).toFixed(2)} + domain={right1Domain} + /> + Number(d).toFixed(2)} + /> + + {/* */} + + + ); });