| /* | |
| * 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 VisualMapping, { VisualMappingOption } from '../../visual/VisualMapping'; | |
| import { each, extend, isArray } from 'zrender/src/core/util'; | |
| import TreemapSeriesModel, { TreemapSeriesNodeItemOption, TreemapSeriesOption } from './TreemapSeries'; | |
| import { TreemapLayoutNode, TreemapItemLayout } from './treemapLayout'; | |
| import Model from '../../model/Model'; | |
| import { ColorString, ZRColor } from '../../util/types'; | |
| import { modifyHSL, modifyAlpha } from 'zrender/src/tool/color'; | |
| import { makeInner } from '../../util/model'; | |
| type NodeModel = Model<TreemapSeriesNodeItemOption>; | |
| type NodeItemStyleModel = Model<TreemapSeriesNodeItemOption['itemStyle']>; | |
| const ITEM_STYLE_NORMAL = 'itemStyle'; | |
| const inner = makeInner<{ | |
| drColorMappingBy: TreemapSeriesNodeItemOption['colorMappingBy'] | |
| }, VisualMapping>(); | |
| interface TreemapVisual { | |
| color?: ZRColor | |
| colorAlpha?: number | |
| colorSaturation?: number | |
| } | |
| type TreemapLevelItemStyleOption = TreemapSeriesOption['levels'][number]['itemStyle']; | |
| export default { | |
| seriesType: 'treemap', | |
| reset(seriesModel: TreemapSeriesModel) { | |
| const tree = seriesModel.getData().tree; | |
| const root = tree.root; | |
| if (root.isRemoved()) { | |
| return; | |
| } | |
| travelTree( | |
| root, // Visual should calculate from tree root but not view root. | |
| {}, | |
| seriesModel.getViewRoot().getAncestors(), | |
| seriesModel | |
| ); | |
| } | |
| }; | |
| function travelTree( | |
| node: TreemapLayoutNode, | |
| designatedVisual: TreemapVisual, | |
| viewRootAncestors: TreemapLayoutNode[], | |
| seriesModel: TreemapSeriesModel | |
| ) { | |
| const nodeModel = node.getModel<TreemapSeriesNodeItemOption>(); | |
| const nodeLayout = node.getLayout(); | |
| const data = node.hostTree.data; | |
| // Optimize | |
| if (!nodeLayout || nodeLayout.invisible || !nodeLayout.isInView) { | |
| return; | |
| } | |
| const nodeItemStyleModel = nodeModel.getModel(ITEM_STYLE_NORMAL); | |
| const visuals = buildVisuals(nodeItemStyleModel, designatedVisual, seriesModel); | |
| const existsStyle = data.ensureUniqueItemVisual(node.dataIndex, 'style'); | |
| // calculate border color | |
| let borderColor = nodeItemStyleModel.get('borderColor'); | |
| const borderColorSaturation = nodeItemStyleModel.get('borderColorSaturation'); | |
| let thisNodeColor; | |
| if (borderColorSaturation != null) { | |
| // For performance, do not always execute 'calculateColor'. | |
| thisNodeColor = calculateColor(visuals); | |
| borderColor = calculateBorderColor(borderColorSaturation, thisNodeColor); | |
| } | |
| existsStyle.stroke = borderColor; | |
| const viewChildren = node.viewChildren; | |
| if (!viewChildren || !viewChildren.length) { | |
| thisNodeColor = calculateColor(visuals); | |
| // Apply visual to this node. | |
| existsStyle.fill = thisNodeColor; | |
| } | |
| else { | |
| const mapping = buildVisualMapping( | |
| node, nodeModel, nodeLayout, nodeItemStyleModel, visuals, viewChildren | |
| ); | |
| // Designate visual to children. | |
| each(viewChildren, function (child, index) { | |
| // If higher than viewRoot, only ancestors of viewRoot is needed to visit. | |
| if (child.depth >= viewRootAncestors.length | |
| || child === viewRootAncestors[child.depth] | |
| ) { | |
| const childVisual = mapVisual( | |
| nodeModel, visuals, child, index, mapping, seriesModel | |
| ); | |
| travelTree(child, childVisual, viewRootAncestors, seriesModel); | |
| } | |
| }); | |
| } | |
| } | |
| function buildVisuals( | |
| nodeItemStyleModel: Model<TreemapSeriesNodeItemOption['itemStyle']>, | |
| designatedVisual: TreemapVisual, | |
| seriesModel: TreemapSeriesModel | |
| ) { | |
| const visuals = extend({}, designatedVisual); | |
| const designatedVisualItemStyle = seriesModel.designatedVisualItemStyle; | |
| each(['color', 'colorAlpha', 'colorSaturation'] as const, function (visualName) { | |
| // Priority: thisNode > thisLevel > parentNodeDesignated > seriesModel | |
| (designatedVisualItemStyle as any)[visualName] = designatedVisual[visualName]; | |
| const val = nodeItemStyleModel.get(visualName); | |
| designatedVisualItemStyle[visualName] = null; | |
| val != null && ((visuals as any)[visualName] = val); | |
| }); | |
| return visuals; | |
| } | |
| function calculateColor(visuals: TreemapVisual) { | |
| let color = getValueVisualDefine(visuals, 'color') as ColorString; | |
| if (color) { | |
| const colorAlpha = getValueVisualDefine(visuals, 'colorAlpha') as number; | |
| const colorSaturation = getValueVisualDefine(visuals, 'colorSaturation') as number; | |
| if (colorSaturation) { | |
| color = modifyHSL(color, null, null, colorSaturation); | |
| } | |
| if (colorAlpha) { | |
| color = modifyAlpha(color, colorAlpha); | |
| } | |
| return color; | |
| } | |
| } | |
| function calculateBorderColor( | |
| borderColorSaturation: number, | |
| thisNodeColor: ColorString | |
| ) { | |
| return thisNodeColor != null | |
| // Can only be string | |
| ? modifyHSL(thisNodeColor, null, null, borderColorSaturation) | |
| : null; | |
| } | |
| function getValueVisualDefine(visuals: TreemapVisual, name: keyof TreemapVisual) { | |
| const value = visuals[name]; | |
| if (value != null && value !== 'none') { | |
| return value; | |
| } | |
| } | |
| function buildVisualMapping( | |
| node: TreemapLayoutNode, | |
| nodeModel: NodeModel, | |
| nodeLayout: TreemapItemLayout, | |
| nodeItemStyleModel: NodeItemStyleModel, | |
| visuals: TreemapVisual, | |
| viewChildren: TreemapLayoutNode[] | |
| ) { | |
| if (!viewChildren || !viewChildren.length) { | |
| return; | |
| } | |
| const rangeVisual = getRangeVisual(nodeModel, 'color') | |
| || ( | |
| visuals.color != null | |
| && visuals.color !== 'none' | |
| && ( | |
| getRangeVisual(nodeModel, 'colorAlpha') | |
| || getRangeVisual(nodeModel, 'colorSaturation') | |
| ) | |
| ); | |
| if (!rangeVisual) { | |
| return; | |
| } | |
| const visualMin = nodeModel.get('visualMin'); | |
| const visualMax = nodeModel.get('visualMax'); | |
| const dataExtent = nodeLayout.dataExtent.slice() as [number, number]; | |
| visualMin != null && visualMin < dataExtent[0] && (dataExtent[0] = visualMin); | |
| visualMax != null && visualMax > dataExtent[1] && (dataExtent[1] = visualMax); | |
| const colorMappingBy = nodeModel.get('colorMappingBy'); | |
| const opt: VisualMappingOption = { | |
| type: rangeVisual.name, | |
| dataExtent: dataExtent, | |
| visual: rangeVisual.range | |
| }; | |
| if (opt.type === 'color' | |
| && (colorMappingBy === 'index' || colorMappingBy === 'id') | |
| ) { | |
| opt.mappingMethod = 'category'; | |
| opt.loop = true; | |
| // categories is ordinal, so do not set opt.categories. | |
| } | |
| else { | |
| opt.mappingMethod = 'linear'; | |
| } | |
| const mapping = new VisualMapping(opt); | |
| inner(mapping).drColorMappingBy = colorMappingBy; | |
| return mapping; | |
| } | |
| // Notice: If we don't have the attribute 'colorRange', but only use | |
| // attribute 'color' to represent both concepts of 'colorRange' and 'color', | |
| // (It means 'colorRange' when 'color' is Array, means 'color' when not array), | |
| // this problem will be encountered: | |
| // If a level-1 node doesn't have children, and its siblings have children, | |
| // and colorRange is set on level-1, then the node cannot be colored. | |
| // So we separate 'colorRange' and 'color' to different attributes. | |
| function getRangeVisual(nodeModel: NodeModel, name: keyof TreemapVisual) { | |
| // 'colorRange', 'colorARange', 'colorSRange'. | |
| // If not exists on this node, fetch from levels and series. | |
| const range = nodeModel.get(name); | |
| return (isArray(range) && range.length) ? { | |
| name: name, | |
| range: range | |
| } : null; | |
| } | |
| function mapVisual( | |
| nodeModel: NodeModel, | |
| visuals: TreemapVisual, | |
| child: TreemapLayoutNode, | |
| index: number, | |
| mapping: VisualMapping, | |
| seriesModel: TreemapSeriesModel | |
| ) { | |
| const childVisuals = extend({}, visuals); | |
| if (mapping) { | |
| // Only support color, colorAlpha, colorSaturation. | |
| const mappingType = mapping.type as keyof TreemapVisual; | |
| const colorMappingBy = mappingType === 'color' && inner(mapping).drColorMappingBy; | |
| const value = colorMappingBy === 'index' | |
| ? index | |
| : colorMappingBy === 'id' | |
| ? seriesModel.mapIdToIndex(child.getId()) | |
| : child.getValue(nodeModel.get('visualDimension')); | |
| (childVisuals as any)[mappingType] = mapping.mapValueToVisual(value); | |
| } | |
| return childVisuals; | |
| } |