Skip to content

Commit

Permalink
True log scale, resolves #3348 (#3475)
Browse files Browse the repository at this point in the history
* ENH: Added normal (true) log scale.

* TST: Added test for ModifiedLog

* MAINT: ModifiedLog now inherits from Log and reuses its methods.

* Revert "MAINT: ModifiedLog now inherits from Log and reuses its methods."

This reverts commit 2d9a4b9.

* MAINT: Use D3 logScale

* BUG: Default extend for log scale should not start at zero.

* TST: Added test for Log scale

* TST: Updated Plottable.Scales.Log test.

* TST: Switched from **-operator to Math.pow
  • Loading branch information
pastewka authored and themadcreator committed Feb 4, 2019
1 parent e9fb12f commit 4b97af0
Show file tree
Hide file tree
Showing 5 changed files with 388 additions and 0 deletions.
44 changes: 44 additions & 0 deletions quicktests/overlaying/tests/basic/log_scale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
function makeData() {
"use strict";

var exponent = 3;
var data = [];

for (var i = 0; i < 10; i++) {
var x = Math.pow(3, i)/100;
data.push({x: x, y: Math.pow(x, exponent)});
}

return data;
}

function run(div, data, Plottable) {
"use strict";

var xScale = new Plottable.Scales.Log();
var yScale = new Plottable.Scales.Log();
var xAxis = new Plottable.Axes.Numeric(xScale, "bottom")
.formatter(Plottable.Formatters.siSuffix());
var yAxis = new Plottable.Axes.Numeric(yScale, "left")
.formatter(Plottable.Formatters.siSuffix());

var plot = new Plottable.Plots.Scatter()
.renderer("svg")
.deferredRendering(true)
.addDataset(new Plottable.Dataset(data))
.labelsEnabled(true)
.x((d) => d.x, xScale)
.y((d) => d.y, yScale);

var table = new Plottable.Components.Table([
[yAxis, plot],
[null, xAxis]
]);

var panZoom = new Plottable.Interactions.PanZoom(xScale, yScale).attachTo(plot);

table.renderTo(div);

panZoom.setMinMaxDomainValuesTo(xScale);
panZoom.setMinMaxDomainValuesTo(yScale);
}
42 changes: 42 additions & 0 deletions quicktests/overlaying/tests/basic/modified_log_scale.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function makeData() {
"use strict";

var exponent = 3;
var data = [];

for (var i = 0; i < 10; i++) {
var x = Math.pow(3, i)/100;
data.push({x: x, y: Math.pow(x, exponent)});
}

return data;
}

function run(div, data, Plottable) {
"use strict";

var xScale = new Plottable.Scales.ModifiedLog();
var yScale = new Plottable.Scales.ModifiedLog();
var xAxis = new Plottable.Axes.Numeric(xScale, "bottom");
var yAxis = new Plottable.Axes.Numeric(yScale, "left");

var plot = new Plottable.Plots.Scatter()
.renderer("svg")
.deferredRendering(true)
.addDataset(new Plottable.Dataset(data))
.labelsEnabled(true)
.x((d) => d.x, xScale)
.y((d) => d.y, yScale);

var table = new Plottable.Components.Table([
[yAxis, plot],
[null, xAxis]
]);

var panZoom = new Plottable.Interactions.PanZoom(xScale, yScale).attachTo(plot);

table.renderTo(div);

panZoom.setMinMaxDomainValuesTo(xScale);
panZoom.setMinMaxDomainValuesTo(yScale);
}
1 change: 1 addition & 0 deletions src/scales/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./categoryScale";
export * from "./colorScale";
export * from "./interpolatedColorScale";
export * from "./linearScale";
export * from "./logScale";
export * from "./modifiedLogScale";
export * from "./timeScale";

Expand Down
92 changes: 92 additions & 0 deletions src/scales/logScale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright 2014-present Palantir Technologies
* @license MIT
*/

import * as d3 from "d3";

import { QuantitativeScale } from "./quantitativeScale";

export class Log extends QuantitativeScale<number> {
private _d3Scale: d3.ScaleLogarithmic<number, number>;

/**
* @constructor
*/
constructor(base = 10) {
super();
this._d3Scale = d3.scaleLog().base(base);
this._setDomain(this._defaultExtent());
}

protected _defaultExtent(): number[] {
return [1, this._d3Scale.base()];
}

protected _expandSingleValueDomain(singleValueDomain: number[]) {
if (singleValueDomain[0] === singleValueDomain[1]) {
return [singleValueDomain[0]/this._d3Scale.base(),
singleValueDomain[1]*this._d3Scale.base()];
}
return singleValueDomain;
}

public scale(value: number) {
return this._d3Scale(value);
}

public scaleTransformation(value: number) {
return this.scale(value);
}

public invertedTransformation(value: number) {
return this.invert(value);
}

public getTransformationExtent() {
return this._getUnboundedExtent(true) as [number, number];
}

public getTransformationDomain() {
return this.domain() as [number, number];
}

public setTransformationDomain(domain: [number, number]) {
this.domain(domain);
}

protected _getDomain() {
return this._backingScaleDomain();
}

protected _backingScaleDomain(): number[]
protected _backingScaleDomain(values: number[]): this
protected _backingScaleDomain(values?: number[]): any {
if (values == null) {
return this._d3Scale.domain();
} else {
this._d3Scale.domain(values);
return this;
}
}

protected _getRange() {
return this._d3Scale.range();
}

protected _setRange(values: number[]) {
this._d3Scale.range(values);
}

public invert(value: number) {
return this._d3Scale.invert(value);
}

public defaultTicks(): number[] {
return this._d3Scale.ticks(Log._DEFAULT_NUM_TICKS);
}

protected _niceDomain(domain: number[], count?: number): number[] {
return this._d3Scale.copy().domain(domain).nice().domain();
}
}
209 changes: 209 additions & 0 deletions test/scales/logScaleTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { assert } from "chai";

import * as Plottable from "../../src";

describe("Scales", () => {
describe("Log Scale", () => {

describe("Basic Usage", () => {
let scale: Plottable.Scales.Log;
const base = 10;
const epsilon = 0.00001;

beforeEach(() => {
scale = new Plottable.Scales.Log(base);
});

it("has log() behavior", () => {
[1e-45, 0.000232, 0.1, 1, 10, 100, 23103.4, 1e+45].forEach((x) => {
assert.closeTo(scale.scale(x), Math.log(x) / Math.log(base), epsilon);
});
});

it("ensures x = invert(scale(x))", () => {
[1, base, 100, 0.001, base - 0.001].forEach((x) => {
assert.closeTo(x, scale.invert(scale.scale(x)), epsilon);
assert.closeTo(x, scale.scale(scale.invert(x)), epsilon);
});
});

it("defaults to the [1, base] domain", () => {
assert.deepEqual(scale.domain(), [1, base], "default domain is [1, base]");
});

it("can be padded", () => {
scale.addIncludedValuesProvider(() => [base/10, 10*base]);
scale.padProportion(0);
const unpaddedDomain = scale.domain();
scale.padProportion(0.1);
assert.operator(scale.domain()[0], "<", unpaddedDomain[0], "left side of domain has been padded");
assert.operator(unpaddedDomain[1], "<", scale.domain()[1], "right side of domain has been padded");
});

it("can have a reversed domain", () => {
scale.domain([20, 10]);
scale.range([400, 500]);
assert.strictEqual(scale.scale(10), 500, "first value in flipped domain maps to first value in range");
assert.strictEqual(scale.scale(20), 400, "last value in flipped domain maps to last value in range");

assert.strictEqual(scale.invert(400), 20, "first value in range maps to first value in flipped domain");
assert.strictEqual(scale.invert(500), 10, "last value in range maps to last value in flipped domain");
});

});

describe("Scale bases", () => {
it("uses 10 as the default base", () => {
const scale = new Plottable.Scales.Log();
scale.range([0, 1]);
assert.strictEqual(scale.scale(10), 1, "10 is base");
assert.strictEqual(scale.scale(100), 2, "10^2 will result in a double value compared to the base");
});

it("can scale values using base 2", () => {
const scale = new Plottable.Scales.Log(2);
scale.domain([1, 16]);
scale.range([0, 1]);

assert.strictEqual(scale.scale(2), 0.25, "scales base");
assert.strictEqual(scale.scale(4), 0.5, "scales other values");
assert.strictEqual(scale.scale(16), 1, "scales maximum value");
assert.strictEqual(scale.scale(256), 2, "scales values outside the domain");

assert.strictEqual(scale.invert(1), 16, "inverts maximum value");
});
});

describe("Auto Domaining", () => {
let scale: Plottable.Scales.Log;
const base = 10;

beforeEach(() => {
scale = new Plottable.Scales.Log(base);
scale.padProportion(0);
});

it("expands single value domains to [value / base, value * base].sort()", () => {
const singleValue = 15;
scale.addIncludedValuesProvider(() => [singleValue]);
assert.deepEqual(scale.domain(), [singleValue / base, singleValue * base],
"positive single-value extent was expanded to [value / base, value * base]");
});

it("doesn't lock up if a zero-width domain is set while there are value providers", () => {
scale.padProportion(0.1);
const provider = () => [1, 10];
scale.addIncludedValuesProvider(provider);
scale.autoDomain();
const originalAutoDomain = scale.domain();

scale.domain([0, 0]);
scale.autoDomain();

assert.deepEqual(scale.domain(), originalAutoDomain, "autodomained as expected");
});

it("can force the minimum of the domain with domainMin()", () => {
const requestedDomain = [1, 5];
scale.addIncludedValuesProvider(() => requestedDomain);

const minBelowBottom = 0.1;
assert.strictEqual(scale.domainMin(minBelowBottom), scale, "the scale is returned by the setter");
assert.strictEqual(scale.domainMin(), minBelowBottom, "can get the domainMin()");
assert.deepEqual(scale.domain(), [minBelowBottom, requestedDomain[1]], "lower end of domain was set by domainMin()");

const minInMiddle = (1 + 5) / 2;
scale.domainMin(minInMiddle);
assert.deepEqual(scale.domain(), [minInMiddle, requestedDomain[1]],
"lower end was set even if requested value cuts off some data");

scale.autoDomain();
assert.deepEqual(scale.domain(), requestedDomain, "calling autoDomain() overrides domainMin()");
assert.strictEqual(scale.domainMin(), scale.domain()[0], "returns autoDomain()-ed min value after autoDomain()-ing");

const minEqualTop = scale.domain()[1];
scale.domainMin(minEqualTop);
assert.deepEqual(scale.domain(), [minEqualTop, minEqualTop * base],
"domain is set to [min, min * base] if the requested value is >= autoDomain()-ed max value");

scale.domainMin(minInMiddle);
const requestedDomain2 = [0.1, 10];
scale.addIncludedValuesProvider(() => requestedDomain2);
assert.deepEqual(scale.domain(), [minInMiddle, requestedDomain2[1]], "adding another ExtentsProvider doesn't change domainMin()");
});

it("can force the maximum of the domain with domainMax()", () => {
const requestedDomain = [1, 5];
scale.addIncludedValuesProvider(() => requestedDomain);

const maxAboveTop = 10;
assert.strictEqual(scale.domainMax(maxAboveTop), scale, "the scale is returned by the setter");
assert.strictEqual(scale.domainMax(), maxAboveTop, "can get the domainMax()");
assert.deepEqual(scale.domain(), [requestedDomain[0], maxAboveTop], "upper end of domain was set by domainMax()");

const maxInMiddle = (1 + 5) / 2;
scale.domainMax(maxInMiddle);
assert.deepEqual(scale.domain(), [requestedDomain[0], maxInMiddle],
"upper end was set even if requested value cuts off some data");

scale.autoDomain();
assert.deepEqual(scale.domain(), requestedDomain, "calling autoDomain() overrides domainMax()");
assert.strictEqual(scale.domainMax(), scale.domain()[1], "returns autoDomain()-ed max value after autoDomain()-ing");

const maxEqualBottom = scale.domain()[0];
scale.domainMax(maxEqualBottom);
assert.deepEqual(scale.domain(), [maxEqualBottom / base, maxEqualBottom],
"domain is set to [max / base, max] if the requested value is <= autoDomain()-ed min value and negative");

scale.domainMax(maxInMiddle);
const requestedDomain2 = [1, 10];
scale.addIncludedValuesProvider(() => requestedDomain2);
assert.deepEqual(scale.domain(), [requestedDomain2[0], maxInMiddle], "adding another ExtentsProvider doesn't change domainMax()");
});

it("can force the domain by using domainMin() and domainMax() together", () => {
const requestedDomain = [1, 5];
scale.addIncludedValuesProvider(() => requestedDomain);

const desiredMin = 0.1;
const desiredMax = 10;
scale.domainMin(desiredMin);
scale.domainMax(desiredMax);
assert.deepEqual(scale.domain(), [desiredMin, desiredMax], "setting domainMin() and domainMax() sets the domain");

scale.autoDomain();
const bigMin = 10;
const smallMax = 0.1;
scale.domainMin(bigMin);
scale.domainMax(smallMax);
assert.deepEqual(scale.domain(), [bigMin, smallMax], "setting both is allowed even if it reverse the domain");
});
});

describe("Ticks", () => {

let scale: Plottable.Scales.Log;
const base = 10;

beforeEach(() => {
scale = new Plottable.Scales.Log(base);
});

it("gives reasonable values for ticks()", () => {
const includedValuesProvider = () => [base / 4, base / 2];
scale.addIncludedValuesProvider(includedValuesProvider);

const ticks = scale.ticks();
assert.operator(ticks.length, ">", 0, "there should be some ticks generated");
});

it("always has more than 2 ticks", () => {
[null, [2, 9], [1, 2], [0.001, 0.01]].forEach((domain) => {
scale.domain(domain);
const ticks = scale.ticks();
assert.operator(ticks.length, ">=", 2, "there should be at least 2 ticks in domain " + domain);
});
});
});
});
});

1 comment on commit 4b97af0

@blueprint-bot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True log scale, resolves #3348 (#3475)

Demo: quicktests | fiddle

Please sign in to comment.