This is an automated email from the ASF dual-hosted git repository. 100pah pushed a commit to branch fix/simplify-21586 in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 06cd2ea5f91c66d25d8accdc771074a7962a51d2 Author: 100pah <[email protected]> AuthorDate: Sat May 30 16:23:58 2026 +0800 fixRegression: Fix #21586. (1). Remove some verbose and unnecessary code. (2). Remove the breaking change to `visualMap[type='continuous'].formatter: function () {}` - not necessary, and that breaking change can cause the existing lables display incorrectly when labels touch. (3). Switch to a more comprehensive implementation - adjust the positions of the two labels to avoid overlap. The previous approach (merging two labels) may occupy extra horizontal space and may unexpectedly caus [...] --- src/component/visualMap/ContinuousView.ts | 185 ++++++++++--------------- src/component/visualMap/VisualMapModel.ts | 5 + test/runTest/actions/__meta__.json | 1 + test/runTest/actions/visualMap-continuous.json | 1 + test/visualMap-continuous.html | 9 +- 5 files changed, 87 insertions(+), 114 deletions(-) diff --git a/src/component/visualMap/ContinuousView.ts b/src/component/visualMap/ContinuousView.ts index ef6776e7f..939707b5c 100644 --- a/src/component/visualMap/ContinuousView.ts +++ b/src/component/visualMap/ContinuousView.ts @@ -22,7 +22,7 @@ import LinearGradient, { LinearGradientObject } from 'zrender/src/graphic/Linear import * as eventTool from 'zrender/src/core/event'; import VisualMapView from './VisualMapView'; import * as graphic from '../../util/graphic'; -import * as numberUtil from '../../util/number'; +import {linearMap, mathMax, mathMin, mathPI} from '../../util/number'; import sliderMove from '../helper/sliderMove'; import * as helper from './helper'; import * as modelUtil from '../../util/model'; @@ -42,10 +42,6 @@ import { createTextStyle } from '../../label/labelStyle'; import { findEventDispatcher } from '../../util/event'; import BoundingRect from 'zrender/src/core/BoundingRect'; -const linearMap = numberUtil.linearMap; -const each = zrUtil.each; -const mathMin = Math.min; -const mathMax = Math.max; // Arbitrary value const HOVER_LINK_SIZE = 12; @@ -53,6 +49,12 @@ const HOVER_LINK_OUT = 6; /** Pixels to inflate handle label bounds when testing overlap (merge slightly before touching). */ const HANDLE_LABEL_MERGE_MARGIN = 2; +const elInner = modelUtil.makeInner<{ + hdlIdx: ContinuousVisualMapHandleIndex +}, Element>(); + +type ContinuousVisualMapHandleIndex = 0 | 1 | 'all'; + type Orient = VisualMapModel['option']['orient']; type ShapeStorage = { @@ -111,8 +113,6 @@ class ContinuousView extends VisualMapView { private _firstShowIndicator: boolean; - /** Whether the two handle value labels are merged into one (overlap). */ - private _handleLabelsMerged: boolean | undefined; init(ecModel: GlobalModel, api: ExtensionAPI) { super.init(ecModel, api); @@ -134,7 +134,6 @@ class ContinuousView extends VisualMapView { private _buildView() { this.group.removeAll(); - this._handleLabelsMerged = undefined; const visualMapModel = this.visualMapModel; const thisGroup = this.group; @@ -223,10 +222,9 @@ class ContinuousView extends VisualMapView { gradientBarGroup.add(shapes.outOfRange = createPolygon()); gradientBarGroup.add(shapes.inRange = createPolygon( null, - useHandle ? getCursor(this._orient) : null, - zrUtil.bind(this._dragHandle, this, 'all', false), - zrUtil.bind(this._dragHandle, this, 'all', true) + useHandle ? getCursor(this._orient) : null )); + this._mountDrag(shapes.inRange, 'all'); // A border radius clip. gradientBarGroup.setClipPath(new graphic.Rect({ @@ -265,8 +263,6 @@ class ContinuousView extends VisualMapView { textSize: number, orient: Orient ) { - const onDrift = zrUtil.bind(this._dragHandle, this, handleIndex, false); - const onDragEnd = zrUtil.bind(this._dragHandle, this, handleIndex, true); const handleSize = parsePercent(visualMapModel.get('handleSize'), itemSize[0]); const handleThumb = createSymbol( visualMapModel.get('handleIcon'), @@ -276,13 +272,11 @@ class ContinuousView extends VisualMapView { const cursor = getCursor(this._orient); handleThumb.attr({ cursor: cursor, - draggable: true, - drift: onDrift, - ondragend: onDragEnd, onmousemove(e) { eventTool.stop(e.event); } }); + this._mountDrag(handleThumb, handleIndex); handleThumb.x = itemSize[0] / 2; handleThumb.useStyle(visualMapModel.getModel('handleStyle').getItemStyle()); @@ -304,19 +298,17 @@ class ContinuousView extends VisualMapView { const textStyleModel = this.visualMapModel.textStyleModel; const handleLabel = new graphic.Text({ cursor: cursor, - draggable: true, - drift: onDrift, onmousemove(e) { // For mobile device, prevent screen slider on the button. eventTool.stop(e.event); }, - ondragend: onDragEnd, style: createTextStyle(textStyleModel, { x: 0, y: 0, text: '' }) }); + this._mountDrag(handleLabel, handleIndex); handleLabel.ensureState('blur').style = { opacity: 0.1 }; @@ -324,11 +316,9 @@ class ContinuousView extends VisualMapView { this.group.add(handleLabel); - const handleLabelPoint = [handleSize, 0]; - const shapes = this._shapes; shapes.handleThumbs[handleIndex] = handleThumb; - shapes.handleLabelPoints[handleIndex] = handleLabelPoint; + shapes.handleLabelPoints[handleIndex] = [handleSize, 0]; shapes.handleLabels[handleIndex] = handleLabel; } @@ -392,8 +382,17 @@ class ContinuousView extends VisualMapView { this._firstShowIndicator = true; } + private _mountDrag(el: Element, handleIndex: ContinuousVisualMapHandleIndex): void { + el.attr({ + draggable: true, + drift: zrUtil.bind(this._dragHandle, this, el, false), + ondragend: zrUtil.bind(this._dragHandle, this, el, true), + }); + elInner(el).hdlIdx = handleIndex; + } + private _dragHandle( - handleIndex: 0 | 1 | 'all', + sourceEl: Element, isEnd?: boolean, // dx is event from ondragend if isEnd is true. It's not used dx?: number | ElementEvent, @@ -403,6 +402,10 @@ class ContinuousView extends VisualMapView { return; } + const handleIndex = elInner(sourceEl).hdlIdx; + if (__DEV__) { + zrUtil.assert(handleIndex != null); + } this._dragging = !isEnd; if (!isEnd) { @@ -457,7 +460,7 @@ class ContinuousView extends VisualMapView { * @param {number} dx * @param {number} dy */ - private _updateInterval(handleIndex: 0 | 1 | 'all', delta: number) { + private _updateInterval(handleIndex: ContinuousVisualMapHandleIndex, delta: number) { delta = delta || 0; const visualMapModel = this.visualMapModel; const handleEnds = this._handleEnds; @@ -589,28 +592,23 @@ class ContinuousView extends VisualMapView { } private _createBarGroup(itemAlign: helper.ItemAlign) { - const orient = this._orient; + const isVertical = this._orient === 'vertical'; + const isItemAlignButtom = itemAlign === 'bottom'; + const isItemAlignLeft = itemAlign === 'left'; const inverse = this.visualMapModel.get('inverse'); return new graphic.Group( - (orient === 'horizontal' && !inverse) - ? {scaleX: itemAlign === 'bottom' ? 1 : -1, rotation: Math.PI / 2} - : (orient === 'horizontal' && inverse) - ? {scaleX: itemAlign === 'bottom' ? -1 : 1, rotation: -Math.PI / 2} - : (orient === 'vertical' && !inverse) - ? {scaleX: itemAlign === 'left' ? 1 : -1, scaleY: -1} - : {scaleX: itemAlign === 'left' ? 1 : -1} + (!isVertical && !inverse) + ? {scaleX: isItemAlignButtom ? 1 : -1, rotation: mathPI / 2} + : (!isVertical && inverse) + ? {scaleX: isItemAlignButtom ? -1 : 1, rotation: -mathPI / 2} + : (isVertical && !inverse) + ? {scaleX: isItemAlignLeft ? 1 : -1, scaleY: -1} + // isVertical && inverse + : {scaleX: isItemAlignLeft ? 1 : -1} ); } - private _getHandleLabelRectInGroup(label: graphic.Text): BoundingRect { - const local = label.getBoundingRect(); - const rect = new BoundingRect(0, 0, 0, 0); - const m = graphic.getTransform(label, this.group); - BoundingRect.applyTransform(rect, local, m); - return rect; - } - private _updateHandle(handleEnds: number[], visualInRange: BarVisual) { if (!this._useHandle) { return; @@ -622,11 +620,14 @@ class ContinuousView extends VisualMapView { const handleLabels = shapes.handleLabels; const itemSize = visualMapModel.itemSize; const dataExtent = visualMapModel.getExtent(); - const align = this._applyTransform('left', shapes.mainGroup); - const textPoints: [number, number][] = []; - - each( - [0, 1], + const barGroup = shapes.mainGroup; + const align = this._applyTransform('left', barGroup); + const isVertical = this._orient === 'vertical'; + const textPosPair: graphic.Point[] = []; + const textRectPair: BoundingRect[] = []; + + zrUtil.each( + [0, 1] as const, function (handleIndex) { const handleThumb = handleThumbs[handleIndex]; handleThumb.setStyle('fill', visualInRange.handlesColor[handleIndex]); @@ -649,7 +650,7 @@ class ContinuousView extends VisualMapView { graphic.getTransform(handleThumb, this.group), ); - if (this._orient === 'horizontal') { + if (!isVertical) { // If visualMap controls symbol size, an additional offset needs to be added to labels to avoid collision at minimum size. // Offset reaches value of 0 at "maximum" position, so maximum position is not altered at all. const minimumOffset = @@ -660,78 +661,45 @@ class ContinuousView extends VisualMapView { textPoint[1] += minimumOffset; } - textPoints[handleIndex] = [textPoint[0], textPoint[1]]; - handleLabels[handleIndex].setStyle({ x: textPoint[0], y: textPoint[1], text: visualMapModel.formatValueText(this._dataInterval[handleIndex]), verticalAlign: 'middle', align: - this._orient === 'vertical' - ? (this._applyTransform('left', shapes.mainGroup) as TextAlign) + isVertical + ? (this._applyTransform('left', barGroup) as TextAlign) : 'center', }); + elInner(handleLabels[handleIndex]).hdlIdx = handleIndex; // May be updated if previously overlapped. + + textPosPair[handleIndex] = new graphic.Point(textPoint[0], textPoint[1]); + textRectPair[handleIndex] = handleLabels[handleIndex].getBoundingRect().clone(); + graphic.expandOrShrinkRect(textRectPair[handleIndex], HANDLE_LABEL_MERGE_MARGIN, false, true); }, this, ); - const rect0 = this._getHandleLabelRectInGroup(handleLabels[0]); - const rect1 = this._getHandleLabelRectInGroup(handleLabels[1]); - const margin = HANDLE_LABEL_MERGE_MARGIN; - const inflated0 = { - x: rect0.x - margin, - y: rect0.y - margin, - width: rect0.width + margin * 2, - height: rect0.height + margin * 2, - }; - const inflated1 = { - x: rect1.x - margin, - y: rect1.y - margin, - width: rect1.width + margin * 2, - height: rect1.height + margin * 2, - }; - const labelsOverlap = BoundingRect.intersect(inflated0, inflated1); - - if (labelsOverlap) { - const midX = (textPoints[0][0] + textPoints[1][0]) / 2; - const midY = (textPoints[0][1] + textPoints[1][1]) / 2; - handleLabels[0].setStyle({ - x: midX, - y: midY, - text: visualMapModel.formatValueText(this._dataInterval), - verticalAlign: 'middle', - align: - this._orient === 'vertical' - ? (this._applyTransform('left', shapes.mainGroup) as TextAlign) - : 'center', - }); - handleLabels[1].attr({ - invisible: true, - silent: true, - draggable: false, - }); - } - else { - handleLabels[1].attr({ - invisible: false, - silent: false, - draggable: true, - }); - } - - if (labelsOverlap !== this._handleLabelsMerged) { - this._handleLabelsMerged = labelsOverlap; - if (labelsOverlap) { - handleLabels[0].drift = zrUtil.bind(this._dragHandle, this, 'all', false); - handleLabels[0].ondragend = zrUtil.bind(this._dragHandle, this, 'all', true); - } - else { - handleLabels[0].drift = zrUtil.bind(this._dragHandle, this, 0, false); - handleLabels[0].ondragend = zrUtil.bind(this._dragHandle, this, 0, true); - handleLabels[1].drift = zrUtil.bind(this._dragHandle, this, 1, false); - handleLabels[1].ondragend = zrUtil.bind(this._dragHandle, this, 1, true); + const mtv = new graphic.Point(); + const directionVec = this._applyTransform([0, 1], barGroup); + const labelsOverlap = BoundingRect.intersect( + textRectPair[0], + textRectPair[1], + mtv, + { + direction: Math.atan2(directionVec[1], directionVec[0]), + bidirectional: false, } + ); + if (labelsOverlap) { + textPosPair[0].scaleAndAdd(mtv, -0.5); + textPosPair[1].scaleAndAdd(mtv, 0.5); + handleLabels[0].setStyle(textPosPair[0]); + handleLabels[1].setStyle(textPosPair[1]); + // When two handles are too close, the bar is difficult to hit, so dragging in + // 'all' mode becomes hard to trigger. Therefore, we provide another way for + // that -- switch labels dragging to 'all' mode. + elInner(handleLabels[0]).hdlIdx = elInner(handleLabels[1]).hdlIdx = 'all'; } } @@ -1020,19 +988,14 @@ class ContinuousView extends VisualMapView { function createPolygon( points?: number[][], cursor?: string, - onDrift?: (x: number, y: number) => void, - onDragEnd?: () => void ) { return new graphic.Polygon({ - shape: {points: points}, - draggable: !!onDrift, + shape: {points}, cursor: cursor, - drift: onDrift, onmousemove(e) { // For mobile device, prevent screen slider on the button. eventTool.stop(e.event); }, - ondragend: onDragEnd }); } diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts index 843387a46..26262e975 100644 --- a/src/component/visualMap/VisualMapModel.ts +++ b/src/component/visualMap/VisualMapModel.ts @@ -331,6 +331,11 @@ class VisualMapModel<Opts extends VisualMapOption = VisualMapOption> extends Com } /** + * [CAVEAT] + * For `visualMap.type: 'continuous'`, the input `value` can only be an `number[]`. + * For `visualMap.type: 'piecewise'`, the input `value` can only be an `number | string`. + * Otherwise a breaking change will be introduced to `visualMap.formatter: function() {}`. + * * @example * this.formatValueText(someVal); // format single numeric value to text. * this.formatValueText(someVal, true); // format single category value to text. diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index c0dc3b70d..ace1711f5 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -264,6 +264,7 @@ "universalTransition2": 3, "universalTransition3": 2, "visualMap-categories": 1, + "visualMap-continuous": 7, "visualMap-multi-continuous": 1, "visualMap-on-data": 1, "visualMap-selectMode": 2 diff --git a/test/runTest/actions/visualMap-continuous.json b/test/runTest/actions/visualMap-continuous.json new file mode 100644 index 000000000..59229906a --- /dev/null +++ b/test/runTest/actions/visualMap-continuous.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":581,"x":774,"y":340},{"type":"mousemove","time":789,"x":713,"y":365},{"type":"mousemove","time":996,"x":512,"y":363},{"type":"mousemove","time":1206,"x":493,"y":368},{"type":"mousemove","time":1379,"x":492,"y":369},{"type":"mousemove","time":1579,"x":370,"y":467},{"type":"mousemove","time":1780,"x":293,"y":515},{"type":"mousemove","time":2003,"x":295,"y":540},{"type":"mousemove","time":2204,"x":304,"y":541},{"type":"mousemove","time": [...] \ No newline at end of file diff --git a/test/visualMap-continuous.html b/test/visualMap-continuous.html index a89e241cf..23bb6d22b 100644 --- a/test/visualMap-continuous.html +++ b/test/visualMap-continuous.html @@ -703,7 +703,7 @@ under the License. }); }); - createCase("Color HueRange handles: merge on overlap", 6, (chart) => { + createCase("Merge handles on overlap (default formatter)", 6, (chart) => { const data0 = []; const MAX_DIM1 = 100; @@ -784,7 +784,7 @@ under the License. }); }); - createCase("Color HueRange handles: dual drag when apart", 7, (chart) => { + createCase("Merge handles on overlap (user-provided formatter: xxx%)", 7, (chart) => { const data0 = []; const MAX_DIM1 = 100; @@ -834,7 +834,10 @@ under the License. min: 0, max: MAX_DIM1, calculable: true, - range: [50, 95], + range: [90, 95], + formatter: function (value) { + return value + '%'; + }, dimension: 0, inRange: { colorHue: [0, 300], --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
