diff --git a/src/chart/sankey/SankeySeries.ts b/src/chart/sankey/SankeySeries.ts index ebd85227ab..590d5a6205 100644 --- a/src/chart/sankey/SankeySeries.ts +++ b/src/chart/sankey/SankeySeries.ts @@ -136,6 +136,11 @@ export interface SankeySeriesOption * The number of iterations to change the position of the node */ layoutIterations?: number + /** + * Sorting method used when resolving node collisions within each depth column. + * Set to null to preserve the original node order. + */ + sort?: 'desc' | null nodeAlign?: 'justify' | 'left' | 'right' // TODO justify should be auto @@ -318,6 +323,7 @@ class SankeySeriesModel extends SeriesModel { draggable: true, layoutIterations: 32, + sort: 'desc', // true | false | 'move' | 'scale', see module:component/helper/RoamController. roam: false, diff --git a/src/chart/sankey/sankeyLayout.ts b/src/chart/sankey/sankeyLayout.ts index 8e9c8d22bb..a75388b1dd 100644 --- a/src/chart/sankey/sankeyLayout.ts +++ b/src/chart/sankey/sankeyLayout.ts @@ -57,8 +57,9 @@ export default function sankeyLayout(ecModel: GlobalModel, api: ExtensionAPI) { const orient = seriesModel.get('orient'); const nodeAlign = seriesModel.get('nodeAlign'); + const sort = seriesModel.get('sort'); - layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign); + layoutSankey(nodes, edges, nodeWidth, nodeGap, width, height, iterations, orient, nodeAlign, sort); }); } @@ -71,10 +72,11 @@ function layoutSankey( height: number, iterations: number, orient: LayoutOrient, - nodeAlign: SankeySeriesOption['nodeAlign'] + nodeAlign: SankeySeriesOption['nodeAlign'], + sort: SankeySeriesOption['sort'] ) { computeNodeBreadths(nodes, edges, nodeWidth, width, height, orient, nodeAlign); - computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient); + computeNodeDepths(nodes, edges, height, width, nodeGap, iterations, orient, sort); computeEdgeDepths(nodes, orient); } @@ -257,6 +259,7 @@ function scaleNodeBreadths(nodes: GraphNode[], kx: number, orient: LayoutOrient) * @param nodeGap the vertical distance between two nodes * in the same column. * @param iterations the number of iterations for the algorithm + * @param sort sorting method used when resolving collisions within each column */ function computeNodeDepths( nodes: GraphNode[], @@ -265,21 +268,22 @@ function computeNodeDepths( width: number, nodeGap: number, iterations: number, - orient: LayoutOrient + orient: LayoutOrient, + sort: SankeySeriesOption['sort'] ) { const nodesByBreadth = prepareNodesByBreadth(nodes, orient); initializeNodeDepth(nodesByBreadth, edges, height, width, nodeGap, orient); - resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); + resolveCollisions(nodesByBreadth, nodeGap, height, width, orient, sort); for (let alpha = 1; iterations > 0; iterations--) { // 0.99 is a experience parameter, ensure that each iterations of // changes as small as possible. alpha *= 0.99; relaxRightToLeft(nodesByBreadth, alpha, orient); - resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); + resolveCollisions(nodesByBreadth, nodeGap, height, width, orient, sort); relaxLeftToRight(nodesByBreadth, alpha, orient); - resolveCollisions(nodesByBreadth, nodeGap, height, width, orient); + resolveCollisions(nodesByBreadth, nodeGap, height, width, orient, sort); } } @@ -355,13 +359,16 @@ function resolveCollisions( nodeGap: number, height: number, width: number, - orient: LayoutOrient + orient: LayoutOrient, + sort: SankeySeriesOption['sort'] ) { const keyAttr = orient === 'vertical' ? 'x' : 'y'; zrUtil.each(nodesByBreadth, function (nodes) { - nodes.sort(function (a, b) { - return a.getLayout()[keyAttr] - b.getLayout()[keyAttr]; - }); + if (sort !== null) { + nodes.sort(function (a, b) { + return a.getLayout()[keyAttr] - b.getLayout()[keyAttr]; + }); + } let nodeX; let node; let dy; @@ -528,4 +535,4 @@ function computeEdgeDepths(nodes: GraphNode[], orient: LayoutOrient) { ty += edge.getLayout().dy; }); }); -} \ No newline at end of file +} diff --git a/src/component/helper/BrushController.ts b/src/component/helper/BrushController.ts index 09022330a7..f394491862 100644 --- a/src/component/helper/BrushController.ts +++ b/src/component/helper/BrushController.ts @@ -72,7 +72,9 @@ export interface BrushCoverConfig { panelId?: string; brushMode?: BrushMode; - // `brushStyle`, `transformable` is not mandatory, use DEFAULT_BRUSH_OPT by default. + // `brushStyle`, `transformable` is not mandatory. When the controller is enabled, + // `updateCovers` inherits from the current brush option first, and then falls back + // to `DEFAULT_BRUSH_OPT`. brushStyle?: Pick; transformable?: boolean; removeOnClick?: boolean; @@ -381,8 +383,9 @@ class BrushController extends Eventful<{ assert(this._mounted); } + const baseBrushOption = this._brushOption || DEFAULT_BRUSH_OPT; coverConfigList = map(coverConfigList, function (coverConfig) { - return merge(clone(DEFAULT_BRUSH_OPT), coverConfig, true); + return merge(clone(baseBrushOption), coverConfig, true); }) as BrushCoverConfig[]; const tmpIdPrefix = '\0-brush-index-'; diff --git a/test/runTest/package-lock.json b/test/runTest/package-lock.json index 43db614435..44431fe9ff 100644 --- a/test/runTest/package-lock.json +++ b/test/runTest/package-lock.json @@ -1936,15 +1936,10 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -2052,13 +2047,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, - "license": "MIT" - }, "node_modules/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -3035,13 +3023,12 @@ } }, "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", "dev": true, - "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -3099,13 +3086,6 @@ "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/streamx": { "version": "2.21.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz", @@ -4993,14 +4973,10 @@ } }, "ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dev": true, - "requires": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - } + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true }, "is-arrayish": { "version": "0.2.1", @@ -5081,12 +5057,6 @@ "argparse": "^2.0.1" } }, - "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true - }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -5841,12 +5811,12 @@ } }, "socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", "dev": true, "requires": { - "ip-address": "^9.0.5", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, @@ -5886,12 +5856,6 @@ "dev": true, "optional": true }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, "streamx": { "version": "2.21.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz", diff --git a/test/sankey-node-sorting.html b/test/sankey-node-sorting.html new file mode 100644 index 0000000000..56430cfb24 --- /dev/null +++ b/test/sankey-node-sorting.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + diff --git a/test/ut/spec/component/helper/BrushController.test.ts b/test/ut/spec/component/helper/BrushController.test.ts new file mode 100644 index 0000000000..94f3382f82 --- /dev/null +++ b/test/ut/spec/component/helper/BrushController.test.ts @@ -0,0 +1,74 @@ +/* +* 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 BrushController from '../../../../../src/component/helper/BrushController'; +import { createChart } from '../../../core/utHelper'; +import { EChartsType } from '../../../../../src/echarts'; +import Rect from 'zrender/src/graphic/shape/Rect'; + + +describe('component/helper/BrushController', function () { + + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('updateCovers inherits the enabled brush style', function () { + const controller = new BrushController(chart.getZr()).mount(); + const brushStyle = { + fill: 'rgba(255, 0, 0, 0.35)', + stroke: 'rgb(255, 0, 0)', + lineWidth: 6, + opacity: 0.45 + }; + + controller + .enableBrush({ + brushType: 'lineX', + brushStyle: brushStyle, + transformable: false, + removeOnClick: true + }) + .updateCovers([{ + brushType: 'lineX', + range: [20, 60] + }]); + + // @ts-ignore access internal state for behavior verification. + const cover = controller._covers[0]; + // @ts-ignore access internal state for behavior verification. + const brushOption = cover.__brushOption; + const mainEl = cover.childAt(0) as Rect; + + expect(brushOption.brushStyle).toEqual(brushStyle); + expect(brushOption.transformable).toEqual(false); + expect(brushOption.removeOnClick).toEqual(true); + expect(mainEl.style.fill).toEqual(brushStyle.fill); + expect(mainEl.style.stroke).toEqual(brushStyle.stroke); + expect(mainEl.style.lineWidth).toEqual(brushStyle.lineWidth); + expect(mainEl.style.opacity).toEqual(brushStyle.opacity); + }); + +});