blob: 42690c1dddce7f10956e40dc7bda8e442e8aa104 [file]
/*
* 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 type Axis from '../coord/Axis';
import type { AxisBaseModel } from '../coord/AxisBaseModel';
import Axis2D from '../coord/cartesian/Axis2D';
import type SingleAxis from '../coord/single/SingleAxis';
import type SeriesModel from '../model/Series';
import { makeInner } from './model';
export function needFixJitter(seriesModel: SeriesModel, axis: Axis): boolean {
const coordinateSystem = seriesModel.coordinateSystem;
const coordType = coordinateSystem && coordinateSystem.type;
const baseAxis = coordinateSystem && coordinateSystem.getBaseAxis && coordinateSystem.getBaseAxis();
const scaleType = baseAxis && baseAxis.scale && baseAxis.scale.type;
const seriesValid = coordType === 'cartesian2d' && scaleType === 'ordinal'
|| coordType === 'single';
const axisValid = (axis.model as AxisBaseModel).get('jitter') > 0;
return seriesValid && axisValid;
}
export type JitterData = {
fixedCoord: number,
floatCoord: number,
r: number
};
const inner = makeInner<{ items: JitterData[] }, Axis2D | SingleAxis>();
/**
* Fix jitter for overlapping data points.
*
* @param fixedAxis The axis whose coord doesn't change with jitter.
* @param fixedCoord The coord of fixedAxis.
* @param floatCoord The coord of the other axis, which should be changed with jittering.
* @param radius The radius of the data point, considering the symbol is a circle.
* @returns updated floatCoord.
*/
export function fixJitter(
fixedAxis: Axis2D | SingleAxis,
fixedCoord: number,
floatCoord: number,
radius: number
): number {
if (fixedAxis instanceof Axis2D) {
const scaleType = fixedAxis.scale.type;
if (scaleType !== 'category' && scaleType !== 'ordinal') {
return floatCoord;
}
}
const axisModel = fixedAxis.model as AxisBaseModel;
const jitter = axisModel.get('jitter');
const jitterOverlap = axisModel.get('jitterOverlap');
const jitterMargin = axisModel.get('jitterMargin') || 0;
// Get band width to limit jitter range
const bandWidth = fixedAxis.scale.type === 'ordinal'
? fixedAxis.getBandWidth()
: null;
if (jitter > 0) {
if (jitterOverlap) {
return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius);
}
else {
return fixJitterAvoidOverlaps(fixedAxis, fixedCoord, floatCoord, radius, jitter, jitterMargin);
}
}
return floatCoord;
}
function fixJitterIgnoreOverlaps(
floatCoord: number,
jitter: number,
bandWidth: number | null,
radius: number
): number {
// Don't clamp single axis
if (bandWidth === null) {
return floatCoord + (Math.random() - 0.5) * jitter;
}
const maxJitter = bandWidth - radius * 2;
const actualJitter = Math.min(Math.max(0, jitter), maxJitter);
return floatCoord + (Math.random() - 0.5) * actualJitter;
}
function fixJitterAvoidOverlaps(
fixedAxis: Axis2D | SingleAxis,
fixedCoord: number,
floatCoord: number,
radius: number,
jitter: number,
margin: number
): number {
const store = inner(fixedAxis);
if (!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 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 the new item is moved too far, then give up.
// Fall back to random jitter.
return fixJitterIgnoreOverlaps(floatCoord, jitter, bandWidth, radius);
}
// Add new point to array
items.push({
fixedCoord: fixedCoord,
floatCoord: minFloat,
r: radius
});
return minFloat;
}
function placeJitterOnDirection(
items: JitterData[],
fixedCoord: number,
floatCoord: number,
radius: number,
jitter: number,
margin: number,
direction: 1 | -1
): number {
let y = floatCoord;
// 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) {
// 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
}
// 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 y;
}