Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/component/tooltip/seriesFormatTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ export function defaultSeriesFormatTooltip(opt: {
const dimInfo = data.getDimensionInfo(tooltipDims[0]);
sortParam = inlineValue = retrieveRawValue(data, dataIndex, tooltipDims[0]);
inlineValueType = dimInfo.type;
const isPercentStackEnabled = data.getCalculationInfo('isPercentStackEnabled');
if (isPercentStackEnabled) {
// Append the normalized value (as a percent of the total stack) when stackPercent is true.
const params = series.getDataParams(dataIndex);
if (params.percent != null) {
inlineValue = `${inlineValue} (${params.percent}%)`;
}
}
}
else {
sortParam = inlineValue = isValueArr ? value[0] : value;
Expand Down
1 change: 1 addition & 0 deletions src/data/SeriesData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export interface DataCalculationInfo<SERIES_MODEL> {
stackedOverDimension: DimensionName;
stackResultDimension: DimensionName;
stackedOnSeries?: SERIES_MODEL;
isPercentStackEnabled?: boolean;
}

// -----------------------------
Expand Down
4 changes: 3 additions & 1 deletion src/data/helper/dataStackHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function enableDataStack(
| 'isStackedByIndex'
| 'stackedOverDimension'
| 'stackResultDimension'
| 'isPercentStackEnabled'
> {
opt = opt || {};
let byIndex = opt.byIndex;
Expand Down Expand Up @@ -192,7 +193,8 @@ export function enableDataStack(
stackedByDimension: stackedByDimInfo && stackedByDimInfo.name,
isStackedByIndex: byIndex,
stackedOverDimension: stackedOverDimension,
stackResultDimension: stackResultDimension
stackResultDimension: stackResultDimension,
isPercentStackEnabled: seriesModel.get('stackPercent'),
};
}

Expand Down
20 changes: 17 additions & 3 deletions src/layout/barGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,19 @@ export function createProgressiveLayout(seriesType: string): StageHandler {
const baseDimIdx = data.getDimensionIndex(data.mapDimension(baseAxis.dim));
const drawBackground = seriesModel.get('showBackground', true);
const valueDim = data.mapDimension(valueAxis.dim);
const stackResultDim = data.getCalculationInfo('stackResultDimension');
const stacked = isDimensionStacked(data, valueDim) && !!data.getCalculationInfo('stackedOnSeries');
const isValueAxisH = valueAxis.isHorizontal();
const valueAxisStart = getValueAxisStart(baseAxis, valueAxis);
const isLarge = isInLargeMode(seriesModel);
const barMinHeight = seriesModel.get('barMinHeight') || 0;

// Determine stacked dimensions.
const stackResultDim = data.getCalculationInfo('stackResultDimension');
const stackedDimIdx = stackResultDim && data.getDimensionIndex(stackResultDim);
const stackedOverDim = data.getCalculationInfo('stackedOverDimension');
const stackedOverDimIdx = stackedOverDim && data.getDimensionIndex(stackedOverDim);
const isPercentStackEnabled = seriesModel.get('stackPercent');
const stacked = isPercentStackEnabled
|| (isDimensionStacked(data, valueDim) && !!data.getCalculationInfo('stackedOnSeries'));

// Layout info.
const columnWidth = data.getLayout('size');
Expand Down Expand Up @@ -521,7 +526,16 @@ export function createProgressiveLayout(seriesType: string): StageHandler {
// Because of the barMinHeight, we can not use the value in
// stackResultDimension directly.
if (stacked) {
stackStartValue = +value - (store.get(valueDimIdx, dataIndex) as number);
if (isPercentStackEnabled) {
// When percentStack is true, use the normalized bottom edge (stackedOverDimension)
// as the start value of the bar segment.
stackStartValue = store.get(stackedOverDimIdx, dataIndex);
}
else {
// For standard (non-percent) stack, subtract the original value from the
// stacked total to compute the bar segment's start value.
stackStartValue = +value - (store.get(valueDimIdx, dataIndex) as number);
}
}

let x;
Expand Down
17 changes: 16 additions & 1 deletion src/model/mixin/dataFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import GlobalModel from '../Global';
import { TooltipMarkupBlockFragment } from '../../component/tooltip/tooltipMarkup';
import { error, makePrintable } from '../../util/log';
import { round } from '../../util/number';

const DIMENSION_LABEL_REG = /\{@(.+?)\}/g;

Expand Down Expand Up @@ -72,7 +73,7 @@ export class DataFormatMixin {
const isSeries = mainType === 'series';
const userOutput = data.userOutput && data.userOutput.get();

return {
const params: CallbackDataParams = {
componentType: mainType,
componentSubType: this.subType,
componentIndex: this.componentIndex,
Expand All @@ -93,6 +94,20 @@ export class DataFormatMixin {
// Param name list for mapping `a`, `b`, `c`, `d`, `e`
$vars: ['seriesName', 'name', 'value']
};

const isPercentStackEnabled = data.getCalculationInfo('isPercentStackEnabled');
if (isPercentStackEnabled) {
// Include the normalized value when stackPercent is true.
const stackResultDim = data.getCalculationInfo('stackResultDimension');
const stackedOverDim = data.getCalculationInfo('stackedOverDimension');
const stackTop = data.get(stackResultDim, dataIndex) as number;
const stackBottom = data.get(stackedOverDim, dataIndex) as number;
if (!isNaN(stackTop) && !isNaN(stackBottom)) {
const normalizedValue = stackTop - stackBottom;
params.percent = round(normalizedValue, 2);
}
}
return params;
}

/**
Expand Down
26 changes: 10 additions & 16 deletions src/processor/dataStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,9 @@
import {createHashMap, each} from 'zrender/src/core/util';
import GlobalModel from '../model/Global';
import SeriesModel from '../model/Series';
import { SeriesOption, SeriesStackOptionMixin } from '../util/types';
import SeriesData, { DataCalculationInfo } from '../data/SeriesData';
import { SeriesOption, SeriesStackOptionMixin, StackInfo } from '../util/types';
import { addSafe } from '../util/number';

type StackInfo = Pick<
DataCalculationInfo<SeriesOption & SeriesStackOptionMixin>,
'stackedDimension'
| 'isStackedByIndex'
| 'stackedByDimension'
| 'stackResultDimension'
| 'stackedOverDimension'
> & {
data: SeriesData
seriesModel: SeriesModel<SeriesOption & SeriesStackOptionMixin>
};
import { calculatePercentStack } from '../util/stack';

// (1) [Caution]: the logic is correct based on the premises:
// data processing stage is blocked in stream.
Expand Down Expand Up @@ -95,11 +83,17 @@ export default function dataStack(ecModel: GlobalModel) {
});

// Calculate stack values
calculateStack(stackInfoList);
const isPercentStackEnabled = stackInfoList.some((info) => info.seriesModel.get('stackPercent'));
if (isPercentStackEnabled) {
calculatePercentStack(stackInfoList);
}
else {
calculateStandardStack(stackInfoList);
}
});
}

function calculateStack(stackInfoList: StackInfo[]) {
function calculateStandardStack(stackInfoList: StackInfo[]) {
each(stackInfoList, function (targetStackInfo, idxInStack) {
const resultVal: number[] = [];
const resultNaN = [NaN, NaN];
Expand Down
94 changes: 94 additions & 0 deletions src/util/stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { each } from 'zrender/src/core/util';
import { addSafe } from './number';
import { StackInfo } from './types';

/**
* Percent stackStrategy logic to normalize each value as a percentage of the total per index.
*/
export function calculatePercentStack(stackInfoList: StackInfo[]) {
const dataLength = stackInfoList[0].data.count();
if (dataLength === 0) {
return;
}

// Calculate totals per data index across all series in the stack group.
const totals = calculateStackTotals(stackInfoList, dataLength);

// Used to track running total of percent values at each index.
const cumulativePercents = new Float64Array(dataLength);

const resultNaN = [NaN, NaN];

each(stackInfoList, function (targetStackInfo) {
const resultVal: number[] = [];
const dims: [string, string] = [targetStackInfo.stackResultDimension, targetStackInfo.stackedOverDimension];
const targetData = targetStackInfo.data;
const stackedDim = targetStackInfo.stackedDimension;

// Should not write on raw data, because stack series model list changes
// depending on legend selection.
targetData.modify(dims, function (v0, v1, dataIndex) {
const rawValue = targetData.get(stackedDim, dataIndex) as number;

// Consider `connectNulls` of line area, if value is NaN, stackedOver
// should also be NaN, to draw a appropriate belt area.
if (isNaN(rawValue)) {
return resultNaN;
}

// Pre-calculated total for this specific data index.
const total = totals[dataIndex];

// Percentage contribution of this segment.
const percent = total === 0 ? 0 : (rawValue / total) * 100;

// Bottom edge of this segment (cumulative % before this series).
const stackedOver = cumulativePercents[dataIndex];

// Update the cumulative percentage for the next series at this index to use.
cumulativePercents[dataIndex] = addSafe(stackedOver, percent);

// Result: [Top edge %, Bottom edge %]
resultVal[0] = cumulativePercents[dataIndex];
resultVal[1] = stackedOver;
return resultVal;
});
});
}

/**
* Helper to calculate the total value across all series for each data index.
*/
function calculateStackTotals(stackInfoList: StackInfo[], dataLength: number): number[] {
const totals = Array(dataLength).fill(0);
each(stackInfoList, (stackInfo) => {
const data = stackInfo.data;
const dim = stackInfo.stackedDimension;
for (let i = 0; i < dataLength; i++) {
const val = data.get(dim, i) as number;
if (!isNaN(val)) {
totals[i] = addSafe(totals[i], val);
}
}
});
return totals;
}
20 changes: 17 additions & 3 deletions src/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import ExtensionAPI from '../core/ExtensionAPI';
import SeriesModel from '../model/Series';
import { createHashMap, HashMap } from 'zrender/src/core/util';
import { TaskPlanCallbackReturn, TaskProgressParams } from '../core/task';
import SeriesData from '../data/SeriesData';
import SeriesData, { DataCalculationInfo } from '../data/SeriesData';
import { Dictionary, ElementEventName, ImageLike, TextAlign, TextVerticalAlign } from 'zrender/src/core/types';
import { PatternObject } from 'zrender/src/graphic/Pattern';
import { TooltipMarker } from './format';
Expand Down Expand Up @@ -880,7 +880,7 @@ export interface CallbackDataParams {
marker?: TooltipMarker;
status?: DisplayState;
dimensionIndex?: number;
percent?: number; // Only for chart like 'pie'
percent?: number; // Only for chart like 'pie' or when 'stackPercent' is used.

// Param name list for mapping `a`, `b`, `c`, `d`, `e`
$vars: string[];
Expand Down Expand Up @@ -1983,7 +1983,21 @@ export interface SeriesStackOptionMixin {
stack?: string
stackStrategy?: 'samesign' | 'all' | 'positive' | 'negative';
stackOrder?: 'seriesAsc' | 'seriesDesc'; // default: seriesAsc
}
stackPercent?: boolean;
}

export type StackInfo = Pick<
DataCalculationInfo<SeriesOption & SeriesStackOptionMixin>,
'stackedDimension'
| 'isStackedByIndex'
| 'stackedByDimension'
| 'stackResultDimension'
| 'stackedOverDimension'
| 'isPercentStackEnabled'
> & {
data: SeriesData
seriesModel: SeriesModel<SeriesOption & SeriesStackOptionMixin>
};

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

Expand Down
Loading