From 57832f2e3edfd58c93b7b18134ef6789a86713ef Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 6 Jun 2025 11:10:29 +0800 Subject: [PATCH 1/2] fix: disable sampling when label is rich text --- .../src/axis/tick-data/continuous.ts | 79 +++++++------ .../src/axis/tick-data/discrete/linear.ts | 109 ++++++++++-------- .../src/axis/tick-data/util.ts | 16 ++- 3 files changed, 113 insertions(+), 91 deletions(-) diff --git a/packages/vrender-components/src/axis/tick-data/continuous.ts b/packages/vrender-components/src/axis/tick-data/continuous.ts index 777203125..bd0683ead 100644 --- a/packages/vrender-components/src/axis/tick-data/continuous.ts +++ b/packages/vrender-components/src/axis/tick-data/continuous.ts @@ -166,7 +166,7 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick samplingScaleTicks.push(tick); } }); - items = getCartesianLabelBounds(scale, samplingScaleTicks, op as ICartesianTickDataOpt).map( + items = getCartesianLabelBounds(scale, samplingScaleTicks, op as ICartesianTickDataOpt)?.map( (bounds, i) => ({ AABBBounds: bounds, @@ -174,7 +174,7 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick } as ILabelItem) ); } else { - items = getCartesianLabelBounds(scale, scaleTicks, op as ICartesianTickDataOpt).map( + items = getCartesianLabelBounds(scale, scaleTicks, op as ICartesianTickDataOpt)?.map( (bounds, i) => ({ AABBBounds: bounds, @@ -182,49 +182,52 @@ export const continuousTicks = (scale: ContinuousScale, op: ITickDataOpt): ITick } as ILabelItem) ); } - const firstSourceItem = items[0]; - const lastSourceItem = last(items); - const samplingMethod = breakData && breakData() ? methods.greedy : methods.parity; // 由于轴截断后刻度会存在不均匀的情况,所以不能使用 parity 算法 - while (items.length >= 3 && hasOverlap(items as any, labelGap)) { - items = samplingMethod(items, labelGap); - } - - const checkFirst = op.labelFirstVisible; - let checkLast = op.labelLastVisible; // 这里和 auto-hide 里的逻辑有差异,不根据 length 自动强制显示最后一个(会引起 vtable 较多 badcase)。 + if (items) { + const firstSourceItem = items[0]; + const lastSourceItem = last(items); - if (intersect(firstSourceItem as any, lastSourceItem as any, labelGap)) { - if (items.includes(lastSourceItem) && items.length > 1 && checkFirst && checkLast) { - items.splice(items.indexOf(lastSourceItem), 1); - checkLast = false; + const samplingMethod = breakData && breakData() ? methods.greedy : methods.parity; // 由于轴截断后刻度会存在不均匀的情况,所以不能使用 parity 算法 + while (items.length >= 3 && hasOverlap(items as any, labelGap)) { + items = samplingMethod(items, labelGap); } - } - forceItemVisible(firstSourceItem, items, checkFirst, (item: ILabelItem) => - intersect(item as any, firstSourceItem as any, labelGap) - ); - forceItemVisible( - lastSourceItem, - items, - checkLast, - (item: ILabelItem) => - intersect(item as any, lastSourceItem as any, labelGap) || - (checkFirst && item !== firstSourceItem ? intersect(item as any, firstSourceItem as any, labelGap) : false), - true - ); - - const ticks = items.map(item => item.value); - - if (ticks.length < 3 && labelFlush) { - if (ticks.length > 1) { - ticks.pop(); + const checkFirst = op.labelFirstVisible; + let checkLast = op.labelLastVisible; // 这里和 auto-hide 里的逻辑有差异,不根据 length 自动强制显示最后一个(会引起 vtable 较多 badcase)。 + + if (intersect(firstSourceItem as any, lastSourceItem as any, labelGap)) { + if (items.includes(lastSourceItem) && items.length > 1 && checkFirst && checkLast) { + items.splice(items.indexOf(lastSourceItem), 1); + checkLast = false; + } } - if (last(ticks) !== last(scaleTicks)) { - ticks.push(last(scaleTicks)); + + forceItemVisible(firstSourceItem, items, checkFirst, (item: ILabelItem) => + intersect(item as any, firstSourceItem as any, labelGap) + ); + forceItemVisible( + lastSourceItem, + items, + checkLast, + (item: ILabelItem) => + intersect(item as any, lastSourceItem as any, labelGap) || + (checkFirst && item !== firstSourceItem ? intersect(item as any, firstSourceItem as any, labelGap) : false), + true + ); + + const ticks = items.map(item => item.value); + + if (ticks.length < 3 && labelFlush) { + if (ticks.length > 1) { + ticks.pop(); + } + if (last(ticks) !== last(scaleTicks)) { + ticks.push(last(scaleTicks)); + } } - } - scaleTicks = ticks; + scaleTicks = ticks; + } } } return convertDomainToTickData(scaleTicks); diff --git a/packages/vrender-components/src/axis/tick-data/discrete/linear.ts b/packages/vrender-components/src/axis/tick-data/discrete/linear.ts index 7b73b87f8..d32ac2a8c 100644 --- a/packages/vrender-components/src/axis/tick-data/discrete/linear.ts +++ b/packages/vrender-components/src/axis/tick-data/discrete/linear.ts @@ -1,5 +1,5 @@ import type { BandScale, IBaseScale } from '@visactor/vscale'; -import { isFunction, isValid, maxInArray, minInArray, binaryFuzzySearchInNumberRange } from '@visactor/vutils'; +import { isFunction, isValid, maxInArray, minInArray, binaryFuzzySearchInNumberRange, isNil } from '@visactor/vutils'; import type { ICartesianTickDataOpt, ITickData } from '../../type'; import { convertDomainToTickData, getCartesianLabelBounds, isAxisHorizontal } from '../util'; @@ -13,12 +13,15 @@ const getOneDimensionalLabelBounds = ( isHorizontal: boolean ): OneDimensionalBounds[] => { const labelBoundsList = getCartesianLabelBounds(scale, domain, op); - return labelBoundsList.map(bounds => { - if (isHorizontal) { - return [bounds.x1, bounds.x2, bounds.width()]; - } - return [bounds.y1, bounds.y2, bounds.height()]; - }); + return ( + labelBoundsList && + labelBoundsList.map(bounds => { + if (isHorizontal) { + return [bounds.x1, bounds.x2, bounds.width()]; + } + return [bounds.y1, bounds.y2, bounds.height()]; + }) + ); }; /** 判断两个 bounds 是否有重叠情况 */ @@ -79,63 +82,71 @@ export const linearDiscreteTicks = (scale: BandScale, op: ICartesianTickDataOpt) const rangeEnd = maxInArray(range); if (domain.length <= rangeSize / fontSize) { - const incrementUnit = (rangeEnd - rangeStart) / domain.length; const labelBoundsList = getOneDimensionalLabelBounds(scale, domain, op, isHorizontal); - const minBoundsLength = Math.min(...labelBoundsList.map(bounds => bounds[2])); - - const stepResult = getStep( - domain, - labelBoundsList, - labelGap, - op.labelLastVisible, - Math.floor(minBoundsLength / incrementUnit), // 给step赋上合适的初值,有效改善外层循环次数 - false - ); - - scaleTicks = (scale as BandScale).stepTicks(stepResult.step); - if (op.labelLastVisible) { - if (stepResult.delCount) { - scaleTicks = scaleTicks.slice(0, scaleTicks.length - stepResult.delCount); + + if (labelBoundsList) { + const minBoundsLength = Math.min(...labelBoundsList.map(bounds => bounds[2])); + + const incrementUnit = (rangeEnd - rangeStart) / domain.length; + const stepResult = getStep( + domain, + labelBoundsList, + labelGap, + op.labelLastVisible, + Math.floor(minBoundsLength / incrementUnit), // 给step赋上合适的初值,有效改善外层循环次数 + false + ); + + scaleTicks = (scale as BandScale).stepTicks(stepResult.step); + if (op.labelLastVisible) { + if (stepResult.delCount) { + scaleTicks = scaleTicks.slice(0, scaleTicks.length - stepResult.delCount); + } + scaleTicks.push(domain[domain.length - 1]); } - scaleTicks.push(domain[domain.length - 1]); } } else { // only check first middle last, use the max size to sampling const tempDomain = [domain[0], domain[Math.floor(domain.length / 2)], domain[domain.length - 1]]; const tempList = getOneDimensionalLabelBounds(scale, tempDomain, op, isHorizontal); - let maxBounds: OneDimensionalBounds = null; - tempList.forEach(current => { - if (!maxBounds) { - maxBounds = current; - return; - } - if (maxBounds[2] < current[2]) { - maxBounds = current; - } - }); - const step = - rangeEnd - rangeStart - labelGap > 0 - ? Math.ceil((domain.length * (labelGap + maxBounds[2])) / (rangeEnd - rangeStart - labelGap)) - : domain.length - 1; + if (tempList) { + let maxBounds: OneDimensionalBounds = null; + tempList.forEach(current => { + if (!maxBounds) { + maxBounds = current; + return; + } + if (maxBounds[2] < current[2]) { + maxBounds = current; + } + }); + + const step = + rangeEnd - rangeStart - labelGap > 0 + ? Math.ceil((domain.length * (labelGap + maxBounds[2])) / (rangeEnd - rangeStart - labelGap)) + : domain.length - 1; - scaleTicks = (scale as BandScale).stepTicks(step); + scaleTicks = (scale as BandScale).stepTicks(step); - if ( - op.labelLastVisible && - (!scaleTicks.length || scaleTicks[scaleTicks.length - 1] !== domain[domain.length - 1]) - ) { if ( - scaleTicks.length && - Math.abs(scale.scale(scaleTicks[scaleTicks.length - 1]) - scale.scale(domain[domain.length - 1])) < - maxBounds[2] + op.labelLastVisible && + (!scaleTicks.length || scaleTicks[scaleTicks.length - 1] !== domain[domain.length - 1]) ) { - scaleTicks = scaleTicks.slice(0, -1); + if ( + scaleTicks.length && + Math.abs(scale.scale(scaleTicks[scaleTicks.length - 1]) - scale.scale(domain[domain.length - 1])) < + maxBounds[2] + ) { + scaleTicks = scaleTicks.slice(0, -1); + } + scaleTicks.push(domain[domain.length - 1]); } - scaleTicks.push(domain[domain.length - 1]); } } - } else { + } + + if (isNil(scaleTicks)) { scaleTicks = scale.domain(); } diff --git a/packages/vrender-components/src/axis/tick-data/util.ts b/packages/vrender-components/src/axis/tick-data/util.ts index 651d55353..f5cbbf980 100644 --- a/packages/vrender-components/src/axis/tick-data/util.ts +++ b/packages/vrender-components/src/axis/tick-data/util.ts @@ -1,5 +1,5 @@ import type { IBaseScale } from '@visactor/vscale'; -import { AABBBounds, degreeToRadian } from '@visactor/vutils'; +import { AABBBounds, degreeToRadian, isPlainObject } from '@visactor/vutils'; import type { TextAlignType, TextBaselineType } from '@visactor/vrender-core'; import { initTextMeasure } from '../../util/text'; import type { ICartesianTickDataOpt, IOrientType, ITickData } from '../type'; @@ -74,9 +74,17 @@ export const getCartesianLabelBounds = (scale: IBaseScale, domain: any[], op: IC const textMeasure = initTextMeasure(labelStyle); const range = scale.range(); - const labelBoundsList = domain.map((v: any, i: number) => { + let labelBoundsList: AABBBounds[] = []; + + for (let i = 0; i < domain.length; i++) { + const v = domain[i]; const str = labelFormatter ? labelFormatter(v) : `${v}`; + if (isPlainObject(str)) { + labelBoundsList = undefined; + break; + } + // 估算文本宽高 const { width, height } = textMeasure.quickMeasure(str); const textWidth = Math.max(width, MIN_TICK_GAP); @@ -124,8 +132,8 @@ export const getCartesianLabelBounds = (scale: IBaseScale, domain: any[], op: IC bounds.rotate(labelAngle, baseTextX, baseTextY); } - return bounds; - }); + labelBoundsList.push(bounds); + } return labelBoundsList; }; From 9d13c279fcae3f99ed05bac9a56c34e7019ca676 Mon Sep 17 00:00:00 2001 From: xile611 Date: Fri, 6 Jun 2025 11:14:22 +0800 Subject: [PATCH 2/2] docs: update changlog of rush --- ...abel-sampling-error-for-rich_2025-06-06-03-14.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json diff --git a/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json b/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json new file mode 100644 index 000000000..7139a5153 --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-label-sampling-error-for-rich_2025-06-06-03-14.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: disable sampling when label is rich text\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "dingling112@gmail.com" +} \ No newline at end of file