Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(series): stack series in percentage mode #250

Merged
merged 9 commits into from
Jul 5, 2019
105 changes: 105 additions & 0 deletions src/lib/series/domains/y_domain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,4 +691,109 @@ describe('Y Domain', () => {
const errorMessage = 'custom yDomain for a is invalid, computed min is greater than custom max';
expect(attemptToMerge).toThrowError(errorMessage);
});
test('Should merge Y domain with stacked as percentage', () => {
const dataSeries1: RawDataSeries[] = [
{
specId: getSpecId('a'),
key: [''],
seriesColorKey: '',
data: [{ x: 1, y1: 2 }, { x: 2, y1: 2 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }],
},
{
specId: getSpecId('a'),
key: [''],
seriesColorKey: '',
data: [{ x: 1, y1: 2 }, { x: 4, y1: 7 }],
},
];
const dataSeries2: RawDataSeries[] = [
{
specId: getSpecId('a'),
key: [''],
seriesColorKey: '',
data: [{ x: 1, y1: 10 }, { x: 2, y1: 10 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }],
},
];
const specDataSeries = new Map();
specDataSeries.set(getSpecId('a'), dataSeries1);
specDataSeries.set(getSpecId('b'), dataSeries2);
const mergedDomain = mergeYDomain(
specDataSeries,
[
{
seriesType: 'area',
yScaleType: ScaleType.Linear,
groupId: getGroupId('a'),
id: getSpecId('a'),
stackAccessors: ['a'],
yScaleToDataExtent: true,
stackAsPercentage: true,
},
{
seriesType: 'area',
yScaleType: ScaleType.Log,
groupId: getGroupId('a'),
id: getSpecId('b'),
yScaleToDataExtent: true,
},
],
new Map(),
);
expect(mergedDomain).toEqual([
{
groupId: 'a',
domain: [0, 1],
scaleType: ScaleType.Linear,
isBandScale: false,
type: 'yDomain',
},
]);
});
test('Should merge Y domain with as percentage regadless of custom domains', () => {
const groupId = getGroupId('a');

const dataSeries: RawDataSeries[] = [
{
specId: getSpecId('a'),
key: [''],
seriesColorKey: '',
data: [{ x: 1, y1: 2 }, { x: 2, y1: 2 }, { x: 3, y1: 2 }, { x: 4, y1: 5 }],
},
{
specId: getSpecId('a'),
key: [''],
seriesColorKey: '',
data: [{ x: 1, y1: 2 }, { x: 4, y1: 7 }],
},
];
const specDataSeries = new Map();
specDataSeries.set(getSpecId('a'), dataSeries);
const domainsByGroupId = new Map<GroupId, DomainRange>();
domainsByGroupId.set(groupId, { min: 2, max: 20 });

const mergedDomain = mergeYDomain(
specDataSeries,
[
{
seriesType: 'area',
yScaleType: ScaleType.Linear,
groupId,
id: getSpecId('a'),
stackAccessors: ['a'],
yScaleToDataExtent: true,
stackAsPercentage: true,
},
],
domainsByGroupId,
);
expect(mergedDomain).toEqual([
{
type: 'yDomain',
groupId,
domain: [0, 1],
scaleType: ScaleType.Linear,
isBandScale: false,
},
]);
});
});
99 changes: 55 additions & 44 deletions src/lib/series/domains/y_domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type YDomain = BaseDomain & {
export type YBasicSeriesSpec = Pick<
BasicSeriesSpec,
'id' | 'seriesType' | 'yScaleType' | 'groupId' | 'stackAccessors' | 'yScaleToDataExtent' | 'colorAccessors'
>;
> & { stackAsPercentage?: boolean };

export function mergeYDomain(
dataSeries: Map<SpecId, RawDataSeries[]>,
Expand All @@ -32,50 +32,54 @@ export function mergeYDomain(
const yDomains = specsByGroupIdsEntries.map(
([groupId, groupSpecs]): YDomain => {
const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]);

// compute stacked domain
const isStackedScaleToExtent = groupSpecs.stacked.some((spec) => {
return spec.yScaleToDataExtent;
});
const stackedDataSeries = getDataSeriesOnGroup(dataSeries, groupSpecs.stacked);
const stackedDomain = computeYStackedDomain(stackedDataSeries, isStackedScaleToExtent);

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

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

const [computedDomainMin, computedDomainMax] = groupDomain;
let domain = groupDomain;

const customDomain = domainsByGroupId.get(groupId);

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

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

let domain: number[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the Domain type here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no. On the Y Axis we, currently can handle only numeric values, so the domain here is number[], or better: it should be [number, number]. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh yes, of course. Yes I agree [number, number]; maybe in the future we can consider how to tighten the Domain type so that we can between distinguish between types of domains so it is clearer.

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

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

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

const [computedDomainMin, computedDomainMax] = domain;

const customDomain = domainsByGroupId.get(groupId);

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

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

domain = [computedDomainMin, customDomain.max];
}

domain = [computedDomainMin, customDomain.max];
}

return {
type: 'yDomain',
isBandScale: false,
Expand Down Expand Up @@ -139,10 +143,14 @@ function computeYNonStackedDomain(dataseries: RawDataSeries[], scaleToExtent: bo
return computeContinuousDataDomain([...yValues.values()], identity, scaleToExtent);
}
export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) {
const specsByGroupIds = new Map<GroupId, { stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] }>();
const specsByGroupIds = new Map<
GroupId,
{ isPercentageStack: boolean; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] }
>();
// split each specs by groupId and by stacked or not
specs.forEach((spec) => {
const group = specsByGroupIds.get(spec.groupId) || {
isPercentageStack: false,
stacked: [],
nonStacked: [],
};
Expand All @@ -151,6 +159,9 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) {
} else {
group.nonStacked.push(spec);
}
if (spec.stackAsPercentage === true) {
group.isPercentageStack = true;
}
specsByGroupIds.set(spec.groupId, group);
});
return specsByGroupIds;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/series/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function computeLegend(
isLegendItemVisible: !hideInLegend,
displayValue: {
raw: series.lastValue,
formatted: formatter(series.lastValue),
formatted: isSeriesVisible ? formatter(series.lastValue) : undefined,
},
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/lib/series/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,18 @@ export function getFormattedDataseries(
}[] = [];

specsByGroupIdsEntries.forEach(([groupId, groupSpecs]) => {
const { isPercentageStack } = groupSpecs;
// format stacked data series
const stackedDataSeries = getRawDataSeries(groupSpecs.stacked, dataSeries);
const stackedDataSeriesValues = formatStackedDataSeriesValues(
stackedDataSeries.rawDataSeries,
false,
isPercentageStack,
);
stackedFormattedDataSeries.push({
groupId,
counts: stackedDataSeries.counts,
dataSeries: formatStackedDataSeriesValues(stackedDataSeries.rawDataSeries, false),
dataSeries: stackedDataSeriesValues,
});

// format non stacked data series
Expand Down
8 changes: 8 additions & 0 deletions src/lib/series/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export type BarSeriesSpec = BasicSeriesSpec & {
/** If true, will stack all BarSeries and align bars to ticks (instead of centered on ticks) */
enableHistogramMode?: boolean;
barSeriesStyle?: CustomBarSeriesStyle;
/**
* Stack each series in percentage for each point.
*/
stackAsPercentage?: boolean;
};

/**
Expand Down Expand Up @@ -167,6 +171,10 @@ export type AreaSeriesSpec = BasicSeriesSpec &
/** The type of interpolator to be used to interpolate values between points */
curve?: CurveType;
areaSeriesStyle?: AreaSeriesStyle;
/**
* Stack each series in percentage for each point.
*/
stackAsPercentage?: boolean;
};

interface HistogramConfig {
Expand Down
Loading