blob: 081d88463473e6c13781d1a9f907a41837dd201b [file] [log] [blame]
/*
* 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 * as zrUtil from 'zrender/src/core/util';
import {parsePercent} from '../util/number';
import {isDimensionStacked} from '../data/helper/dataStackHelper';
import type BarSeriesModel from '../chart/bar/BarSeries';
import type Polar from '../coord/polar/Polar';
import AngleAxis from '../coord/polar/AngleAxis';
import RadiusAxis from '../coord/polar/RadiusAxis';
import GlobalModel from '../model/Global';
import ExtensionAPI from '../core/ExtensionAPI';
import { Dictionary } from '../util/types';
type PolarAxis = AngleAxis | RadiusAxis;
interface StackInfo {
width: number
maxWidth: number
}
interface LayoutColumnInfo {
autoWidthCount: number
bandWidth: number
remainedWidth: number
categoryGap: string | number
gap: string | number
stacks: Dictionary<StackInfo>
}
interface BarWidthAndOffset {
width: number
offset: number
}
function getSeriesStackId(seriesModel: BarSeriesModel) {
return seriesModel.get('stack')
|| '__ec_stack_' + seriesModel.seriesIndex;
}
function getAxisKey(polar: Polar, axis: PolarAxis) {
return axis.dim + polar.model.componentIndex;
}
function barLayoutPolar(seriesType: string, ecModel: GlobalModel, api: ExtensionAPI) {
const lastStackCoords: Dictionary<{p: number, n: number}[]> = {};
const barWidthAndOffset = calRadialBar(
zrUtil.filter(
ecModel.getSeriesByType(seriesType) as BarSeriesModel[],
function (seriesModel) {
return !ecModel.isSeriesFiltered(seriesModel)
&& seriesModel.coordinateSystem
&& seriesModel.coordinateSystem.type === 'polar';
}
)
);
ecModel.eachSeriesByType(seriesType, function (seriesModel: BarSeriesModel) {
// Check series coordinate, do layout for polar only
if (seriesModel.coordinateSystem.type !== 'polar') {
return;
}
const data = seriesModel.getData();
const polar = seriesModel.coordinateSystem as Polar;
const baseAxis = polar.getBaseAxis();
const axisKey = getAxisKey(polar, baseAxis);
const stackId = getSeriesStackId(seriesModel);
const columnLayoutInfo = barWidthAndOffset[axisKey][stackId];
const columnOffset = columnLayoutInfo.offset;
const columnWidth = columnLayoutInfo.width;
const valueAxis = polar.getOtherAxis(baseAxis);
const cx = seriesModel.coordinateSystem.cx;
const cy = seriesModel.coordinateSystem.cy;
const barMinHeight = seriesModel.get('barMinHeight') || 0;
const barMinAngle = seriesModel.get('barMinAngle') || 0;
lastStackCoords[stackId] = lastStackCoords[stackId] || [];
const valueDim = data.mapDimension(valueAxis.dim);
const baseDim = data.mapDimension(baseAxis.dim);
const stacked = isDimensionStacked(data, valueDim /*, baseDim*/);
const clampLayout = baseAxis.dim !== 'radius'
|| !seriesModel.get('roundCap', true);
const valueAxisStart = valueAxis.dataToCoord(0);
for (let idx = 0, len = data.count(); idx < len; idx++) {
const value = data.get(valueDim, idx) as number;
const baseValue = data.get(baseDim, idx) as number;
const sign = value >= 0 ? 'p' : 'n' as 'p' | 'n';
let baseCoord = valueAxisStart;
// Because of the barMinHeight, we can not use the value in
// stackResultDimension directly.
// Only ordinal axis can be stacked.
if (stacked) {
if (!lastStackCoords[stackId][baseValue]) {
lastStackCoords[stackId][baseValue] = {
p: valueAxisStart, // Positive stack
n: valueAxisStart // Negative stack
};
}
// Should also consider #4243
baseCoord = lastStackCoords[stackId][baseValue][sign];
}
let r0;
let r;
let startAngle;
let endAngle;
// radial sector
if (valueAxis.dim === 'radius') {
let radiusSpan = valueAxis.dataToCoord(value) - valueAxisStart;
const angle = baseAxis.dataToCoord(baseValue);
if (Math.abs(radiusSpan) < barMinHeight) {
radiusSpan = (radiusSpan < 0 ? -1 : 1) * barMinHeight;
}
r0 = baseCoord;
r = baseCoord + radiusSpan;
startAngle = angle - columnOffset;
endAngle = startAngle - columnWidth;
stacked && (lastStackCoords[stackId][baseValue][sign] = r);
}
// tangential sector
else {
let angleSpan = valueAxis.dataToCoord(value, clampLayout) - valueAxisStart;
const radius = baseAxis.dataToCoord(baseValue);
if (Math.abs(angleSpan) < barMinAngle) {
angleSpan = (angleSpan < 0 ? -1 : 1) * barMinAngle;
}
r0 = radius + columnOffset;
r = r0 + columnWidth;
startAngle = baseCoord;
endAngle = baseCoord + angleSpan;
// if the previous stack is at the end of the ring,
// add a round to differentiate it from origin
// let extent = angleAxis.getExtent();
// let stackCoord = angle;
// if (stackCoord === extent[0] && value > 0) {
// stackCoord = extent[1];
// }
// else if (stackCoord === extent[1] && value < 0) {
// stackCoord = extent[0];
// }
stacked && (lastStackCoords[stackId][baseValue][sign] = endAngle);
}
data.setItemLayout(idx, {
cx: cx,
cy: cy,
r0: r0,
r: r,
// Consider that positive angle is anti-clockwise,
// while positive radian of sector is clockwise
startAngle: -startAngle * Math.PI / 180,
endAngle: -endAngle * Math.PI / 180
});
}
});
}
/**
* Calculate bar width and offset for radial bar charts
*/
function calRadialBar(barSeries: BarSeriesModel[]) {
// Columns info on each category axis. Key is polar name
const columnsMap: Dictionary<LayoutColumnInfo> = {};
zrUtil.each(barSeries, function (seriesModel, idx) {
const data = seriesModel.getData();
const polar = seriesModel.coordinateSystem as Polar;
const baseAxis = polar.getBaseAxis();
const axisKey = getAxisKey(polar, baseAxis);
const axisExtent = baseAxis.getExtent();
const bandWidth = baseAxis.type === 'category'
? baseAxis.getBandWidth()
: (Math.abs(axisExtent[1] - axisExtent[0]) / data.count());
const columnsOnAxis = columnsMap[axisKey] || {
bandWidth: bandWidth,
remainedWidth: bandWidth,
autoWidthCount: 0,
categoryGap: '20%',
gap: '30%',
stacks: {}
};
const stacks = columnsOnAxis.stacks;
columnsMap[axisKey] = columnsOnAxis;
const stackId = getSeriesStackId(seriesModel);
if (!stacks[stackId]) {
columnsOnAxis.autoWidthCount++;
}
stacks[stackId] = stacks[stackId] || {
width: 0,
maxWidth: 0
};
let barWidth = parsePercent(
seriesModel.get('barWidth'),
bandWidth
);
const barMaxWidth = parsePercent(
seriesModel.get('barMaxWidth'),
bandWidth
);
const barGap = seriesModel.get('barGap');
const barCategoryGap = seriesModel.get('barCategoryGap');
if (barWidth && !stacks[stackId].width) {
barWidth = Math.min(columnsOnAxis.remainedWidth, barWidth);
stacks[stackId].width = barWidth;
columnsOnAxis.remainedWidth -= barWidth;
}
barMaxWidth && (stacks[stackId].maxWidth = barMaxWidth);
(barGap != null) && (columnsOnAxis.gap = barGap);
(barCategoryGap != null) && (columnsOnAxis.categoryGap = barCategoryGap);
});
const result: Dictionary<Dictionary<BarWidthAndOffset>> = {};
zrUtil.each(columnsMap, function (columnsOnAxis, coordSysName) {
result[coordSysName] = {};
const stacks = columnsOnAxis.stacks;
const bandWidth = columnsOnAxis.bandWidth;
const categoryGap = parsePercent(columnsOnAxis.categoryGap, bandWidth);
const barGapPercent = parsePercent(columnsOnAxis.gap, 1);
let remainedWidth = columnsOnAxis.remainedWidth;
let autoWidthCount = columnsOnAxis.autoWidthCount;
let autoWidth = (remainedWidth - categoryGap)
/ (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
autoWidth = Math.max(autoWidth, 0);
// Find if any auto calculated bar exceeded maxBarWidth
zrUtil.each(stacks, function (column, stack) {
let maxWidth = column.maxWidth;
if (maxWidth && maxWidth < autoWidth) {
maxWidth = Math.min(maxWidth, remainedWidth);
if (column.width) {
maxWidth = Math.min(maxWidth, column.width);
}
remainedWidth -= maxWidth;
column.width = maxWidth;
autoWidthCount--;
}
});
// Recalculate width again
autoWidth = (remainedWidth - categoryGap)
/ (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
autoWidth = Math.max(autoWidth, 0);
let widthSum = 0;
let lastColumn: StackInfo;
zrUtil.each(stacks, function (column, idx) {
if (!column.width) {
column.width = autoWidth;
}
lastColumn = column;
widthSum += column.width * (1 + barGapPercent);
});
if (lastColumn) {
widthSum -= lastColumn.width * barGapPercent;
}
let offset = -widthSum / 2;
zrUtil.each(stacks, function (column, stackId) {
result[coordSysName][stackId] = result[coordSysName][stackId] || {
offset: offset,
width: column.width
} as BarWidthAndOffset;
offset += column.width * (1 + barGapPercent);
});
});
return result;
}
export default barLayoutPolar;