/* * 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, { SunburstSeriesOption } from './SunburstSeries'; import { TreeNode } from '../../data/Tree'; // 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; }); } }