This is an automated email from the ASF dual-hosted git repository. plainheart pushed a commit to branch jitter-rng in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 20b64c18ac1010f9e9873401af52096c1dd83424 Author: plainheart <[email protected]> AuthorDate: Wed Sep 16 17:15:13 2026 +0800 feat(scatter): add `jitterRng` / `jitterSeed` option to configure a custom random number generator or a random seed for deterministic jitter layout --- src/chart/scatter/jitterLayout.ts | 33 +++++++++++++++++++++++++++++++-- src/coord/axisCommonTypes.ts | 2 ++ src/util/jitter.ts | 19 +++++++++++-------- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/chart/scatter/jitterLayout.ts b/src/chart/scatter/jitterLayout.ts index a68ea6550..7dc332762 100644 --- a/src/chart/scatter/jitterLayout.ts +++ b/src/chart/scatter/jitterLayout.ts @@ -26,6 +26,26 @@ import createRenderPlanner from '../helper/createRenderPlanner'; import { COORD_SYS_TYPE_CARTESIAN_2D } from '../../coord/cartesian/GridModel'; import { COORD_SYS_TYPE_SINGLE } from '../../coord/single/AxisModel'; import { validateUpstreamOutputRange } from '../../util/model'; +import type { AxisBaseModel } from '../../coord/AxisBaseModel'; +import { isString, reduce } from 'zrender/src/core/util'; + +const DEFAULT_JITTER_SEED = 'echarts-jitter'; +const DEFAULT_JITTER_RNG = createRNG(DEFAULT_JITTER_SEED); + +/** + * Mulberry32 RNG + */ +function createRNG(seed: string | number) { + let state = isString(seed) + ? reduce(seed.split(''), (h, c) => Math.imul(31, h) + c.charCodeAt(0) | 0, 0) + : (Number(seed) | 0); + return function () { + let t = state += 0x6D2B79F5; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} export default function jitterLayout(): StageHandler { return { @@ -58,6 +78,15 @@ export default function jitterLayout(): StageHandler { return; } + const baseAxisModel = baseAxis.model as AxisBaseModel; + let jitterRng = baseAxisModel.get('jitterRng'); + if (!jitterRng) { + const jitterSeed = baseAxisModel.get('jitterSeed'); + + jitterRng = jitterSeed == null || jitterSeed === DEFAULT_JITTER_SEED + ? DEFAULT_JITTER_RNG + : createRNG(jitterSeed); + } return { progress(params, data): void { const points = data.getLayout('points') as Float32Array; @@ -79,7 +108,7 @@ export default function jitterLayout(): StageHandler { if (jitterOnY) { // x is fixed, and y is floating - const jittered = fixJitter(baseAxis, layout[0], layout[1], size / 2); + const jittered = fixJitter(baseAxis, layout[0], layout[1], size / 2, jitterRng); if (hasPoints) { points[offset + 1] = jittered; } @@ -89,7 +118,7 @@ export default function jitterLayout(): StageHandler { } else if (jitterOnX) { // y is fixed, and x is floating - const jittered = fixJitter(baseAxis, layout[1], layout[0], size / 2); + const jittered = fixJitter(baseAxis, layout[1], layout[0], size / 2, jitterRng); if (hasPoints) { points[offset] = jittered; } diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index 058dea900..89199a529 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -128,6 +128,8 @@ export interface AxisBaseOptionCommon extends ComponentOption, jitter?: number; jitterOverlap?: boolean; jitterMargin?: number; + jitterSeed?: string | number; + jitterRng?: () => number; breaks?: AxisBreakOption[]; breakArea?: { diff --git a/src/util/jitter.ts b/src/util/jitter.ts index c0e1e394e..c68183189 100644 --- a/src/util/jitter.ts +++ b/src/util/jitter.ts @@ -61,7 +61,8 @@ export function fixJitter( fixedAxis: Axis2D | SingleAxis, fixedCoord: number, floatCoord: number, - radius: number + radius: number, + rng: () => number ): number { if (fixedAxis instanceof Axis2D) { const scaleType = fixedAxis.scale.type; @@ -81,24 +82,25 @@ export function fixJitter( ? calcBandWidth(fixedAxis).w : null; if (jitterOverlap) { - return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius); + return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius, rng); } - return fixJitterAvoidOverlaps(fixedAxis, fixedCoord, floatCoord, radius, jitter, jitterMargin); + return fixJitterAvoidOverlaps(fixedAxis, fixedCoord, floatCoord, radius, jitter, jitterMargin, rng); } function fixJitterIgnoreOverlaps( floatCoord: number, jitter: number, bandWidth: number | null, - radius: number + radius: number, + rng: () => number ): number { // Don't clamp single axis if (bandWidth === null) { - return floatCoord + (Math.random() - 0.5) * jitter; + return floatCoord + (rng() - 0.5) * jitter; } const maxJitter = bandWidth - radius * 2; const actualJitter = Math.min(Math.max(0, jitter), maxJitter); - return floatCoord + (Math.random() - 0.5) * actualJitter; + return floatCoord + (rng() - 0.5) * actualJitter; } function fixJitterAvoidOverlaps( @@ -107,7 +109,8 @@ function fixJitterAvoidOverlaps( floatCoord: number, radius: number, jitter: number, - margin: number + margin: number, + rng: () => number ): number { const store = inner(fixedAxis); if (!store.items) { @@ -129,7 +132,7 @@ function fixJitterAvoidOverlaps( if (distance > jitter / 2 || (bandWidth && distance > bandWidth / 2 - radius)) { // If the new item is moved too far, then give up. // Fall back to random jitter. - return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius); + return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius, rng); } // Add new point to array --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
