Skip to content

Commit df130cd

Browse files
committed
feat: percent stack strategy
1 parent fc6656f commit df130cd

File tree

8 files changed

+680
-23
lines changed

8 files changed

+680
-23
lines changed

src/component/tooltip/seriesFormatTooltip.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
TooltipMarkupSection
2828
} from './tooltipMarkup';
2929
import { retrieveRawValue } from '../../data/helper/dataProvider';
30-
import { isNameSpecified } from '../../util/model';
30+
import { isNameSpecified, getStackStrategy } from '../../util/model';
3131

3232

3333
export function defaultSeriesFormatTooltip(opt: {
@@ -65,6 +65,20 @@ export function defaultSeriesFormatTooltip(opt: {
6565
const dimInfo = data.getDimensionInfo(tooltipDims[0]);
6666
sortParam = inlineValue = retrieveRawValue(data, dataIndex, tooltipDims[0]);
6767
inlineValueType = dimInfo.type;
68+
const stackStrategy = getStackStrategy(series);
69+
if (stackStrategy === 'percent') {
70+
// Append the normalized value (as a percent of the total stack) when 'percent' stackStrategy is used.
71+
const stackResultDim = data.getCalculationInfo('stackResultDimension');
72+
const stackedOverDim = data.getCalculationInfo('stackedOverDimension');
73+
const stackTop = data.get(stackResultDim, dataIndex) as number;
74+
const stackBottom = data.get(stackedOverDim, dataIndex) as number;
75+
// Difference between the cumulative sum including this series and the cumulative sum before
76+
// this series gives its individual contribution.
77+
if (!isNaN(stackTop) && !isNaN(stackBottom)) {
78+
const percentVal = stackTop - stackBottom;
79+
inlineValue = `${inlineValue} (${percentVal.toFixed(1)}%)`;
80+
}
81+
}
6882
}
6983
else {
7084
sortParam = inlineValue = isValueArr ? value[0] : value;

src/layout/barGrid.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -482,14 +482,20 @@ export function createProgressiveLayout(seriesType: string): StageHandler {
482482
const baseDimIdx = data.getDimensionIndex(data.mapDimension(baseAxis.dim));
483483
const drawBackground = seriesModel.get('showBackground', true);
484484
const valueDim = data.mapDimension(valueAxis.dim);
485-
const stackResultDim = data.getCalculationInfo('stackResultDimension');
486-
const stacked = isDimensionStacked(data, valueDim) && !!data.getCalculationInfo('stackedOnSeries');
487485
const isValueAxisH = valueAxis.isHorizontal();
488486
const valueAxisStart = getValueAxisStart(baseAxis, valueAxis);
489487
const isLarge = isInLargeMode(seriesModel);
490488
const barMinHeight = seriesModel.get('barMinHeight') || 0;
491489

490+
// Determine stacked dimensions and account for stackStrategy.
491+
const stackResultDim = data.getCalculationInfo('stackResultDimension');
492492
const stackedDimIdx = stackResultDim && data.getDimensionIndex(stackResultDim);
493+
const stackedOverDim = data.getCalculationInfo('stackedOverDimension');
494+
const stackedOverDimIdx = stackedOverDim && data.getDimensionIndex(stackedOverDim);
495+
const stackStrategy = seriesModel.get('stackStrategy');
496+
const isPercentStack = stackStrategy === 'percent';
497+
const stacked = isPercentStack
498+
|| (isDimensionStacked(data, valueDim) && !!data.getCalculationInfo('stackedOnSeries'));
493499

494500
// Layout info.
495501
const columnWidth = data.getLayout('size');
@@ -518,7 +524,12 @@ export function createProgressiveLayout(seriesType: string): StageHandler {
518524
// Because of the barMinHeight, we can not use the value in
519525
// stackResultDimension directly.
520526
if (stacked) {
521-
stackStartValue = +value - (store.get(valueDimIdx, dataIndex) as number);
527+
if (isPercentStack) {
528+
stackStartValue = store.get(stackedOverDimIdx, dataIndex);
529+
}
530+
else {
531+
stackStartValue = +value - (store.get(valueDimIdx, dataIndex) as number);
532+
}
522533
}
523534

524535
let x;

src/processor/dataStack.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,9 @@
2020
import {createHashMap, each} from 'zrender/src/core/util';
2121
import GlobalModel from '../model/Global';
2222
import SeriesModel from '../model/Series';
23-
import { SeriesOption, SeriesStackOptionMixin } from '../util/types';
24-
import SeriesData, { DataCalculationInfo } from '../data/SeriesData';
23+
import { SeriesOption, SeriesStackOptionMixin, StackInfo } from '../util/types';
2524
import { addSafe } from '../util/number';
26-
27-
type StackInfo = Pick<
28-
DataCalculationInfo<SeriesOption & SeriesStackOptionMixin>,
29-
'stackedDimension'
30-
| 'isStackedByIndex'
31-
| 'stackedByDimension'
32-
| 'stackResultDimension'
33-
| 'stackedOverDimension'
34-
> & {
35-
data: SeriesData
36-
seriesModel: SeriesModel<SeriesOption & SeriesStackOptionMixin>
37-
};
25+
import { calculatePercentStack } from '../util/stack';
3826

3927
// (1) [Caution]: the logic is correct based on the premises:
4028
// data processing stage is blocked in stream.
@@ -77,7 +65,15 @@ export default function dataStack(ecModel: GlobalModel) {
7765
}
7866
});
7967

80-
stackInfoMap.each(calculateStack);
68+
stackInfoMap.each(function (stackInfoList) {
69+
const isPercentStack = stackInfoList.some((info) => info.seriesModel.get('stackStrategy') === 'percent');
70+
if (isPercentStack) {
71+
calculatePercentStack(stackInfoList);
72+
}
73+
else {
74+
calculateStack(stackInfoList);
75+
}
76+
});
8177
}
8278

8379
function calculateStack(stackInfoList: StackInfo[]) {

src/util/model.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import {
4545
Payload,
4646
OptionId,
4747
OptionName,
48-
InterpolatableValue
48+
InterpolatableValue,
49+
SeriesStackOptionMixin
4950
} from './types';
5051
import { Dictionary } from 'zrender/src/core/types';
5152
import SeriesModel from '../model/Series';
@@ -1094,3 +1095,7 @@ export function interpolateRawValues(
10941095
return interpolated;
10951096
}
10961097
}
1098+
1099+
export function getStackStrategy(series: SeriesModel): SeriesStackOptionMixin['stackStrategy'] | undefined {
1100+
return (series.get as (path: string) => unknown)('stackStrategy') as SeriesStackOptionMixin['stackStrategy'];
1101+
}

src/util/stack.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { each } from 'zrender/src/core/util';
21+
import { addSafe } from './number';
22+
import { StackInfo } from './types';
23+
24+
/**
25+
* Percent stackStrategy logic to normalize each value as a percentage of the total per index.
26+
*/
27+
export function calculatePercentStack(stackInfoList: StackInfo[]) {
28+
const dataLength = stackInfoList[0].data.count();
29+
if (dataLength === 0) {
30+
return;
31+
}
32+
33+
// Calculate totals per data index across all series in the stack group.
34+
const totals = calculateStackTotals(stackInfoList, dataLength);
35+
36+
// Used to track running total of percent values at each index.
37+
const cumulativePercents = new Float64Array(dataLength);
38+
39+
const resultNaN = [NaN, NaN];
40+
41+
each(stackInfoList, function (targetStackInfo) {
42+
const resultVal: number[] = [];
43+
const dims: [string, string] = [targetStackInfo.stackResultDimension, targetStackInfo.stackedOverDimension];
44+
const targetData = targetStackInfo.data;
45+
const stackedDim = targetStackInfo.stackedDimension;
46+
47+
// Should not write on raw data, because stack series model list changes
48+
// depending on legend selection.
49+
targetData.modify(dims, function (v0, v1, dataIndex) {
50+
const rawValue = targetData.get(stackedDim, dataIndex) as number;
51+
52+
// Consider `connectNulls` of line area, if value is NaN, stackedOver
53+
// should also be NaN, to draw a appropriate belt area.
54+
if (isNaN(rawValue)) {
55+
return resultNaN;
56+
}
57+
58+
// Pre-calculated total for this specific data index.
59+
const total = totals[dataIndex];
60+
61+
// Percentage contribution of this segment.
62+
const percent = total === 0 ? 0 : (rawValue / total) * 100;
63+
64+
// Bottom edge of this segment (cumulative % before this series).
65+
const stackedOver = cumulativePercents[dataIndex];
66+
67+
// Update the cumulative percentage for the next series at this index to use.
68+
cumulativePercents[dataIndex] = addSafe(stackedOver, percent);
69+
70+
// Result: [Top edge %, Bottom edge %]
71+
resultVal[0] = cumulativePercents[dataIndex];
72+
resultVal[1] = stackedOver;
73+
return resultVal;
74+
});
75+
});
76+
}
77+
78+
/**
79+
* Helper to calculate the total value across all series for each data index.
80+
*/
81+
function calculateStackTotals(stackInfoList: StackInfo[], dataLength: number): number[] {
82+
const totals = Array(dataLength).fill(0);
83+
each(stackInfoList, (stackInfo) => {
84+
const data = stackInfo.data;
85+
const dim = stackInfo.stackedDimension;
86+
for (let i = 0; i < dataLength; i++) {
87+
const val = data.get(dim, i) as number;
88+
if (!isNaN(val)) {
89+
totals[i] = addSafe(totals[i], val);
90+
}
91+
}
92+
});
93+
return totals;
94+
}

src/util/types.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import ExtensionAPI from '../core/ExtensionAPI';
3333
import SeriesModel from '../model/Series';
3434
import { createHashMap, HashMap } from 'zrender/src/core/util';
3535
import { TaskPlanCallbackReturn, TaskProgressParams } from '../core/task';
36-
import SeriesData from '../data/SeriesData';
36+
import SeriesData, { DataCalculationInfo } from '../data/SeriesData';
3737
import { Dictionary, ElementEventName, ImageLike, TextAlign, TextVerticalAlign } from 'zrender/src/core/types';
3838
import { PatternObject } from 'zrender/src/graphic/Pattern';
3939
import { TooltipMarker } from './format';
@@ -1678,8 +1678,20 @@ export interface SeriesLargeOptionMixin {
16781678
}
16791679
export interface SeriesStackOptionMixin {
16801680
stack?: string
1681-
stackStrategy?: 'samesign' | 'all' | 'positive' | 'negative';
1682-
}
1681+
stackStrategy?: 'samesign' | 'all' | 'positive' | 'negative' | 'percent';
1682+
}
1683+
1684+
export type StackInfo = Pick<
1685+
DataCalculationInfo<SeriesOption & SeriesStackOptionMixin>,
1686+
'stackedDimension'
1687+
| 'isStackedByIndex'
1688+
| 'stackedByDimension'
1689+
| 'stackResultDimension'
1690+
| 'stackedOverDimension'
1691+
> & {
1692+
data: SeriesData
1693+
seriesModel: SeriesModel<SeriesOption & SeriesStackOptionMixin>
1694+
};
16831695

16841696
type SamplingFunc = (frame: ArrayLike<number>) => number;
16851697

0 commit comments

Comments
 (0)