Skip to content

Commit f7679a8

Browse files
committed
fix: limit log scale domain
Limit the log scale limit and computed values to 1 or -1 depending on original domain. fix #21
1 parent 8359414 commit f7679a8

File tree

6 files changed

+148
-8
lines changed

6 files changed

+148
-8
lines changed

src/lib/series/rendering.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { area, line } from 'd3-shape';
22
import { DEFAULT_THEME } from '../themes/theme';
33
import { SpecId } from '../utils/ids';
4-
import { Scale } from '../utils/scales/scales';
4+
import { Scale, ScaleType } from '../utils/scales/scales';
55
import { CurveType, getCurveFactory } from './curves';
66
import { LegendItem } from './legend';
77
import { DataSeriesDatum } from './series';
@@ -99,11 +99,28 @@ export function renderBars(
9999
seriesKey: any[],
100100
): BarGeometry[] {
101101
return dataset.map((datum, i) => {
102+
const { x, y0, y1 } = datum;
103+
let height = 0;
104+
let y = 0;
105+
if (yScale.type === ScaleType.Log) {
106+
y = y1 === 0 ? yScale.range[0] : yScale.scale(y1);
107+
let y0Scaled;
108+
if (yScale.isInverted) {
109+
y0Scaled = y0 === 0 ? yScale.range[1] : yScale.scale(y0);
110+
} else {
111+
y0Scaled = y0 === 0 ? yScale.range[0] : yScale.scale(y0);
112+
}
113+
height = y0Scaled - y;
114+
} else {
115+
y = yScale.scale(y1);
116+
height = yScale.scale(y0) - y;
117+
}
118+
102119
return {
103-
x: xScale.scale(datum.x) + xScale.bandwidth * orderIndex,
104-
y: yScale.scale(datum.y1), // top most value
120+
x: xScale.scale(x) + xScale.bandwidth * orderIndex,
121+
y, // top most value
105122
width: xScale.bandwidth,
106-
height: yScale.scale(datum.y0) - yScale.scale(datum.y1),
123+
height,
107124
color,
108125
value: {
109126
specId,

src/lib/utils/scales/scale_band.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class ScaleBand implements Scale {
88
readonly type: ScaleType;
99
readonly domain: any[];
1010
readonly range: number[];
11+
readonly isInverted: boolean;
1112
private readonly d3Scale: any;
1213

1314
constructor(
@@ -35,6 +36,7 @@ export class ScaleBand implements Scale {
3536
if (overrideBandwidth) {
3637
this.bandwidth = overrideBandwidth;
3738
}
39+
this.isInverted = this.domain[0] > this.domain[1];
3840
}
3941

4042
scale(value: any) {

src/lib/utils/scales/scale_continuous.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,64 @@ const SCALES = {
1010
[ScaleType.Time]: scaleTime,
1111
};
1212

13+
export function limitToMin(value: number, positive: boolean) {
14+
if (value === 0) {
15+
return positive ? 1 : -1;
16+
}
17+
return value;
18+
}
19+
/**
20+
* As log(0) = -Infinite, a log scale domain must be strictly-positive
21+
* or strictly-negative; the domain must not include or cross zero value.
22+
* We need to limit the domain scale to the right value on all possible cases.
23+
* @param domain the domain to limit
24+
*/
25+
export function limitLogScaleDomain(domain: any[]) {
26+
if (domain[0] === 0) {
27+
if (domain[1] > 0) {
28+
return [1, domain[1]];
29+
} else if (domain[1] < 0) {
30+
return [-1, domain[1]];
31+
} else {
32+
return [1, 1];
33+
}
34+
}
35+
if (domain[1] === 0) {
36+
if (domain[0] > 0) {
37+
return [domain[0], 1];
38+
} else if (domain[0] < 0) {
39+
return [domain[0], -1];
40+
} else {
41+
return [1, 1];
42+
}
43+
}
44+
if (domain[0] < 0 && domain[1] > 0) {
45+
const isD0Min = Math.abs(domain[1]) - Math.abs(domain[0]) >= 0;
46+
if (isD0Min) {
47+
return [1, domain[1]];
48+
} else {
49+
return [domain[0], -1];
50+
}
51+
}
52+
if (domain[0] > 0 && domain[1] < 0) {
53+
const isD0Max = Math.abs(domain[0]) - Math.abs(domain[1]) >= 0;
54+
if (isD0Max) {
55+
return [domain[0], 1];
56+
} else {
57+
return [-1, domain[1]];
58+
}
59+
}
60+
return domain;
61+
}
62+
1363
export class ScaleContinuous implements Scale {
1464
readonly bandwidth: number;
1565
readonly minInterval: number;
1666
readonly step: number;
1767
readonly type: ScaleType;
1868
readonly domain: any[];
1969
readonly range: number[];
70+
readonly isInverted: boolean;
2071
private readonly d3Scale: any;
2172

2273
constructor(
@@ -28,16 +79,22 @@ export class ScaleContinuous implements Scale {
2879
minInterval?: number,
2980
) {
3081
this.d3Scale = SCALES[type]();
31-
this.d3Scale.domain(domain);
82+
if (type === ScaleType.Log) {
83+
this.domain = limitLogScaleDomain(domain);
84+
this.d3Scale.domain(this.domain);
85+
} else {
86+
this.domain = domain;
87+
this.d3Scale.domain(domain);
88+
}
3289
this.d3Scale.range(range);
3390
this.d3Scale.clamp(clamp);
3491
// this.d3Scale.nice();
3592
this.bandwidth = bandwidth || 0;
3693
this.step = 0;
37-
this.domain = domain;
3894
this.type = type;
3995
this.range = range;
4096
this.minInterval = minInterval || 0;
97+
this.isInverted = this.domain[0] > this.domain[1];
4198
}
4299

43100
scale(value: any) {

src/lib/utils/scales/scales.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { limitLogScaleDomain } from './scale_continuous';
12
import { createContinuousScale, createOrdinalScale, ScaleType } from './scales';
23

34
describe('Scale Test', () => {
@@ -49,6 +50,19 @@ describe('Scale Test', () => {
4950
const scaledValue3 = logScale.scale(5);
5051
expect(scaledValue3).toBe((Math.log(5) / Math.log(10)) * 100);
5152
});
53+
test('Create an log scale starting with 0 as min', () => {
54+
const data = [0, 10];
55+
const minRange = 0;
56+
const maxRange = 100;
57+
const logScale = createContinuousScale(ScaleType.Log, data, minRange, maxRange);
58+
const { domain, range } = logScale;
59+
expect(domain).toEqual([1, 10]);
60+
expect(range).toEqual([minRange, maxRange]);
61+
const scaledValue1 = logScale.scale(1);
62+
expect(scaledValue1).toBe(0);
63+
const scaledValue3 = logScale.scale(5);
64+
expect(scaledValue3).toBe((Math.log(5) / Math.log(10)) * 100);
65+
});
5266
test('Create an sqrt scale', () => {
5367
const data = [0, 10];
5468
const minRange = 0;
@@ -62,4 +76,44 @@ describe('Scale Test', () => {
6276
const scaledValue3 = sqrtScale.scale(5);
6377
expect(scaledValue3).toBe((Math.sqrt(5) / Math.sqrt(10)) * 100);
6478
});
79+
test('Check log scale domain limiting', () => {
80+
let limitedDomain = limitLogScaleDomain([10, 20]);
81+
expect(limitedDomain).toEqual([10, 20]);
82+
83+
limitedDomain = limitLogScaleDomain([0, 100]);
84+
expect(limitedDomain).toEqual([1, 100]);
85+
86+
limitedDomain = limitLogScaleDomain([100, 0]);
87+
expect(limitedDomain).toEqual([100, 1]);
88+
89+
limitedDomain = limitLogScaleDomain([0, 0]);
90+
expect(limitedDomain).toEqual([1, 1]);
91+
92+
limitedDomain = limitLogScaleDomain([-100, 0]);
93+
expect(limitedDomain).toEqual([-100, -1]);
94+
95+
limitedDomain = limitLogScaleDomain([0, -100]);
96+
expect(limitedDomain).toEqual([-1, -100]);
97+
98+
limitedDomain = limitLogScaleDomain([-100, 100]);
99+
expect(limitedDomain).toEqual([1, 100]);
100+
101+
limitedDomain = limitLogScaleDomain([-100, 50]);
102+
expect(limitedDomain).toEqual([-100, -1]);
103+
104+
limitedDomain = limitLogScaleDomain([-100, 150]);
105+
expect(limitedDomain).toEqual([1, 150]);
106+
107+
limitedDomain = limitLogScaleDomain([100, -100]);
108+
expect(limitedDomain).toEqual([100, 1]);
109+
110+
limitedDomain = limitLogScaleDomain([100, -50]);
111+
expect(limitedDomain).toEqual([100, 1]);
112+
113+
limitedDomain = limitLogScaleDomain([150, -100]);
114+
expect(limitedDomain).toEqual([150, 1]);
115+
116+
limitedDomain = limitLogScaleDomain([50, -100]);
117+
expect(limitedDomain).toEqual([-1, -100]);
118+
});
65119
});

src/lib/utils/scales/scales.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Scale {
99
invert: (value: number) => any;
1010
bandwidth: number;
1111
type: ScaleType;
12+
isInverted: boolean;
1213
}
1314
export type ScaleFunction = (value: any) => number;
1415

stories/bar_chart.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ storiesOf('Bar Chart', module)
153153
</Chart>
154154
);
155155
})
156-
.add('with log y axis (TO FIX)', () => {
156+
.add('with log y axis', () => {
157157
return (
158158
<Chart renderer="canvas" className={'story-chart'}>
159159
<Axis
@@ -175,7 +175,16 @@ storiesOf('Bar Chart', module)
175175
yScaleType={ScaleType.Log}
176176
xAccessor="x"
177177
yAccessors={['y']}
178-
data={[{ x: 1, y: 2 }, { x: 2, y: 7 }, { x: 4, y: 3 }, { x: 9, y: 6 }]}
178+
data={[
179+
{ x: 1, y: 0 },
180+
{ x: 2, y: 1 },
181+
{ x: 3, y: 2 },
182+
{ x: 4, y: 3 },
183+
{ x: 5, y: 4 },
184+
{ x: 6, y: 5 },
185+
{ x: 7, y: 6 },
186+
{ x: 8, y: 7 },
187+
]}
179188
yScaleToDataExtent={true}
180189
/>
181190
</Chart>

0 commit comments

Comments
 (0)