diff --git a/plottable.d.ts b/plottable.d.ts
index ad21dcd960..886acb9313 100644
--- a/plottable.d.ts
+++ b/plottable.d.ts
@@ -35,6 +35,7 @@ declare module Plottable {
function accessorize(accessor: any): IAccessor;
function applyAccessor(accessor: IAccessor, dataSource: DataSource): (d: any, i: number) => any;
function uniq(strings: string[]): string[];
+ function uniqNumbers(a: number[]): number[];
/**
* Creates an array of length `count`, filled with value or (if value is a function), value()
*
@@ -1405,6 +1406,58 @@ declare module Plottable {
}
+declare module Plottable {
+ module Scale {
+ class ModifiedLog extends Abstract.QuantitiveScale {
+ /**
+ * Creates a new Scale.ModifiedLog.
+ *
+ * A ModifiedLog scale acts as a regular log scale for large numbers.
+ * As it approaches 0, it gradually becomes linear. This means that the
+ * scale won't freak out if you give it 0 or a negative number, where an
+ * ordinary Log scale would.
+ *
+ * However, it does mean that scale will be effectively linear as values
+ * approach 0. If you want very small values on a log scale, you should use
+ * an ordinary Scale.Log instead.
+ *
+ * @constructor
+ * @param {number} [base]
+ * The base of the log. Defaults to 10, and must be > 1.
+ *
+ * For base <= x, scale(x) = log(x).
+ *
+ * For 0 < x < base, scale(x) will become more and more
+ * linear as it approaches 0.
+ *
+ * At x == 0, scale(x) == 0.
+ *
+ * For negative values, scale(-x) = -scale(x).
+ */
+ constructor(base?: number);
+ public scale(x: number): number;
+ public invert(x: number): number;
+ public ticks(count?: number): number[];
+ public copy(): ModifiedLog;
+ /**
+ * @returns {boolean}
+ * Whether or not to return tick values other than powers of base.
+ *
+ * This defaults to false, so you'll normally only see ticks like
+ * [10, 100, 1000]. If you turn it on, you might see ticks values
+ * like [10, 50, 100, 500, 1000].
+ */
+ public showIntermediateTicks(): boolean;
+ /**
+ * @param {boolean} show
+ * Whether or not to return ticks values other than powers of the base.
+ */
+ public showIntermediateTicks(show: boolean): ModifiedLog;
+ }
+ }
+}
+
+
declare module Plottable {
module Scale {
class Ordinal extends Abstract.Scale {
diff --git a/plottable.js b/plottable.js
index 0c26ca6c10..cd05744b7e 100644
--- a/plottable.js
+++ b/plottable.js
@@ -107,6 +107,19 @@ var Plottable;
}
Methods.uniq = uniq;
+ function uniqNumbers(a) {
+ var seen = d3.set();
+ var result = [];
+ a.forEach(function (n) {
+ if (!seen.has(n)) {
+ seen.add(n);
+ result.push(n);
+ }
+ });
+ return result;
+ }
+ Methods.uniqNumbers = uniqNumbers;
+
/**
* Creates an array of length `count`, filled with value or (if value is a function), value()
*
@@ -2794,7 +2807,7 @@ var Plottable;
Scale.prototype.domain = function (values) {
if (values == null) {
- return this._d3Scale.domain();
+ return this._getDomain();
} else {
this.autoDomainAutomatically = false;
this._setDomain(values);
@@ -2802,6 +2815,10 @@ var Plottable;
}
};
+ Scale.prototype._getDomain = function () {
+ return this._d3Scale.domain();
+ };
+
Scale.prototype._setDomain = function (values) {
this._d3Scale.domain(values);
this.broadcaster.broadcast();
@@ -3488,8 +3505,8 @@ var Plottable;
// This scaling is done to account for log scales and other non-linear
// scales. A log scale should be padded more on the max than on the min.
- var newMin = scale._d3Scale.invert(scale.scale(min) - (scale.scale(max) - scale.scale(min)) * p);
- var newMax = scale._d3Scale.invert(scale.scale(max) + (scale.scale(max) - scale.scale(min)) * p);
+ var newMin = scale.invert(scale.scale(min) - (scale.scale(max) - scale.scale(min)) * p);
+ var newMax = scale.invert(scale.scale(max) + (scale.scale(max) - scale.scale(min)) * p);
var exceptionValues = this.paddingExceptions.values().concat(this.unregisteredPaddingExceptions.values());
var exceptionSet = d3.set(exceptionValues);
if (exceptionSet.has(min)) {
@@ -3542,7 +3559,7 @@ var Plottable;
*/
function QuantitiveScale(scale) {
_super.call(this, scale);
- this.lastRequestedTickCount = 10;
+ this._lastRequestedTickCount = 10;
this._PADDING_FOR_IDENTICAL_DOMAIN = 1;
this._userSetDomainer = false;
this._domainer = new Plottable.Domainer();
@@ -3619,9 +3636,9 @@ var Plottable;
*/
QuantitiveScale.prototype.ticks = function (count) {
if (count != null) {
- this.lastRequestedTickCount = count;
+ this._lastRequestedTickCount = count;
}
- return this._d3Scale.ticks(this.lastRequestedTickCount);
+ return this._d3Scale.ticks(this._lastRequestedTickCount);
};
/**
@@ -3728,6 +3745,221 @@ var Plottable;
var Scale = Plottable.Scale;
})(Plottable || (Plottable = {}));
+///
+var __extends = this.__extends || function (d, b) {
+ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ __.prototype = b.prototype;
+ d.prototype = new __();
+};
+var Plottable;
+(function (Plottable) {
+ (function (Scale) {
+ var ModifiedLog = (function (_super) {
+ __extends(ModifiedLog, _super);
+ /**
+ * Creates a new Scale.ModifiedLog.
+ *
+ * A ModifiedLog scale acts as a regular log scale for large numbers.
+ * As it approaches 0, it gradually becomes linear. This means that the
+ * scale won't freak out if you give it 0 or a negative number, where an
+ * ordinary Log scale would.
+ *
+ * However, it does mean that scale will be effectively linear as values
+ * approach 0. If you want very small values on a log scale, you should use
+ * an ordinary Scale.Log instead.
+ *
+ * @constructor
+ * @param {number} [base]
+ * The base of the log. Defaults to 10, and must be > 1.
+ *
+ * For base <= x, scale(x) = log(x).
+ *
+ * For 0 < x < base, scale(x) will become more and more
+ * linear as it approaches 0.
+ *
+ * At x == 0, scale(x) == 0.
+ *
+ * For negative values, scale(-x) = -scale(x).
+ */
+ function ModifiedLog(base) {
+ if (typeof base === "undefined") { base = 10; }
+ _super.call(this, d3.scale.linear());
+ this._showIntermediateTicks = false;
+ this.base = base;
+ this.pivot = this.base;
+ this.untransformedDomain = this._defaultExtent();
+ this._lastRequestedTickCount = 10;
+ if (base <= 1) {
+ throw new Error("ModifiedLogScale: The base must be > 1");
+ }
+ }
+ /**
+ * Returns an adjusted log10 value for graphing purposes. The first
+ * adjustment is that negative values are changed to positive during
+ * the calculations, and then the answer is negated at the end. The
+ * second is that, for values less than 10, an increasingly large
+ * (0 to 1) scaling factor is added such that at 0 the value is
+ * adjusted to 1, resulting in a returned result of 0.
+ */
+ ModifiedLog.prototype.adjustedLog = function (x) {
+ var negationFactor = x < 0 ? -1 : 1;
+ x *= negationFactor;
+
+ if (x < this.pivot) {
+ x += (this.pivot - x) / this.pivot;
+ }
+
+ x = Math.log(x) / Math.log(this.base);
+
+ x *= negationFactor;
+ return x;
+ };
+
+ ModifiedLog.prototype.invertedAdjustedLog = function (x) {
+ var negationFactor = x < 0 ? -1 : 1;
+ x *= negationFactor;
+
+ x = Math.pow(this.base, x);
+
+ if (x < this.pivot) {
+ x = (this.pivot * (x - 1)) / (this.pivot - 1);
+ }
+
+ x *= negationFactor;
+ return x;
+ };
+
+ ModifiedLog.prototype.scale = function (x) {
+ return this._d3Scale(this.adjustedLog(x));
+ };
+
+ ModifiedLog.prototype.invert = function (x) {
+ return this.invertedAdjustedLog(this._d3Scale.invert(x));
+ };
+
+ ModifiedLog.prototype._getDomain = function () {
+ return this.untransformedDomain;
+ };
+
+ ModifiedLog.prototype._setDomain = function (values) {
+ this.untransformedDomain = values;
+ var transformedDomain = [this.adjustedLog(values[0]), this.adjustedLog(values[1])];
+ this._d3Scale.domain(transformedDomain);
+ this.broadcaster.broadcast();
+ return this;
+ };
+
+ ModifiedLog.prototype.ticks = function (count) {
+ if (count != null) {
+ _super.prototype.ticks.call(this, count);
+ }
+
+ // Say your domain is [-100, 100] and your pivot is 10.
+ // then we're going to draw negative log ticks from -100 to -10,
+ // linear ticks from -10 to 10, and positive log ticks from 10 to 100.
+ var middle = function (x, y, z) {
+ return [x, y, z].sort(function (a, b) {
+ return a - b;
+ })[1];
+ };
+ var min = d3.min(this.untransformedDomain);
+ var max = d3.max(this.untransformedDomain);
+ var negativeLower = min;
+ var negativeUpper = middle(min, max, -this.pivot);
+ var positiveLower = middle(min, max, this.pivot);
+ var positiveUpper = max;
+
+ var negativeLogTicks = this.logTicks(-negativeUpper, -negativeLower).map(function (x) {
+ return -x;
+ }).reverse();
+ var positiveLogTicks = this.logTicks(positiveLower, positiveUpper);
+ var linearTicks = this._showIntermediateTicks ? d3.scale.linear().domain([negativeUpper, positiveLower]).ticks(this.howManyTicks(negativeUpper, positiveLower)) : [-this.pivot, 0, this.pivot].filter(function (x) {
+ return min <= x && x <= max;
+ });
+
+ return negativeLogTicks.concat(linearTicks).concat(positiveLogTicks);
+ };
+
+ /**
+ * Return an appropriate number of ticks from lower to upper.
+ *
+ * This will first try to fit as many powers of this.base as it can from
+ * lower to upper.
+ *
+ * If it still has ticks after that, it will generate ticks in "clusters",
+ * e.g. [20, 30, ... 90, 100] would be a cluster, [200, 300, ... 900, 1000]
+ * would be another cluster.
+ *
+ * This function will generate clusters as large as it can while not
+ * drastically exceeding its number of ticks.
+ */
+ ModifiedLog.prototype.logTicks = function (lower, upper) {
+ var _this = this;
+ var nTicks = this.howManyTicks(lower, upper);
+ if (nTicks === 0) {
+ return [];
+ }
+ var startLogged = Math.floor(Math.log(lower) / Math.log(this.base));
+ var endLogged = Math.ceil(Math.log(upper) / Math.log(this.base));
+ var bases = d3.range(endLogged, startLogged, -Math.ceil((endLogged - startLogged) / nTicks));
+ var nMultiples = this._showIntermediateTicks ? Math.floor(nTicks / bases.length) : 1;
+ var multiples = d3.range(this.base, 1, -(this.base - 1) / nMultiples).map(Math.floor);
+ var uniqMultiples = Plottable.Util.Methods.uniqNumbers(multiples);
+ var clusters = bases.map(function (b) {
+ return uniqMultiples.map(function (x) {
+ return Math.pow(_this.base, b - 1) * x;
+ });
+ });
+ var flattened = Plottable.Util.Methods.flatten(clusters);
+ var filtered = flattened.filter(function (x) {
+ return lower <= x && x <= upper;
+ });
+ var sorted = filtered.sort(function (x, y) {
+ return x - y;
+ });
+ return sorted;
+ };
+
+ /**
+ * How many ticks does the range [lower, upper] deserve?
+ *
+ * e.g. if your domain was [10, 1000] and I asked howManyTicks(10, 100),
+ * I would get 1/2 of the ticks. The range 10, 100 takes up 1/2 of the
+ * distance when plotted.
+ */
+ ModifiedLog.prototype.howManyTicks = function (lower, upper) {
+ var adjustedMin = this.adjustedLog(d3.min(this.untransformedDomain));
+ var adjustedMax = this.adjustedLog(d3.max(this.untransformedDomain));
+ var adjustedLower = this.adjustedLog(lower);
+ var adjustedUpper = this.adjustedLog(upper);
+ var proportion = (adjustedUpper - adjustedLower) / (adjustedMax - adjustedMin);
+ var ticks = Math.ceil(proportion * this._lastRequestedTickCount);
+ return ticks;
+ };
+
+ ModifiedLog.prototype.copy = function () {
+ return new ModifiedLog(this.base);
+ };
+
+ ModifiedLog.prototype._niceDomain = function (domain, count) {
+ return domain;
+ };
+
+ ModifiedLog.prototype.showIntermediateTicks = function (show) {
+ if (show == null) {
+ return this._showIntermediateTicks;
+ } else {
+ this._showIntermediateTicks = show;
+ }
+ };
+ return ModifiedLog;
+ })(Plottable.Abstract.QuantitiveScale);
+ Scale.ModifiedLog = ModifiedLog;
+ })(Plottable.Scale || (Plottable.Scale = {}));
+ var Scale = Plottable.Scale;
+})(Plottable || (Plottable = {}));
+
///
var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
@@ -4494,6 +4726,9 @@ var Plottable;
};
var tickLabels = this._tickLabelContainer.selectAll("." + Abstract.Axis.TICK_LABEL_CLASS);
+ if (tickLabels[0].length === 0) {
+ return;
+ }
var firstTickLabel = tickLabels[0][0];
if (!isInsideBBox(firstTickLabel.getBoundingClientRect())) {
d3.select(firstTickLabel).style("visibility", "hidden");
diff --git a/quicktests/list_of_quicktests.json b/quicktests/list_of_quicktests.json
index 603bd31f73..7550747c54 100644
--- a/quicktests/list_of_quicktests.json
+++ b/quicktests/list_of_quicktests.json
@@ -10,5 +10,6 @@
{"name":"scale_interactive", "categories":["scale", "interaction", "scatter", "gridlines"]},
{"name":"scale_date", "categories":["scale", "date", "line"]},
{"name":"basic_area", "categories":["area", "basic", "gridlines"]},
- {"name":"cat-axis_verticalBar", "categories":["verticalbar", "cat-axis", "animate"]}
+ {"name":"cat-axis_verticalBar", "categories":["verticalbar", "cat-axis", "animate"]},
+ {"name":"modifiedLogScale_test", "categories":["scale", "modified-log"]}
]
diff --git a/quicktests/modifiedLogScale_test.js b/quicktests/modifiedLogScale_test.js
new file mode 100644
index 0000000000..d52e28bcb6
--- /dev/null
+++ b/quicktests/modifiedLogScale_test.js
@@ -0,0 +1,43 @@
+function makeData() {
+ var data = makeRandomData(100, 1e15);
+ // data.push({x: 0, y: 0});
+ return data;
+}
+
+function run(div, data, Plottable) {
+ // doesn't exist on master yet
+ if (Plottable.Scale.ModifiedLog == null) {
+ return;
+ }
+
+ var svg = div.append("svg").attr("height", 500);
+ var doAnimate = true;
+ var circleRenderer;
+ var xScale = new Plottable.Scale.Linear();
+ var xAxis = new Plottable.Axis.Numeric(xScale, "bottom");
+
+ var yScale = new Plottable.Scale.ModifiedLog();
+ var yAxis = new Plottable.Axis.Numeric(yScale, "left", new Plottable.Formatter.SISuffix());
+ yAxis.showEndTickLabel("top", false);
+ yAxis.showEndTickLabel("bottom", false);
+
+ circleRenderer = new Plottable.Plot.Scatter(data, xScale, yScale);
+ circleRenderer.project("r", 8);
+ circleRenderer.project("opacity", 0.75);
+ circleRenderer.animate(doAnimate);
+
+ var gridlines = new Plottable.Component.Gridlines(xScale, yScale);
+
+ var circleChart = new Plottable.Component.Table([[yAxis, circleRenderer.merge(gridlines)],
+ [null, xAxis]]);
+ circleChart.renderTo(svg);
+
+ cb = function(x, y){
+ d = circleRenderer.dataSource().data();
+ circleRenderer.dataSource().data(d);
+ };
+
+ window.xy = new Plottable.Interaction.Click(circleRenderer)
+ .callback(cb)
+ .registerWithComponent();
+}
\ No newline at end of file
diff --git a/src/components/baseAxis.ts b/src/components/baseAxis.ts
index 837454b7a9..ad01fd9119 100644
--- a/src/components/baseAxis.ts
+++ b/src/components/baseAxis.ts
@@ -417,6 +417,9 @@ export module Abstract {
};
var tickLabels = this._tickLabelContainer.selectAll("." + Abstract.Axis.TICK_LABEL_CLASS);
+ if (tickLabels[0].length === 0) {
+ return;
+ }
var firstTickLabel = tickLabels[0][0];
if (!isInsideBBox(firstTickLabel.getBoundingClientRect())) {
d3.select(firstTickLabel).style("visibility", "hidden");
diff --git a/src/core/domainer.ts b/src/core/domainer.ts
index 03de5d61ab..0a33b9083e 100644
--- a/src/core/domainer.ts
+++ b/src/core/domainer.ts
@@ -185,9 +185,9 @@ module Plottable {
var p = this.padProportion / 2;
// This scaling is done to account for log scales and other non-linear
// scales. A log scale should be padded more on the max than on the min.
- var newMin = scale._d3Scale.invert(scale.scale(min) -
+ var newMin = scale.invert(scale.scale(min) -
(scale.scale(max) - scale.scale(min)) * p);
- var newMax = scale._d3Scale.invert(scale.scale(max) +
+ var newMax = scale.invert(scale.scale(max) +
(scale.scale(max) - scale.scale(min)) * p);
var exceptionValues = this.paddingExceptions.values().concat(this.unregisteredPaddingExceptions.values());
var exceptionSet = d3.set(exceptionValues);
diff --git a/src/core/scale.ts b/src/core/scale.ts
index b819d37a36..df5ba44173 100644
--- a/src/core/scale.ts
+++ b/src/core/scale.ts
@@ -73,7 +73,7 @@ export module Abstract {
public domain(values: any[]): Scale;
public domain(values?: any[]): any {
if (values == null) {
- return this._d3Scale.domain();
+ return this._getDomain();
} else {
this.autoDomainAutomatically = false;
this._setDomain(values);
@@ -81,6 +81,10 @@ export module Abstract {
}
}
+ public _getDomain() {
+ return this._d3Scale.domain();
+ }
+
public _setDomain(values: any[]) {
this._d3Scale.domain(values);
this.broadcaster.broadcast();
diff --git a/src/reference.ts b/src/reference.ts
index e491206ac3..cc6ce1aa2a 100644
--- a/src/reference.ts
+++ b/src/reference.ts
@@ -37,6 +37,7 @@
///
///
///
+///
///
///
///
diff --git a/src/scales/modifiedLogScale.ts b/src/scales/modifiedLogScale.ts
new file mode 100644
index 0000000000..087d445035
--- /dev/null
+++ b/src/scales/modifiedLogScale.ts
@@ -0,0 +1,209 @@
+///
+
+module Plottable {
+export module Scale {
+ export class ModifiedLog extends Abstract.QuantitiveScale {
+ private base: number;
+ private pivot: number;
+ private untransformedDomain: number[];
+ private _showIntermediateTicks = false;
+
+ /**
+ * Creates a new Scale.ModifiedLog.
+ *
+ * A ModifiedLog scale acts as a regular log scale for large numbers.
+ * As it approaches 0, it gradually becomes linear. This means that the
+ * scale won't freak out if you give it 0 or a negative number, where an
+ * ordinary Log scale would.
+ *
+ * However, it does mean that scale will be effectively linear as values
+ * approach 0. If you want very small values on a log scale, you should use
+ * an ordinary Scale.Log instead.
+ *
+ * @constructor
+ * @param {number} [base]
+ * The base of the log. Defaults to 10, and must be > 1.
+ *
+ * For base <= x, scale(x) = log(x).
+ *
+ * For 0 < x < base, scale(x) will become more and more
+ * linear as it approaches 0.
+ *
+ * At x == 0, scale(x) == 0.
+ *
+ * For negative values, scale(-x) = -scale(x).
+ */
+ constructor(base = 10) {
+ super(d3.scale.linear());
+ this.base = base;
+ this.pivot = this.base;
+ this.untransformedDomain = this._defaultExtent();
+ this._lastRequestedTickCount = 10;
+ if (base <= 1) {
+ throw new Error("ModifiedLogScale: The base must be > 1");
+ }
+ }
+
+ /**
+ * Returns an adjusted log10 value for graphing purposes. The first
+ * adjustment is that negative values are changed to positive during
+ * the calculations, and then the answer is negated at the end. The
+ * second is that, for values less than 10, an increasingly large
+ * (0 to 1) scaling factor is added such that at 0 the value is
+ * adjusted to 1, resulting in a returned result of 0.
+ */
+ private adjustedLog(x: number): number {
+ var negationFactor = x < 0 ? -1 : 1;
+ x *= negationFactor;
+
+ if (x < this.pivot) {
+ x += (this.pivot - x) / this.pivot;
+ }
+
+ x = Math.log(x) / Math.log(this.base);
+
+ x *= negationFactor;
+ return x;
+ }
+
+ private invertedAdjustedLog(x: number): number {
+ var negationFactor = x < 0 ? -1 : 1;
+ x *= negationFactor;
+
+ x = Math.pow(this.base, x);
+
+ if (x < this.pivot) {
+ x = (this.pivot * (x - 1)) / (this.pivot - 1);
+ }
+
+ x *= negationFactor;
+ return x;
+ }
+
+ public scale(x: number): number {
+ return this._d3Scale(this.adjustedLog(x));
+ }
+
+ public invert(x: number): number {
+ return this.invertedAdjustedLog(this._d3Scale.invert(x));
+ }
+
+ public _getDomain() {
+ return this.untransformedDomain;
+ }
+
+ public _setDomain(values: number[]) {
+ this.untransformedDomain = values;
+ var transformedDomain = [this.adjustedLog(values[0]), this.adjustedLog(values[1])];
+ this._d3Scale.domain(transformedDomain);
+ this.broadcaster.broadcast();
+ return this;
+ }
+
+ public ticks(count?: number) {
+ if (count != null) {
+ super.ticks(count);
+ }
+
+ // Say your domain is [-100, 100] and your pivot is 10.
+ // then we're going to draw negative log ticks from -100 to -10,
+ // linear ticks from -10 to 10, and positive log ticks from 10 to 100.
+ var middle = (x: number, y: number, z: number) => [x, y, z].sort((a, b) => a - b)[1];
+ var min = d3.min(this.untransformedDomain);
+ var max = d3.max(this.untransformedDomain);
+ var negativeLower = min;
+ var negativeUpper = middle(min, max, -this.pivot);
+ var positiveLower = middle(min, max, this.pivot);
+ var positiveUpper = max;
+
+ var negativeLogTicks = this.logTicks(-negativeUpper, -negativeLower).map((x) => -x).reverse();
+ var positiveLogTicks = this.logTicks(positiveLower, positiveUpper);
+ var linearTicks = this._showIntermediateTicks ?
+ d3.scale.linear().domain([negativeUpper, positiveLower])
+ .ticks(this.howManyTicks(negativeUpper, positiveLower)) :
+ [-this.pivot, 0, this.pivot].filter((x) => min <= x && x <= max);
+
+ return negativeLogTicks.concat(linearTicks).concat(positiveLogTicks);
+ }
+
+ /**
+ * Return an appropriate number of ticks from lower to upper.
+ *
+ * This will first try to fit as many powers of this.base as it can from
+ * lower to upper.
+ *
+ * If it still has ticks after that, it will generate ticks in "clusters",
+ * e.g. [20, 30, ... 90, 100] would be a cluster, [200, 300, ... 900, 1000]
+ * would be another cluster.
+ *
+ * This function will generate clusters as large as it can while not
+ * drastically exceeding its number of ticks.
+ */
+ private logTicks(lower: number, upper: number): number[] {
+ var nTicks = this.howManyTicks(lower, upper);
+ if (nTicks === 0) {
+ return [];
+ }
+ var startLogged = Math.floor(Math.log(lower) / Math.log(this.base));
+ var endLogged = Math.ceil(Math.log(upper) / Math.log(this.base));
+ var bases = d3.range(endLogged, startLogged, -Math.ceil((endLogged - startLogged) / nTicks));
+ var nMultiples = this._showIntermediateTicks ? Math.floor(nTicks / bases.length) : 1;
+ var multiples = d3.range(this.base, 1, -(this.base - 1) / nMultiples).map(Math.floor);
+ var uniqMultiples = Util.Methods.uniqNumbers(multiples);
+ var clusters = bases.map((b) => uniqMultiples.map((x) => Math.pow(this.base, b - 1) * x));
+ var flattened = Util.Methods.flatten(clusters);
+ var filtered = flattened.filter((x) => lower <= x && x <= upper);
+ var sorted = filtered.sort((x, y) => x - y);
+ return sorted;
+ }
+
+ /**
+ * How many ticks does the range [lower, upper] deserve?
+ *
+ * e.g. if your domain was [10, 1000] and I asked howManyTicks(10, 100),
+ * I would get 1/2 of the ticks. The range 10, 100 takes up 1/2 of the
+ * distance when plotted.
+ */
+ private howManyTicks(lower: number, upper: number): number {
+ var adjustedMin = this.adjustedLog(d3.min(this.untransformedDomain));
+ var adjustedMax = this.adjustedLog(d3.max(this.untransformedDomain));
+ var adjustedLower = this.adjustedLog(lower);
+ var adjustedUpper = this.adjustedLog(upper);
+ var proportion = (adjustedUpper - adjustedLower) / (adjustedMax - adjustedMin);
+ var ticks = Math.ceil(proportion * this._lastRequestedTickCount);
+ return ticks;
+ }
+
+ public copy(): ModifiedLog {
+ return new ModifiedLog(this.base);
+ }
+
+ public _niceDomain(domain: any[], count?: number): any[] {
+ return domain;
+ }
+
+ /**
+ * @returns {boolean}
+ * Whether or not to return tick values other than powers of base.
+ *
+ * This defaults to false, so you'll normally only see ticks like
+ * [10, 100, 1000]. If you turn it on, you might see ticks values
+ * like [10, 50, 100, 500, 1000].
+ */
+ public showIntermediateTicks(): boolean;
+ /**
+ * @param {boolean} show
+ * Whether or not to return ticks values other than powers of the base.
+ */
+ public showIntermediateTicks(show: boolean): ModifiedLog;
+ public showIntermediateTicks(show?: boolean): any {
+ if (show == null) {
+ return this._showIntermediateTicks;
+ } else {
+ this._showIntermediateTicks = show;
+ }
+ }
+
+ }
+}
+}
diff --git a/src/scales/quantitiveScale.ts b/src/scales/quantitiveScale.ts
index 3c3db2b573..3aa48f9e60 100644
--- a/src/scales/quantitiveScale.ts
+++ b/src/scales/quantitiveScale.ts
@@ -4,7 +4,7 @@ module Plottable {
export module Abstract {
export class QuantitiveScale extends Scale {
public _d3Scale: D3.Scale.QuantitiveScale;
- private lastRequestedTickCount = 10;
+ public _lastRequestedTickCount = 10;
public _PADDING_FOR_IDENTICAL_DOMAIN = 1;
public _userSetDomainer: boolean = false;
private _domainer: Domainer = new Domainer();
@@ -112,9 +112,9 @@ export module Abstract {
*/
public ticks(count?: number) {
if (count != null) {
- this.lastRequestedTickCount = count;
+ this._lastRequestedTickCount = count;
}
- return this._d3Scale.ticks(this.lastRequestedTickCount);
+ return this._d3Scale.ticks(this._lastRequestedTickCount);
}
/**
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 819d29b689..5e5f64c14a 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -84,6 +84,18 @@ export module Util {
return d3.keys(seen);
}
+ export function uniqNumbers(a: number[]): number[] {
+ var seen = d3.set();
+ var result: number[] = [];
+ a.forEach((n) => {
+ if (!seen.has(n)) {
+ seen.add(n);
+ result.push(n);
+ }
+ });
+ return result;
+ }
+
/**
* Creates an array of length `count`, filled with value or (if value is a function), value()
*
diff --git a/test/scales/scaleTests.ts b/test/scales/scaleTests.ts
index 64dca6be31..d29a29887f 100644
--- a/test/scales/scaleTests.ts
+++ b/test/scales/scaleTests.ts
@@ -309,4 +309,95 @@ describe("Scales", () => {
assert.equal(scale.domain(), startDomain);
});
});
+ describe("Modified Log Scale", () => {
+ var scale: Plottable.Scale.ModifiedLog;
+ var base = 10;
+ var epsilon = 0.00001;
+ beforeEach(() => {
+ scale = new Plottable.Scale.ModifiedLog(base);
+ });
+
+ it("is an increasing, continuous function that can go negative", () => {
+ d3.range(-base * 2, base * 2, base / 20).forEach((x: number) => {
+ // increasing
+ assert.operator(scale.scale(x - epsilon), "<", scale.scale(x));
+ assert.operator(scale.scale(x), "<", scale.scale(x + epsilon));
+ // continuous
+ assert.closeTo(scale.scale(x - epsilon), scale.scale(x), epsilon);
+ assert.closeTo(scale.scale(x), scale.scale(x + epsilon), epsilon);
+ });
+ assert.closeTo(scale.scale(0), 0, epsilon);
+ });
+
+ it("is close to log() for large values", () => {
+ [10, 100, 23103.4, 5].forEach((x) => {
+ assert.closeTo(scale.scale(x), Math.log(x) / Math.log(10), 0.1);
+ });
+ });
+
+ it("x = invert(scale(x))", () => {
+ [0, 1, base, 100, 0.001, -1, -0.3, -base, base - 0.001].forEach((x) => {
+ assert.closeTo(x, scale.invert(scale.scale(x)), epsilon);
+ assert.closeTo(x, scale.scale(scale.invert(x)), epsilon);
+ });
+ });
+
+ it("domain defaults to [0, 1]", () => {
+ scale = new Plottable.Scale.ModifiedLog(base);
+ assert.deepEqual(scale.domain(), [0, 1]);
+ });
+
+ it("works with a domainer", () => {
+ scale.updateExtent(1, "x", [0, base * 2]);
+ var domain = scale.domain();
+ scale.domainer(new Plottable.Domainer().pad(0.1));
+ assert.operator(scale.domain()[0], "<", domain[0]);
+ assert.operator(domain[1], "<", scale.domain()[1]);
+
+ scale.domainer(new Plottable.Domainer().nice());
+ assert.operator(scale.domain()[0], "<=", domain[0]);
+ assert.operator(domain[1], "<=", scale.domain()[1]);
+
+ scale = new Plottable.Scale.ModifiedLog(base);
+ scale.domainer(new Plottable.Domainer());
+ assert.deepEqual(scale.domain(), [0, 1]);
+ });
+
+ it("gives reasonable values for ticks()", () => {
+ scale.updateExtent(1, "x", [0, base / 2]);
+ var ticks = scale.ticks();
+ assert.operator(ticks.length, ">", 0);
+
+ scale.updateExtent(1, "x", [-base * 2, base * 2]);
+ ticks = scale.ticks();
+ var beforePivot = ticks.filter((x) => x <= -base);
+ var afterPivot = ticks.filter((x) => base <= x);
+ var betweenPivots = ticks.filter((x) => -base < x && x < base);
+ assert.operator(beforePivot.length, ">", 0, "should be ticks before -base");
+ assert.operator(afterPivot.length, ">", 0, "should be ticks after base");
+ assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base");
+ });
+
+ it("works on inverted domain", () => {
+ scale.updateExtent(1, "x", [200, -100]);
+ var range = scale.range();
+ assert.closeTo(scale.scale(-100), range[1], epsilon);
+ assert.closeTo(scale.scale(200), range[0], epsilon);
+ var a = [-100, -10, -3, 0, 1, 3.64, 50, 60, 200];
+ var b = a.map((x) => scale.scale(x));
+ // should be decreasing function; reverse is sorted
+ assert.deepEqual(b.slice().reverse(), b.slice().sort((x, y) => x - y));
+
+ var ticks = scale.ticks();
+ assert.deepEqual(ticks, ticks.slice().sort((x, y) => x - y), "ticks should be sorted");
+ assert.deepEqual(ticks, Plottable.Util.Methods.uniqNumbers(ticks), "ticks should not be repeated");
+ var beforePivot = ticks.filter((x) => x <= -base);
+ var afterPivot = ticks.filter((x) => base <= x);
+ var betweenPivots = ticks.filter((x) => -base < x && x < base);
+ assert.operator(beforePivot.length, ">", 0, "should be ticks before -base");
+ assert.operator(afterPivot.length, ">", 0, "should be ticks after base");
+ assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base");
+ });
+ });
+
});
diff --git a/test/tests.js b/test/tests.js
index 47a52de082..2c7b1764fb 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -3789,6 +3789,116 @@ describe("Scales", function () {
assert.equal(scale.domain(), startDomain);
});
});
+ describe("Modified Log Scale", function () {
+ var scale;
+ var base = 10;
+ var epsilon = 0.00001;
+ beforeEach(function () {
+ scale = new Plottable.Scale.ModifiedLog(base);
+ });
+
+ it("is an increasing, continuous function that can go negative", function () {
+ d3.range(-base * 2, base * 2, base / 20).forEach(function (x) {
+ // increasing
+ assert.operator(scale.scale(x - epsilon), "<", scale.scale(x));
+ assert.operator(scale.scale(x), "<", scale.scale(x + epsilon));
+
+ // continuous
+ assert.closeTo(scale.scale(x - epsilon), scale.scale(x), epsilon);
+ assert.closeTo(scale.scale(x), scale.scale(x + epsilon), epsilon);
+ });
+ assert.closeTo(scale.scale(0), 0, epsilon);
+ });
+
+ it("is close to log() for large values", function () {
+ [10, 100, 23103.4, 5].forEach(function (x) {
+ assert.closeTo(scale.scale(x), Math.log(x) / Math.log(10), 0.1);
+ });
+ });
+
+ it("x = invert(scale(x))", function () {
+ [0, 1, base, 100, 0.001, -1, -0.3, -base, base - 0.001].forEach(function (x) {
+ assert.closeTo(x, scale.invert(scale.scale(x)), epsilon);
+ assert.closeTo(x, scale.scale(scale.invert(x)), epsilon);
+ });
+ });
+
+ it("domain defaults to [0, 1]", function () {
+ scale = new Plottable.Scale.ModifiedLog(base);
+ assert.deepEqual(scale.domain(), [0, 1]);
+ });
+
+ it("works with a domainer", function () {
+ scale.updateExtent(1, "x", [0, base * 2]);
+ var domain = scale.domain();
+ scale.domainer(new Plottable.Domainer().pad(0.1));
+ assert.operator(scale.domain()[0], "<", domain[0]);
+ assert.operator(domain[1], "<", scale.domain()[1]);
+
+ scale.domainer(new Plottable.Domainer().nice());
+ assert.operator(scale.domain()[0], "<=", domain[0]);
+ assert.operator(domain[1], "<=", scale.domain()[1]);
+
+ scale = new Plottable.Scale.ModifiedLog(base);
+ scale.domainer(new Plottable.Domainer());
+ assert.deepEqual(scale.domain(), [0, 1]);
+ });
+
+ it("gives reasonable values for ticks()", function () {
+ scale.updateExtent(1, "x", [0, base / 2]);
+ var ticks = scale.ticks();
+ assert.operator(ticks.length, ">", 0);
+
+ scale.updateExtent(1, "x", [-base * 2, base * 2]);
+ ticks = scale.ticks();
+ var beforePivot = ticks.filter(function (x) {
+ return x <= -base;
+ });
+ var afterPivot = ticks.filter(function (x) {
+ return base <= x;
+ });
+ var betweenPivots = ticks.filter(function (x) {
+ return -base < x && x < base;
+ });
+ assert.operator(beforePivot.length, ">", 0, "should be ticks before -base");
+ assert.operator(afterPivot.length, ">", 0, "should be ticks after base");
+ assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base");
+ });
+
+ it("works on inverted domain", function () {
+ scale.updateExtent(1, "x", [200, -100]);
+ var range = scale.range();
+ assert.closeTo(scale.scale(-100), range[1], epsilon);
+ assert.closeTo(scale.scale(200), range[0], epsilon);
+ var a = [-100, -10, -3, 0, 1, 3.64, 50, 60, 200];
+ var b = a.map(function (x) {
+ return scale.scale(x);
+ });
+
+ // should be decreasing function; reverse is sorted
+ assert.deepEqual(b.slice().reverse(), b.slice().sort(function (x, y) {
+ return x - y;
+ }));
+
+ var ticks = scale.ticks();
+ assert.deepEqual(ticks, ticks.slice().sort(function (x, y) {
+ return x - y;
+ }), "ticks should be sorted");
+ assert.deepEqual(ticks, Plottable.Util.Methods.uniqNumbers(ticks), "ticks should not be repeated");
+ var beforePivot = ticks.filter(function (x) {
+ return x <= -base;
+ });
+ var afterPivot = ticks.filter(function (x) {
+ return base <= x;
+ });
+ var betweenPivots = ticks.filter(function (x) {
+ return -base < x && x < base;
+ });
+ assert.operator(beforePivot.length, ">", 0, "should be ticks before -base");
+ assert.operator(afterPivot.length, ">", 0, "should be ticks after base");
+ assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base");
+ });
+ });
});
///