Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 35 additions & 57 deletions src/util/jitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ export function needFixJitter(seriesModel: SeriesModel, axis: Axis): boolean {
export type JitterData = {
fixedCoord: number,
floatCoord: number,
r: number,
next: JitterData | null,
prev: JitterData | null
r: number
};

const inner = makeInner<{ items: JitterData }, Axis2D | SingleAxis>();
const inner = makeInner<{ items: JitterData[] }, Axis2D | SingleAxis>();

/**
* Fix jitter for overlapping data points.
Expand Down Expand Up @@ -92,94 +90,74 @@ function fixJitterAvoidOverlaps(
): number {
const store = inner(fixedAxis);
if (!store.items) {
store.items = {
fixedCoord: -1,
floatCoord: -1,
r: -1,
next: null,
prev: null
};
store.items.next = store.items;
store.items.prev = store.items;
store.items = [];
}
const items = store.items;

// Try both positive and negative directions, choose the one with smaller movement
const overlapA = placeJitterOnDirection(items, fixedCoord, floatCoord, radius, jitter, margin, 1);
const overlapB = placeJitterOnDirection(items, fixedCoord, floatCoord, radius, jitter, margin, -1);
const overlapResult = Math.abs(overlapA.resultCoord - floatCoord) < Math.abs(overlapB.resultCoord - floatCoord)
? overlapA : overlapB;
let minFloat = overlapResult.resultCoord;
const minFloat = Math.abs(overlapA - floatCoord) < Math.abs(overlapB - floatCoord) ? overlapA : overlapB;

// Clamp only category axis
const bandWidth = fixedAxis.scale.type === 'ordinal'
? fixedAxis.getBandWidth()
: null;
const distance = Math.abs(minFloat - floatCoord);
if (distance > jitter / 2
|| (bandWidth && distance > bandWidth / 2 - radius)
) {

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.
minFloat = fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius);
return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius);
}

// Insert to store
const insertBy = overlapResult.insertBy;
const resultDirection = overlapResult.direction;
const pointer1 = resultDirection > 0 ? 'next' : 'prev';
const pointer2 = resultDirection > 0 ? 'prev' : 'next';
const newItem: JitterData = {
// Add new point to array
items.push({
fixedCoord: fixedCoord,
floatCoord: overlapResult.resultCoord,
r: radius,
next: null,
prev: null
};
newItem[pointer1] = insertBy[pointer1];
newItem[pointer2] = insertBy;
insertBy[pointer1][pointer2] = newItem;
insertBy[pointer1] = newItem;
floatCoord: minFloat,
r: radius
});

return minFloat;
}

function placeJitterOnDirection(
items: JitterData,
items: JitterData[],
fixedCoord: number,
floatCoord: number,
radius: number,
jitter: number,
margin: number,
direction: 1 | -1
): {
resultCoord: number;
insertBy: JitterData;
direction: 1 | -1;
} {
// Check for overlap with previous items.
): number {
let y = floatCoord;
const pointer1 = direction > 0 ? 'next' : 'prev';
let insertBy = items;
let item = items[pointer1];

while (item !== items) {
// Check all existing items for overlap and find the maximum adjustment needed
for (let i = 0; i < items.length; i++) {
const item = items[i];
const dx = fixedCoord - item.fixedCoord;
const dy = y - item.floatCoord;
const d2 = dx * dx + dy * dy;
const r = radius + item.r + margin;

if (d2 < r * r) {
// Overlap. Try to move the new item along otherCoord direction.
y = item.floatCoord + Math.sqrt(r * r - dx * dx) * direction;
insertBy = item;

if (Math.abs(y - floatCoord) > jitter / 2) {
// If the new item is moved too far, then give up.
// Fall back to random jitter.
return {resultCoord: Number.MAX_VALUE, insertBy, direction};
// Has overlap, calculate required adjustment
const requiredY = item.floatCoord + Math.sqrt(r * r - dx * dx) * direction;

// Check if this adjustment would move too far
if (Math.abs(requiredY - floatCoord) > jitter / 2) {
return Number.MAX_VALUE; // Give up
}
}

item = item[pointer1];
// Update y only when it's larger to the center
if (direction === 1 && requiredY > y || direction === -1 && requiredY < y) {
y = requiredY;
// Loop from the start again
i = -1; // Reset index to recheck all items
continue; // Recalculate with the new y position
}
}
}

return {resultCoord: y, insertBy, direction};
return y;
}
8 changes: 8 additions & 0 deletions test/runTest/marks/scatter-jitter.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions test/scatter-jitter.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.