| /* |
| * 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; |
| } |