| /* |
| * 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 { parsePercent } from '../../util/number'; |
| import * as zrUtil from 'zrender/src/core/util'; |
| import GlobalModel from '../../model/Global'; |
| import ExtensionAPI from '../../core/ExtensionAPI'; |
| import SunburstSeriesModel, { SunburstSeriesNodeItemOption, SunburstSeriesOption } from './SunburstSeries'; |
| import { TreeNode } from '../../data/Tree'; |
| import SeriesModel from '../../model/Series'; |
| |
| // let PI2 = Math.PI * 2; |
| const RADIAN = Math.PI / 180; |
| |
| export default function sunburstLayout( |
| seriesType: 'sunburst', |
| ecModel: GlobalModel, |
| api: ExtensionAPI |
| ) { |
| ecModel.eachSeriesByType(seriesType, function (seriesModel: SunburstSeriesModel) { |
| let center = seriesModel.get('center'); |
| let radius = seriesModel.get('radius'); |
| |
| if (!zrUtil.isArray(radius)) { |
| radius = [0, radius]; |
| } |
| if (!zrUtil.isArray(center)) { |
| center = [center, center]; |
| } |
| |
| const width = api.getWidth(); |
| const height = api.getHeight(); |
| const size = Math.min(width, height); |
| const cx = parsePercent(center[0], width); |
| const cy = parsePercent(center[1], height); |
| const r0 = parsePercent(radius[0], size / 2); |
| const r = parsePercent(radius[1], size / 2); |
| |
| const startAngle = -seriesModel.get('startAngle') * RADIAN; |
| const minAngle = seriesModel.get('minAngle') * RADIAN; |
| |
| const virtualRoot = seriesModel.getData().tree.root; |
| const treeRoot = seriesModel.getViewRoot(); |
| const rootDepth = treeRoot.depth; |
| |
| const sort = seriesModel.get('sort'); |
| if (sort != null) { |
| initChildren(treeRoot, sort); |
| } |
| |
| let validDataCount = 0; |
| zrUtil.each(treeRoot.children, function (child) { |
| !isNaN(child.getValue() as number) && validDataCount++; |
| }); |
| |
| const sum = treeRoot.getValue() as number; |
| // Sum may be 0 |
| const unitRadian = Math.PI / (sum || validDataCount) * 2; |
| |
| const renderRollupNode = treeRoot.depth > 0; |
| const levels = treeRoot.height - (renderRollupNode ? -1 : 1); |
| const rPerLevel = (r - r0) / (levels || 1); |
| |
| const clockwise = seriesModel.get('clockwise'); |
| |
| const stillShowZeroSum = seriesModel.get('stillShowZeroSum'); |
| |
| // In the case some sector angle is smaller than minAngle |
| // let restAngle = PI2; |
| // let valueSumLargerThanMinAngle = 0; |
| |
| const dir = clockwise ? 1 : -1; |
| |
| /** |
| * Render a tree |
| * @return increased angle |
| */ |
| const renderNode = function (node: TreeNode, startAngle: number) { |
| if (!node) { |
| return; |
| } |
| |
| let endAngle = startAngle; |
| |
| // Render self |
| if (node !== virtualRoot) { |
| // Tree node is virtual, so it doesn't need to be drawn |
| const value = node.getValue() as number; |
| |
| let angle = (sum === 0 && stillShowZeroSum) |
| ? unitRadian : (value * unitRadian); |
| if (angle < minAngle) { |
| angle = minAngle; |
| // restAngle -= minAngle; |
| } |
| // else { |
| // valueSumLargerThanMinAngle += value; |
| // } |
| |
| endAngle = startAngle + dir * angle; |
| |
| const depth = node.depth - rootDepth |
| - (renderRollupNode ? -1 : 1); |
| let rStart = r0 + rPerLevel * depth; |
| let rEnd = r0 + rPerLevel * (depth + 1); |
| |
| const levelModel = seriesModel.getLevelModel(node); |
| if (levelModel) { |
| let r0 = levelModel.get('r0', true); |
| let r = levelModel.get('r', true); |
| const radius = levelModel.get('radius', true); |
| |
| if (radius != null) { |
| r0 = radius[0]; |
| r = radius[1]; |
| } |
| |
| (r0 != null) && (rStart = parsePercent(r0, size / 2)); |
| (r != null) && (rEnd = parsePercent(r, size / 2)); |
| } |
| |
| node.setLayout({ |
| angle: angle, |
| startAngle: startAngle, |
| endAngle: endAngle, |
| clockwise: clockwise, |
| cx: cx, |
| cy: cy, |
| r0: rStart, |
| r: rEnd |
| }); |
| } |
| |
| // Render children |
| if (node.children && node.children.length) { |
| // currentAngle = startAngle; |
| let siblingAngle = 0; |
| zrUtil.each(node.children, function (node) { |
| siblingAngle += renderNode(node, startAngle + siblingAngle); |
| }); |
| } |
| |
| return endAngle - startAngle; |
| }; |
| |
| // Virtual root node for roll up |
| if (renderRollupNode) { |
| const rStart = r0; |
| const rEnd = r0 + rPerLevel; |
| |
| const angle = Math.PI * 2; |
| virtualRoot.setLayout({ |
| angle: angle, |
| startAngle: startAngle, |
| endAngle: startAngle + angle, |
| clockwise: clockwise, |
| cx: cx, |
| cy: cy, |
| r0: rStart, |
| r: rEnd |
| }); |
| } |
| |
| renderNode(treeRoot, startAngle); |
| }); |
| } |
| |
| /** |
| * Init node children by order and update visual |
| */ |
| function initChildren(node: TreeNode, sortOrder?: SunburstSeriesOption['sort']) { |
| const children = node.children || []; |
| |
| node.children = sort(children, sortOrder); |
| |
| // Init children recursively |
| if (children.length) { |
| zrUtil.each(node.children, function (child) { |
| initChildren(child, sortOrder); |
| }); |
| } |
| } |
| |
| /** |
| * Sort children nodes |
| * |
| * @param {TreeNode[]} children children of node to be sorted |
| * @param {string | function | null} sort sort method |
| * See SunburstSeries.js for details. |
| */ |
| function sort(children: TreeNode[], sortOrder: SunburstSeriesOption['sort']) { |
| if (zrUtil.isFunction(sortOrder)) { |
| const sortTargets = zrUtil.map(children, (child, idx) => { |
| const value = child.getValue() as number; |
| return { |
| params: { |
| depth: child.depth, |
| height: child.height, |
| dataIndex: child.dataIndex, |
| getValue: () => value |
| }, |
| index: idx |
| }; |
| }); |
| sortTargets.sort((a, b) => { |
| return sortOrder(a.params, b.params); |
| }); |
| |
| return zrUtil.map(sortTargets, (target) => { |
| return children[target.index]; |
| }); |
| } |
| else { |
| const isAsc = sortOrder === 'asc'; |
| return children.sort(function (a, b) { |
| const diff = ((a.getValue() as number) - (b.getValue() as number)) * (isAsc ? 1 : -1); |
| return diff === 0 |
| ? (a.dataIndex - b.dataIndex) * (isAsc ? -1 : 1) |
| : diff; |
| }); |
| } |
| } |