| /* | |
| * 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, | |
| /** | |
| * Keep the same logic with bar in catesion: use end value to | |
| * control direction. Notice that if clockwise is true (by | |
| * default), the sector will always draw clockwisely, no matter | |
| * whether endAngle is greater or less than startAngle. | |
| */ | |
| clockwise: startAngle >= endAngle | |
| }); | |
| } | |
| }); | |
| } | |
| /** | |
| * 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; |