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"); + }); + }); }); ///