chartjs / chartjs/Chart.js

Symmetric logarithmic (symlog) scale

Open
#11,919 1 comment 2 reactions 0 assignees View on GitHub
type: enhancement
Dominant language
JavaScript
Stars
67.7k
Forks
11.9k
Avg merge
7h 39m
Merged PRs (30d)
5

Description

### Feature Proposal

In a project that used Chart.js I came across a situation where we needed a logarithmic scale, but with the ability to display negative values ​​as well. Unfortunately, the library does not provide such an option, and I think it can be useful in many cases.

### Possible Implementation

In our case this was solved by creating a symmetric logarithmic scale.
```
import { BubbleDataPoint, Chart, ChartTypeRegistry, Point, Scale } from 'chart.js';

const constant = 1;

const symlogTransform = (value: number) => {
return Math.sign(value) * Math.log10(Math.abs(value) / constant + 1);
};

const symlogInverseTransform = (value: number) => {
return Math.sign(value) * (Math.pow(10, Math.abs(value)) - 1) * constant;
};

export class SymlogScale extends Scale {
static id = 'symlog';
private _startValue!: number;
private _valueRange!: number;

constructor(cfg: {
id: string;
type: string;
ctx: CanvasRenderingContext2D;
chart: Chart;
}) {
super(cfg);
}

parse(raw: unknown, index: number | undefined) {
const value = super.parse(raw, index) as number;
return symlogTransform(value);
}

determineDataLimits() {
const { min, max } = this.getMinMax(true);
this.min = symlogTransform(min);
this.max = symlogTransform(max);
this._startValue = this.min;
this._valueRange = this.max - this.min;
}

getPixelForValue(value: number) {
const symlogValue = symlogTransform(value);
const decimal = (symlogValue - this._startValue) / this._valueRange;
return this.getPixelForDecimal(decimal);
}

getLabelForValue(value: number) {
return symlogInverseTransform(value).toLocaleString();
}

minimumButNotZero(alwaysNotZero: number, other: number) {
if (!other || other === 0) {
return alwaysNotZero;
} else {
return Math.min(alwaysNotZero, other);
}
}

minDecimalPlaces(numbers: number[]) {
if (!numbers || numbers.length == 0) {
return 0;
}
numbers.sort((a, b) => a - b);

let smallest = this.minimumButNotZero(Infinity, Math.abs(numbers[0]));
for (let i = 0; i < numbers.length - 1; i++) {
smallest = this.minimumButNotZero(smallest, numbers[i + 1] - numbers[i]);
smallest = this.minimumButNotZero(smallest, Math.abs(numbers[i + 1]));
}

return Math.max(0, Math.ceil(-Math.log10(smallest)));
}

generateTickLabels(ticks: any[]) {
const minimalDecimalPlaces = this.minDecimalPlaces(ticks.map(tick => tick.value));

ticks.forEach((tick: { label: string; value: number }) => {
tick.label = parseFloat(symlogInverseTransform(tick.value).toFixed(minimalDecimalPlaces)).toString();
});
}

buildTicks() {
const ticks = [];
const tickCount = 11;
const min = symlogInverseTransform(this.min);
const max = symlogInverseTransform(this.max);
const range = max - min;
const stepSize = range / (tickCount - 1);
for (let i = min; i <= max; i += stepSize) {
ticks.push({ value: i });
}
return ticks;
}
}
```

![showcase](https://github.com/user-attachments/assets/8297efff-a84b-47b6-9149-b487420170f3)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.