Skip to content

Commit

Permalink
feat(axis): allow pixel domain padding for y axes (#1145)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickofthyme authored Jun 4, 2021
1 parent 71474a5 commit 7c1fa8e
Show file tree
Hide file tree
Showing 24 changed files with 399 additions and 50 deletions.
13 changes: 12 additions & 1 deletion api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,16 @@ export type DisplayValueStyle = Omit<TextStyle, 'fill' | 'fontSize'> & {
};
};

// @public
export const DomainPaddingUnit: Readonly<{
Domain: "domain";
Pixel: "pixel";
DomainRatio: "domainRatio";
}>;

// @public
export type DomainPaddingUnit = $Values<typeof DomainPaddingUnit>;

// @public (undocumented)
export type DomainRange = LowerBoundedDomain | UpperBoundedDomain | CompleteBoundedDomain | UnboundedDomainWithInterval;

Expand Down Expand Up @@ -2263,7 +2273,8 @@ export interface XYChartSeriesIdentifier extends SeriesIdentifier {
export interface YDomainBase {
constrainPadding?: boolean;
fit?: boolean;
padding?: number | string;
padding?: number;
paddingUnit?: DomainPaddingUnit;
}

// @public (undocumented)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions integration/tests/bar_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,52 @@ describe('Bar series stories', () => {
);
});
});

describe('domain padding', () => {
it('should allow domain space padding', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-yScaleDataToExtent=&knob-fit Y domain to data=true&knob-constrain padding=true&knob-domain padding=5&knob-Domain padding unit=domain&knob-data=all negative&knob-console log domains=true&knob-nice ticks=',
);
});
it('should allow pixel space padding', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-yScaleDataToExtent=&knob-fit Y domain to data=true&knob-constrain padding=true&knob-domain padding=100&knob-Domain padding unit=pixel&knob-data=all negative&knob-console log domains=true&knob-nice ticks=',
);
});
it('should apply padding with positive and negative values', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-fit Y domain to data=true&knob-constrain padding=true&knob-nice ticks=&knob-domain padding=50&knob-Domain padding unit=pixel&knob-data=mixed&knob-SeriesType=bar&knob-console log domains=true',
);
});
it('should apply padding within intersept - positive values', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-fit Y domain to data=true&knob-constrain padding=true&knob-nice ticks=&knob-domain padding=50&knob-Domain padding unit=pixel&knob-data=all positive&knob-SeriesType=line&knob-console log domains=true',
);
});
it('should constrain padding to intersept - positive values', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-fit Y domain to data=true&knob-constrain padding=true&knob-nice ticks=&knob-domain padding=100&knob-Domain padding unit=pixel&knob-data=all positive&knob-SeriesType=line&knob-console log domains=true',
);
});
it('should apply padding within intersept - negative values', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-fit Y domain to data=true&knob-constrain padding=true&knob-nice ticks=&knob-domain padding=50&knob-Domain padding unit=pixel&knob-data=all negative&knob-SeriesType=line&knob-console log domains=true',
);
});
it('should constrain padding to intersept - negative values', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-fit Y domain to data=true&knob-constrain padding=true&knob-nice ticks=&knob-domain padding=100&knob-Domain padding unit=pixel&knob-data=all negative&knob-SeriesType=line&knob-console log domains=true',
);
});
it('should allow domain ratio padding', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-yScaleDataToExtent=&knob-fit Y domain to data=true&knob-constrain padding=true&knob-domain padding=0.5&knob-Domain padding unit=domainRatio&knob-data=all negative&knob-console log domains=true&knob-nice ticks=',
);
});
it('should nice ticks after domain padding is applied', async () => {
await common.expectChartAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/bar-chart--scale-to-extent&knob-yScaleDataToExtent=&knob-fit Y domain to data=true&knob-constrain padding=&knob-domain padding=100&knob-Domain padding unit=pixel&knob-data=all negative&knob-console log domains=true&knob-nice ticks=true',
);
});
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"test:integration:local": "LOCAL_VRT_SERVER=true PORT=9001 yarn test:integration",
"test:integration:debug": "DEBUG=true yarn test:integration:local",
"test:integration:legacy": "LEGACY_VRT_SERVER=true yarn test:integration",
"test:integration:local:legacy": "LOCAL_VRT_SERVER=true PORT=9002 test:integration:legacy",
"test:integration:local:legacy": "LOCAL_VRT_SERVER=true PORT=9002 yarn test:integration:legacy",
"test:integration:generate": "yarn test:integration:generate:examples && yarn test:integration:generate:page",
"test:integration:generate:examples": "./scripts/extract_examples.sh",
"test:integration:generate:page": "./scripts/compile_vrt_page.sh",
Expand Down
2 changes: 2 additions & 0 deletions src/chart_types/xy_chart/domains/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ export type YDomain = LogScaleOptions & {
groupId: GroupId;
domain: ContinuousDomain;
desiredTickCount: number;
domainPixelPadding?: number;
constrainDomainPadding?: boolean;
};
7 changes: 4 additions & 3 deletions src/chart_types/xy_chart/domains/y_domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ 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 { BasicSeriesSpec, YDomainRange, SeriesType, StackMode, DomainPaddingUnit } from '../utils/specs';
import { areAllNiceDomain } from './nice';
import { YDomain } from './types';

Expand Down Expand Up @@ -70,13 +70,12 @@ function mergeYDomainForGroup(
const [{ stackMode, spec }] = dataSeries;
const groupId = getSpecDomainGroupId(spec);
const { customDomain, type, nice, desiredTickCount } = yScaleConfig[groupId];
const newCustomDomain = customDomain ? { ...customDomain } : {};

let domain: ContinuousDomain;
if (stackMode === StackMode.Percentage) {
domain = computeContinuousDataDomain([0, 1], identity, type, customDomain);
} else {
const newCustomDomain = customDomain ? { ...customDomain } : {};

// compute stacked domain
const stackedDomain = computeYDomain(stacked, hasZeroBaselineSpecs, type, newCustomDomain);

Expand Down Expand Up @@ -116,6 +115,8 @@ function mergeYDomainForGroup(
logBase: customDomain?.logBase,
logMinLimit: customDomain?.logMinLimit,
desiredTickCount,
domainPixelPadding: newCustomDomain.paddingUnit === DomainPaddingUnit.Pixel ? newCustomDomain.padding : 0,
constrainDomainPadding: newCustomDomain.constrainPadding,
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/chart_types/xy_chart/scales/scale_defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ export const Y_SCALE_DEFAULT = {
type: ScaleType.Linear,
nice: false,
desiredTickCount: 10,
constrainDomainPadding: undefined,
domainPixelPadding: 0,
logBase: undefined,
logMinLimit: undefined,
};
40 changes: 29 additions & 11 deletions src/chart_types/xy_chart/utils/scales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,40 @@ interface YScaleOptions {
*/
export function computeYScales(options: YScaleOptions): Map<GroupId, Scale> {
const { yDomains, range, integersOnly } = options;
return yDomains.reduce((yScales, { type, nice, desiredTickCount, domain, groupId, logBase, logMinLimit }) => {
const yScale = new ScaleContinuous(
return yDomains.reduce(
(
yScales,
{
type,
domain,
range,
nice,
},
{
desiredTickCount,
integersOnly,
domain,
groupId,
logBase,
logMinLimit,
domainPixelPadding,
constrainDomainPadding,
},
);
yScales.set(groupId, yScale);
return yScales;
}, new Map<GroupId, Scale>());
) => {
const yScale = new ScaleContinuous(
{
type,
domain,
range,
nice,
},
{
desiredTickCount,
integersOnly,
logBase,
logMinLimit,
domainPixelPadding,
constrainDomainPadding,
},
);
yScales.set(groupId, yScale);
return yScales;
},
new Map<GroupId, Scale>(),
);
}
48 changes: 46 additions & 2 deletions src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,43 @@ interface DomainBase {
minInterval?: number;
}

/**
* Padding unit for domain
* @public
*/
export const DomainPaddingUnit = Object.freeze({
/**
* Raw value in the domain space.
*
* Example:
*
* If your domain is `[20, 40]` and your padding value is `10`.
* The resulting domain would be `[10, 50]`
*/
Domain: 'domain' as const,
/**
* Spatial pixel value (aka screenspace) not dependent on domain.
*
* @alpha
*/
Pixel: 'pixel' as const,
/**
* Ratio of total domain relative to domain space
*
* Example:
*
* If your domain is `[20, 40]` and your padding value is `0.1`.
* The resulting padding would be 2 (i.e. `0.1 * (40 - 20)`)
* resulting in a domain of `[18, 42]`
*/
DomainRatio: 'domainRatio' as const,
});
/**
* Padding unit
* @public
*/
export type DomainPaddingUnit = $Values<typeof DomainPaddingUnit>;

/**
* Domain option that **only** apply to `yDomains`.
* @public
Expand All @@ -293,11 +330,18 @@ export interface YDomainBase {
*/
fit?: boolean;
/**
* Padding for computed domain. Positive pixel number or percent string.
* Padding for computed domain as positive number.
* Applied to domain __before__ nicing
*
* Setting `max` or `min` will override this functionality.
*/
padding?: number | string;
padding?: number;
/**
* Unit of padding dimension
*
* @defaultValue 'domain'
*/
paddingUnit?: DomainPaddingUnit;
/**
* Constrains padded domain to the zero baseline.
*
Expand Down
53 changes: 53 additions & 0 deletions src/scales/scale_continuous.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,59 @@ describe('Scale Continuous', () => {
});
});
});

describe('Domain pixel padding', () => {
const scaleOptions = Object.freeze({
type: ScaleType.Linear,
range: [0, 100] as Range,
domain: [10, 60],
});

it('should add pixel padding to domain', () => {
const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 10 });
expect(scale.domain).toEqual([3.75, 66.25]);
});

it('should handle inverted domain pixel padding', () => {
const scale = new ScaleContinuous({ ...scaleOptions, domain: [60, 10] }, { domainPixelPadding: 10 });
expect(scale.domain).toEqual([66.25, 3.75]);
});

it('should handle negative domain pixel padding', () => {
const scale = new ScaleContinuous({ ...scaleOptions, domain: [-60, -20] }, { domainPixelPadding: 10 });
expect(scale.domain).toEqual([-65, -15]);
});

it('should handle negative inverted domain pixel padding', () => {
const scale = new ScaleContinuous({ ...scaleOptions, domain: [-20, -60] }, { domainPixelPadding: 10 });
expect(scale.domain).toEqual([-15, -65]);
});

it('should constrain pixel padding to zero', () => {
const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 20 });
expect(scale.domain).toEqual([0, 75]);
});

it('should not constrain pixel padding to zero', () => {
const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: 18, constrainDomainPadding: false });
expect(scale.domain).toEqual([-4.0625, 74.0625]);
});

it('should nice domain after pixel padding is applied', () => {
const scale = new ScaleContinuous(
{ ...scaleOptions, nice: true },
{ domainPixelPadding: 18, constrainDomainPadding: false },
);
expect(scale.domain).toEqual([-10, 80]);
});

it('should not handle pixel padding when pixel is greater than half the total range', () => {
const criticalPadding = Math.abs(scaleOptions.range[0] - scaleOptions.range[1]) / 2;
const scale = new ScaleContinuous(scaleOptions, { domainPixelPadding: criticalPadding });
expect(scale.domain).toEqual(scaleOptions.domain);
});
});

describe('ticks as integers or floats', () => {
const domain: ContinuousDomain = [0, 7];
const minRange = 0;
Expand Down
Loading

0 comments on commit 7c1fa8e

Please sign in to comment.