diff --git a/Gruntfile.js b/Gruntfile.js index a2e5da6cd5..dc35d72bfd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -66,7 +66,7 @@ module.exports = function(grunt) { }, "rebuild": { "tasks": ["dev-compile"], - "files": ["src/*.ts"] + "files": ["src/**/*.ts"] }, "tests": { "tasks": ["ts:test", "tslint"], @@ -78,9 +78,9 @@ module.exports = function(grunt) { } }, blanket_mocha: { - all: ['test/tests.html'], + all: ['test/coverage.html'], options: { - threshold: 85 + threshold: 80 } }, connect: { @@ -147,4 +147,5 @@ module.exports = function(grunt) { grunt.registerTask("launch", ["connect", "dev-compile", "watch"]); grunt.registerTask("test", ["dev-compile", "blanket_mocha"]); + grunt.registerTask("bm", ["blanket_mocha"]); }; diff --git a/bower.json b/bower.json index dac42c07fb..19b9dec25e 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "plottable", - "version": "0.7.0", + "version": "0.8.0", "ignore": [ "**/*", "!plottable.js", diff --git a/examples/exampleUtil.js b/examples/exampleUtil.js index 2c44365025..94a8de476e 100644 --- a/examples/exampleUtil.js +++ b/examples/exampleUtil.js @@ -9,7 +9,7 @@ function makeRandomData(numPoints, scaleFactor) { data.sort(function (a, b) { return a.x - b.x; }); - return { data: data, metadata: { cssClass: "random-data" } }; + return data; } function makeNormallyDistributedData(n, xMean, xStdDev, yMean, yStdDev) { @@ -85,10 +85,7 @@ function makeRandomBucketData(numBuckets, bucketWidth, maxValue) { y: Math.round(Math.random() * maxValue) }); } - return { - "data": data, - metadata: { cssClass: "random-buckets" } - }; + return data; } function generateHeightWeightData(n) { @@ -104,8 +101,5 @@ function generateHeightWeightData(n) { }); } - return { - data: data, - metadata: { cssClass: "height-weight-data" } - }; + return data; } diff --git a/examples/exampleUtil.ts b/examples/exampleUtil.ts index 8207ecf88d..8ad53bc798 100644 --- a/examples/exampleUtil.ts +++ b/examples/exampleUtil.ts @@ -1,4 +1,4 @@ -function makeRandomData(numPoints, scaleFactor=1): Plottable.IDataset { +function makeRandomData(numPoints, scaleFactor=1): any[] { var data = []; for (var i = 0; i < numPoints; i++) { var x = Math.random(); @@ -6,7 +6,7 @@ function makeRandomData(numPoints, scaleFactor=1): Plottable.IDataset { data.push(r); } data.sort((a: any, b: any) => a.x - b.x); - return {data: data, metadata: {cssClass: "random-data"}}; + return data; } function makeNormallyDistributedData(n=100, xMean?, xStdDev?, yMean?, yStdDev?) { @@ -53,7 +53,7 @@ function binByVal(data: any[], accessor: Plottable.IAccessor, range=[0,100], nBi }) return bins; } -function makeRandomBucketData(numBuckets: number, bucketWidth: number, maxValue = 10): Plottable.IDataset { +function makeRandomBucketData(numBuckets: number, bucketWidth: number, maxValue = 10): any[] { var data = []; for (var i=0; i < numBuckets; i++) { data.push({ @@ -62,10 +62,7 @@ function makeRandomBucketData(numBuckets: number, bucketWidth: number, maxValue y: Math.round(Math.random() * maxValue) }); } - return { - "data": data, - metadata: {cssClass: "random-buckets"} - }; + return data; } function generateHeightWeightData(n: number) { @@ -81,8 +78,5 @@ function generateHeightWeightData(n: number) { }); } - return { - data: data, - metadata: {cssClass: "height-weight-data"} - }; + return data; } diff --git a/examples/main-page/commit-dashboard.js b/examples/main-page/commit-dashboard.js index 51896a6333..7bc35a8183 100644 --- a/examples/main-page/commit-dashboard.js +++ b/examples/main-page/commit-dashboard.js @@ -45,15 +45,15 @@ function commitDashboard(dataManager, svg) { var scatterDateAxis = new Plottable.XAxis(timeScale, "bottom", dateFormatter); var rScale = new Plottable.QuantitiveScale(d3.scale.log()) - .range([2, 12]) - .widenDomainOnData(commits, linesAddedAccessor); + .range([2, 12]); function radiusAccessor(d) { return rScale.scale(linesAddedAccessor(d)); } var scatterRenderer = new Plottable.CircleRenderer(commits, timeScale, scatterYScale) - .xAccessor("date") - .yAccessor(hourAccessor) - .rAccessor(radiusAccessor) - .colorAccessor(function(d) { return contributorColorScale.scale(d.name); }); + .project("x", "date") + .project("y", hourAccessor) + .project("r", linesAddedAccessor, rScale) + .project("fill", "name", contributorColorScale); + window.scatterRenderer = scatterRenderer; var scatterGridlines = new Plottable.Gridlines(timeScale, scatterYScale); var scatterRenderArea = scatterGridlines.merge(scatterRenderer); @@ -68,17 +68,14 @@ function commitDashboard(dataManager, svg) { var tscRenderers = {}; dataManager.directories.forEach(function(dir) { var timeSeries = directoryTimeSeries[dir]; - var directoryDataset = { - data: timeSeries, - metadata: {} - }; - var lineRenderer = new Plottable.LineRenderer(directoryDataset, timeScale, tscYScale); - lineRenderer.xAccessor(function(d) { return d[0]; }) - .yAccessor(function(d) { return d[1]; }) - .colorAccessor(function(d) { return directoryColorScale.scale(dir); }); + var lineRenderer = new Plottable.LineRenderer(timeSeries, timeScale, tscYScale); + lineRenderer.project("x", function(d) { return d[0]; }) + .project("y", function(d) { return d[1]; }) + .project("stroke", function() {return dir}, directoryColorScale); lineRenderer.classed(dir, true); tscRenderers[dir] = lineRenderer; tscRenderArea = tscRenderArea.merge(lineRenderer); + window.lineRenderer = lineRenderer; }); var loadTSCData = function() { @@ -102,17 +99,17 @@ function commitDashboard(dataManager, svg) { // ----- /Legends ----- // ----- Bar1: Lines changed by contributor ----- - var contributorBarYScale = new Plottable.LinearScale(); - var contributorBarYAxis = new Plottable.YAxis(contributorBarYScale, "right"); var contributorBarXScale = new Plottable.OrdinalScale().domain(dataManager.contributors); + var contributorBarYScale = new Plottable.LinearScale(); var contributorBarXAxis = new Plottable.XAxis(contributorBarXScale, "bottom", function(d) { return d}); + var contributorBarYAxis = new Plottable.YAxis(contributorBarYScale, "right"); contributorBarXAxis.classed("no-tick-labels", true).rowMinimum(5); var contributorBarRenderer = new Plottable.CategoryBarRenderer(linesByContributor, contributorBarXScale, contributorBarYScale); - contributorBarRenderer.widthAccessor(40); - contributorBarRenderer.colorAccessor(function(d) { return contributorColorScale.scale(d.name); }); - contributorBarRenderer.xAccessor("name").yAccessor(linesAddedAccessor); + contributorBarRenderer.project("width", 40) + .project("fill", "name", contributorColorScale) + .project("x", "name").project("y", linesAddedAccessor); var contributorGridlines = new Plottable.Gridlines(null, contributorBarYScale); var contributorBarChart = new Plottable.Table([ [contributorBarRenderer.merge(contributorGridlines), contributorBarYAxis], @@ -129,9 +126,10 @@ function commitDashboard(dataManager, svg) { var directoryBarRenderer = new Plottable.CategoryBarRenderer(linesByDirectory, directoryBarXScale, directoryBarYScale); - directoryBarRenderer.widthAccessor(40); - directoryBarRenderer.colorAccessor(function(d) { return directoryColorScale.scale(d.directory); }); - directoryBarRenderer.xAccessor("directory").yAccessor(linesAddedAccessor); + directoryBarRenderer.project("width", 40) + .project("fill", "directory", directoryColorScale) + .project("x", "directory") + .project("y", linesAddedAccessor); var directoryGridlines = new Plottable.Gridlines(null, directoryBarYScale); var directoryBarChart = new Plottable.Table([ [directoryBarRenderer.merge(directoryGridlines), directoryBarYAxis], diff --git a/license_header.txt b/license_header.txt index 68467d16bc..51ac8518a3 100644 --- a/license_header.txt +++ b/license_header.txt @@ -1,5 +1,5 @@ /*! -Plottable v0.7.0 (https://github.com/palantir/plottable) +Plottable v0.8.0 (https://github.com/palantir/plottable) Copyright 2014 Palantir Technologies Licensed under MIT (https://github.com/palantir/plottable/blob/master/LICENSE) */ diff --git a/package.json b/package.json index ea999a97f6..5580bfdbb9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plottable.js", - "version": "0.7.0", + "version": "0.8.0", "description": "Build flexible, performant, interactive charts using D3", "repository": { "type": "git", diff --git a/plottable.d.ts b/plottable.d.ts index 494347ca52..479111627d 100644 --- a/plottable.d.ts +++ b/plottable.d.ts @@ -10,6 +10,8 @@ declare module Plottable { * @returns {SVGRed} The bounding box. */ function getBBox(element: D3.Selection): SVGRect; + function getElementWidth(elem: HTMLScriptElement): number; + function getElementHeight(elem: HTMLScriptElement): number; /** * Truncates a text string to a max length, given the element in which to draw the text * @@ -27,6 +29,33 @@ declare module Plottable { */ function getTextHeight(textElement: D3.Selection): number; function getSVGPixelWidth(svg: D3.Selection): number; + function accessorize(accessor: any): IAccessor; + function applyAccessor(accessor: IAccessor, dataSource: DataSource): (d: any, i: number) => any; + function uniq(strings: string[]): string[]; + /** + * An associative array that can be keyed by anything (inc objects). + * Uses pointer equality checks which is why this works. + * This power has a price: everything is linear time since it is actually backed by an array... + */ + class StrictEqualityAssociativeArray { + /** + * Set a new key/value pair in the store. + * + * @param {any} Key to set in the store + * @param {any} Value to set in the store + * @return {boolean} True if key already in store, false otherwise + */ + public set(key: any, value: any): boolean; + public get(key: any): any; + public has(key: any): boolean; + public values(): any[]; + public delete(key: any): boolean; + } + class IDCounter { + public increment(id: any): number; + public decrement(id: any): number; + public get(id: any): number; + } } } declare module Plottable { @@ -72,7 +101,38 @@ declare module Plottable { } } declare module Plottable { - class Component { + class PlottableObject { + } +} +declare module Plottable { + class Broadcaster extends PlottableObject { + /** + * Registers a callback to be called when the broadcast method is called. Also takes a listener which + * is used to support deregistering the same callback later, by passing in the same listener. + * If there is already a callback associated with that listener, then the callback will be replaced. + * + * @param listener The listener associated with the callback. + * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. + * @returns {Broadcaster} this object + */ + public registerListener(listener: any, callback: IBroadcasterCallback): Broadcaster; + /** + * Call all listening callbacks, optionally with arguments passed through. + * + * @param ...args A variable number of optional arguments + * @returns {Broadcaster} this object + */ + /** + * Registers deregister the callback associated with a listener. + * + * @param listener The listener to deregister. + * @returns {Broadcaster} this object + */ + public deregisterListener(listener: any): Broadcaster; + } +} +declare module Plottable { + class Component extends PlottableObject { public element: D3.Selection; public content: D3.Selection; public backgroundContainer: D3.Selection; @@ -194,7 +254,7 @@ declare module Plottable { } } declare module Plottable { - class Scale implements IBroadcaster { + class Scale extends Broadcaster { /** * Creates a new Scale. * @@ -203,6 +263,14 @@ declare module Plottable { */ constructor(scale: D3.Scale.Scale); /** + * Modify the domain on the scale so that it includes the extent of all + * perspectives it depends on. Extent: The (min, max) pair for a + * QuantitiativeScale, all covered strings for an OrdinalScale. + * Perspective: A combination of a DataSource and an Accessor that + * represents a view in to the data. + */ + public autorangeDomain(): Scale; + /** * Returns the range value corresponding to a given domain value. * * @param value {any} A domain value to be scaled. @@ -212,7 +280,10 @@ declare module Plottable { /** * Retrieves the current domain, or sets the Scale's domain to the specified values. * - * @param {any[]} [values] The new value for the domain. + * @param {any[]} [values] The new value for the domain. This array may + * contain more than 2 values if the scale type allows it (e.g. + * ordinal scales). Other scales such as quantitative scales accept + * only a 2-value extent array. * @returns {any[]|Scale} The current domain, or the calling Scale (if values is supplied). */ public domain(): any[]; @@ -231,42 +302,9 @@ declare module Plottable { * @returns {Scale} A copy of the calling Scale. */ public copy(): Scale; - /** - * Registers a callback to be called when the scale's domain is changed. - * - * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. - * @returns {Scale} The Calling Scale. - */ - public registerListener(callback: IBroadcasterCallback): Scale; - /** - * Expands the Scale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data - * @returns {Scale} The Scale. - */ - public widenDomainOnData(data: any[], accessor?: IAccessor): Scale; - } - class OrdinalScale extends Scale { - /** - * Creates a new OrdinalScale. Domain and Range are set later. - * - * @constructor - */ - constructor(); - public domain(): any[]; - public domain(values: any[]): Scale; - /** - * Returns the range of pixels spanned by the scale, or sets the range. - * - * @param {number[]} [values] The pixel range to set on the scale. - * @returns {number[]|OrdinalScale} The pixel range, or the calling OrdinalScale. - */ - public range(): any[]; - public range(values: number[]): Scale; - public widenDomainOnData(data: any[], accessor?: IAccessor): OrdinalScale; } +} +declare module Plottable { class QuantitiveScale extends Scale { /** * Creates a new QuantitiveScale. @@ -275,6 +313,7 @@ declare module Plottable { * @param {D3.Scale.QuantitiveScale} scale The D3 QuantitiveScale backing the QuantitiveScale. */ constructor(scale: D3.Scale.QuantitiveScale); + public autorangeDomain(): QuantitiveScale; /** * Retrieves the domain value corresponding to a supplied range value. * @@ -288,22 +327,8 @@ declare module Plottable { * @returns {QuantitiveScale} A copy of the calling QuantitiveScale. */ public copy(): QuantitiveScale; - /** - * Expands the QuantitiveScale's domain to cover the new region. - * - * @param {number[]} newDomain The additional domain to be covered by the QuantitiveScale. - * @returns {QuantitiveScale} The scale. - */ - public widenDomain(newDomain: number[]): QuantitiveScale; - /** - * Expands the QuantitiveScale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data. - * @returns {QuantitiveScale} The scale. - */ - public widenDomainOnData(data: any[], accessor?: IAccessor): QuantitiveScale; + public domain(): any[]; + public domain(values: any[]): QuantitiveScale; /** * Sets or gets the QuantitiveScale's output interpolator * @@ -355,6 +380,8 @@ declare module Plottable { */ public padDomain(padProportion?: number): QuantitiveScale; } +} +declare module Plottable { class LinearScale extends QuantitiveScale { /** * Creates a new LinearScale. @@ -371,16 +398,121 @@ declare module Plottable { */ public copy(): LinearScale; } +} +declare module Plottable { + class OrdinalScale extends Scale { + /** + * Creates a new OrdinalScale. Domain and Range are set later. + * + * @constructor + */ + constructor(); + /** + * Retrieves the current domain, or sets the Scale's domain to the specified values. + * + * @param {any[]} [values] The new values for the domain. This array may contain more than 2 values. + * @returns {any[]|Scale} The current domain, or the calling Scale (if values is supplied). + */ + public domain(): any[]; + public domain(values: any[]): OrdinalScale; + /** + * Returns the range of pixels spanned by the scale, or sets the range. + * + * @param {number[]} [values] The pixel range to set on the scale. + * @returns {number[]|OrdinalScale} The pixel range, or the calling OrdinalScale. + */ + public range(): any[]; + public range(values: number[]): OrdinalScale; + /** + * Returns the width of the range band. Only valid when rangeType is set to "bands". + * + * @returns {number} The range band width or 0 if rangeType isn't "bands". + */ + public rangeBand(): number; + /** + * Returns the range type, or sets the range type. + * + * @param {string} [rangeType] Either "points" or "bands" indicating the + * d3 method used to generate range bounds. + * @param {number} [outerPadding] The padding outside the range, + * proportional to the range step. + * @param {number} [innerPadding] The padding between bands in the range, + * proportional to the range step. This parameter is only used in + * "bands" type ranges. + * @returns {string|OrdinalScale} The current range type, or the calling + * OrdinalScale. + */ + public rangeType(): string; + public rangeType(rangeType: string, outerPadding?: number, innerPadding?: number): OrdinalScale; + } +} +declare module Plottable { class ColorScale extends Scale { /** * Creates a ColorScale. * * @constructor - * @param {string} [scaleType] the type of color scale to create (Category10/Category20/Category20b/Category20c) + * @param {string} [scaleType] the type of color scale to create + * (Category10/Category20/Category20b/Category20c). */ constructor(scaleType?: string); } } +declare module Plottable { + class InterpolatedColorScale extends LinearScale { + /** + * Converts the string array into a linear d3 scale. + * + * d3 doesn't accept more than 2 range values unless we use a ordinal + * scale. So, in order to interpolate smoothly between the full color + * range, we must override the interpolator and compute the color values + * manually. + * + * @param {string[]} [colors] an array of strings representing color + * values in hex ("#FFFFFF") or keywords ("white"). + * @returns a linear d3 scale. + */ + /** + * Creates a InterpolatedColorScale. + * + * @constructor + * @param {string|string[]} [scaleType] the type of color scale to create + * (reds/blues/posneg). Default is "reds". An array of color values + * with at least 2 values may also be passed (e.g. ["#FF00FF", "red", + * "dodgerblue"], in which case the resulting scale will interpolate + * linearly between the color values across the domain. + */ + constructor(scaleType?: any); + } +} +declare module Plottable { + class DataSource extends Broadcaster { + /** + * Creates a new DataSource. + * + * @constructor + * @param {any[]} data + * @param {any} metadata An object containing additional information. + */ + constructor(data?: any[], metadata?: any); + /** + * Retrieves the current data from the DataSource, or sets the data. + * + * @param {any[]} [data] The new data. + * @returns {any[]|DataSource} The current data, or the calling DataSource. + */ + public data(): any[]; + public data(data: any[]): DataSource; + /** + * Retrieves the current metadata from the DataSource, or sets the metadata. + * + * @param {any[]} [metadata] The new metadata. + * @returns {any[]|DataSource} The current metadata, or the calling DataSource. + */ + public metadata(): any; + public metadata(metadata: any): DataSource; + } +} declare module Plottable { interface IKeyEventListenerCallback { (e: D3.Event): any; @@ -446,32 +578,6 @@ declare module Plottable { */ public clearBox(): AreaInteraction; } - class ZoomCallbackGenerator { - /** - * Adds listen-update pair of X scales. - * - * @param {QuantitiveScale} listenerScale An X scale to listen for events on. - * @param {QuantitiveScale} [targetScale] An X scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - public addXScale(listenerScale: QuantitiveScale, targetScale?: QuantitiveScale): ZoomCallbackGenerator; - /** - * Adds listen-update pair of Y scales. - * - * @param {QuantitiveScale} listenerScale A Y scale to listen for events on. - * @param {QuantitiveScale} [targetScale] A Y scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - public addYScale(listenerScale: QuantitiveScale, targetScale?: QuantitiveScale): ZoomCallbackGenerator; - /** - * Generates a callback that can be passed to Interactions. - * - * @returns {(area: SelectionArea) => void} A callback that updates the scales previously specified. - */ - public getCallback(): (area: SelectionArea) => void; - } class MousemoveInteraction extends Interaction { constructor(componentToListenTo: Component); public mousemove(x: number, y: number): void; @@ -507,11 +613,6 @@ declare module Plottable { */ public callback(cb: () => any): KeyInteraction; } - class CrosshairsInteraction extends MousemoveInteraction { - constructor(renderer: NumericXYRenderer); - public mousemove(x: number, y: number): void; - public rescale(): void; - } } declare module Plottable { class Label extends Component { @@ -539,28 +640,34 @@ declare module Plottable { } } declare module Plottable { + interface _IProjector { + accessor: IAccessor; + scale?: Scale; + } class Renderer extends Component { public renderArea: D3.Selection; public element: D3.Selection; public scales: Scale[]; + }; /** * Creates a Renderer. * * @constructor - * @param {IDataset} [dataset] The dataset associated with the Renderer. + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Renderer. */ - constructor(dataset?: any); + constructor(); + constructor(dataset: any[]); + constructor(dataset: DataSource); /** - * Sets a new dataset on the Renderer. + * Retrieves the current DataSource, or sets a DataSource if the Renderer doesn't yet have one. * - * @param {IDataset} dataset The new dataset to be associated with the Renderer. - * @returns {Renderer} The calling Renderer. + * @param {DataSource} [source] The DataSource the Renderer should use, if it doesn't yet have one. + * @return {DataSource|Renderer} The current DataSource or the calling Renderer. */ - public dataset(dataset: IDataset): Renderer; - public metadata(metadata: IMetadata): Renderer; - public data(data: any[]): Renderer; - public colorAccessor(a: IAccessor): Renderer; - public autorange(): Renderer; + public dataSource(): DataSource; + public dataSource(source: DataSource): Renderer; + public project(attrToSet: string, accessor: any, scale?: Scale): Renderer; + }; } } declare module Plottable { @@ -568,101 +675,54 @@ declare module Plottable { public dataSelection: D3.UpdateSelection; public xScale: Scale; public yScale: Scale; - public autorangeDataOnLayout: boolean; /** * Creates an XYRenderer. * * @constructor - * @param {IDataset} dataset The dataset to render. + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Renderer. * @param {Scale} xScale The x scale to use. * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. */ - constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor); - public xAccessor(accessor: any): XYRenderer; - public yAccessor(accessor: any): XYRenderer; - /** - * Autoranges the scales over the data. - * Actual behavior is dependent on the scales. - */ - public autorange(): XYRenderer; - } -} -declare module Plottable { - class NumericXYRenderer extends XYRenderer { - public dataSelection: D3.UpdateSelection; - public xScale: QuantitiveScale; - public yScale: QuantitiveScale; - public autorangeDataOnLayout: boolean; - /** - * Creates an NumericXYRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor); - /** - * Converts a SelectionArea with pixel ranges to one with data ranges. - * - * @param {SelectionArea} pixelArea The selected area, in pixels. - * @returns {SelectionArea} The corresponding selected area in the domains of the scales. - */ - public invertXYSelectionArea(pixelArea: SelectionArea): SelectionArea; - /** - * Gets the data in a selected area. - * - * @param {SelectionArea} dataArea The selected area. - * @returns {D3.UpdateSelection} The data in the selected area. - */ - public getSelectionFromArea(dataArea: SelectionArea): D3.UpdateSelection; - /** - * Gets the indices of data in a selected area - * - * @param {SelectionArea} dataArea The selected area. - * @returns {number[]} An array of the indices of datapoints in the selected area. - */ - public getDataIndicesFromArea(dataArea: SelectionArea): number[]; + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: any, yAccessor?: any); + public project(attrToSet: string, accessor: any, scale?: Scale): XYRenderer; } } declare module Plottable { - class CircleRenderer extends NumericXYRenderer { + class CircleRenderer extends XYRenderer { /** * Creates a CircleRenderer. * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: any, yAccessor?: any, rAccessor?: any); - public rAccessor(a: any): CircleRenderer; + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: any, yAccessor?: any, rAccessor?: any); + public project(attrToSet: string, accessor: any, scale?: Scale): CircleRenderer; } } declare module Plottable { - class LineRenderer extends NumericXYRenderer { + class LineRenderer extends XYRenderer { /** * Creates a LineRenderer. * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor); + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor); } } declare module Plottable { - class BarRenderer extends NumericXYRenderer { + class BarRenderer extends XYRenderer { public barPaddingPx: number; public dxAccessor: any; /** @@ -670,31 +730,84 @@ declare module Plottable { * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. * @param {IAccessor} [dxAccessor] A function for extracting the width of each bar from the data. * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, dxAccessor?: IAccessor, yAccessor?: IAccessor); - public autorange(): BarRenderer; + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, dxAccessor?: IAccessor, yAccessor?: IAccessor); } } declare module Plottable { - class SquareRenderer extends NumericXYRenderer { + class SquareRenderer extends XYRenderer { /** * Creates a SquareRenderer. * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor, rAccessor?: IAccessor); - public rAccessor(a: any): SquareRenderer; + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor, rAccessor?: IAccessor); + } +} +declare module Plottable { + class CategoryBarRenderer extends XYRenderer { + public xScale: OrdinalScale; + /** + * Creates a CategoryBarRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {OrdinalScale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. + * @param {IAccessor} [widthAccessor] A function for extracting the width position of each bar, in pixels, from the data. + * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. + */ + constructor(dataset: any, xScale: OrdinalScale, yScale: Scale, xAccessor?: IAccessor, widthAccessor?: IAccessor, yAccessor?: IAccessor); + /** + * Selects the bar under the given pixel position. + * + * @param {number} x The pixel x position. + * @param {number} y The pixel y position. + * @param {boolean} [select] Whether or not to select the bar (by classing it "selected"); + * @return {D3.Selection} The selected bar, or null if no bar was selected. + */ + public selectBar(x: number, y: number, select?: boolean): D3.Selection; + /** + * Deselects all bars. + */ + public deselectAll(): void; + } +} +declare module Plottable { + class GridRenderer extends XYRenderer { + public colorScale: Scale; + public xScale: OrdinalScale; + public yScale: OrdinalScale; + /** + * Creates a GridRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {OrdinalScale} xScale The x scale to use. + * @param {OrdinalScale} yScale The y scale to use. + * @param {ColorScale|InterpolatedColorScale} colorScale The color scale to use for each grid + * cell. + * @param {IAccessor|string|number} [xAccessor] An accessor for extracting + * the x position of each grid cell from the data. + * @param {IAccessor|string|number} [yAccessor] An accessor for extracting + * the y position of each grid cell from the data. + * @param {IAccessor|string|number} [valueAccessor] An accessor for + * extracting value of each grid cell from the data. This value will + * be pass through the color scale to determine the color of the cell. + */ + constructor(dataset: any, xScale: OrdinalScale, yScale: OrdinalScale, colorScale: Scale, xAccessor?: any, yAccessor?: any, valueAccessor?: any); } } declare module Plottable { @@ -800,61 +913,6 @@ declare module Plottable { public addCenterComponent(c: Component): StandardChart; } } -declare module Plottable { - class CategoryXYRenderer extends XYRenderer { - public dataSelection: D3.UpdateSelection; - public xScale: OrdinalScale; - public yScale: QuantitiveScale; - /** - * Creates a CategoryXYRenderer with an Ordinal x scale and Quantitive y scale. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, xScale: OrdinalScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor); - } -} -declare module Plottable { - class CategoryBarRenderer extends CategoryXYRenderer { - /** - * Creates a CategoryBarRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. - * @param {IAccessor} [widthAccessor] A function for extracting the width position of each bar, in pixels, from the data. - * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. - */ - constructor(dataset: any, xScale: OrdinalScale, yScale: QuantitiveScale, xAccessor?: IAccessor, widthAccessor?: IAccessor, yAccessor?: IAccessor); - /** - * Sets the width accessor. - * - * @param {any} accessor The new width accessor. - * @returns {CategoryBarRenderer} The calling CategoryBarRenderer. - */ - public widthAccessor(accessor: any): CategoryBarRenderer; - public autorange(): CategoryBarRenderer; - /** - * Selects the bar under the given pixel position. - * - * @param {number} x The pixel x position. - * @param {number} y The pixel y position. - * @param {boolean} [select] Whether or not to select the bar (by classing it "selected"); - * @return {D3.Selection} The selected bar, or null if no bar was selected. - */ - public selectBar(x: number, y: number, select?: boolean): D3.Selection; - /** - * Deselects all bars. - */ - public deselectAll(): void; - } -} declare module Plottable { class Axis extends Component { static yWidth: number; @@ -977,6 +1035,9 @@ declare module Plottable { interface IAccessor { (datum: any, index?: number, metadata?: any): any; } + interface IAppliedAccessor { + (datum: any, index: number): any; + } interface SelectionArea { xMin: number; xMax: number; @@ -988,9 +1049,6 @@ declare module Plottable { data: SelectionArea; } interface IBroadcasterCallback { - (broadcaster: IBroadcaster, ...args: any[]): any; - } - interface IBroadcaster { - registerListener: (cb: IBroadcasterCallback) => IBroadcaster; + (broadcaster: Broadcaster, ...args: any[]): any; } } diff --git a/plottable.js b/plottable.js index 09500a06c0..7aca75a566 100644 --- a/plottable.js +++ b/plottable.js @@ -26,6 +26,26 @@ var Plottable; } Utils.getBBox = getBBox; + function _getParsedStyleValue(style, prop) { + var value = style.getPropertyValue(prop); + if (value == null) { + return 0; + } + return parseFloat(value); + } + + function getElementWidth(elem) { + var style = window.getComputedStyle(elem); + return _getParsedStyleValue(style, "width") + _getParsedStyleValue(style, "padding-left") + _getParsedStyleValue(style, "padding-right") + _getParsedStyleValue(style, "border-left-width") + _getParsedStyleValue(style, "border-right-width"); + } + Utils.getElementWidth = getElementWidth; + + function getElementHeight(elem) { + var style = window.getComputedStyle(elem); + return _getParsedStyleValue(style, "height") + _getParsedStyleValue(style, "padding-top") + _getParsedStyleValue(style, "padding-bottom") + _getParsedStyleValue(style, "border-top-width") + _getParsedStyleValue(style, "border-bottom-width"); + } + Utils.getElementHeight = getElementHeight; + /** * Truncates a text string to a max length, given the element in which to draw the text * @@ -100,6 +120,131 @@ var Plottable; return width; } Utils.getSVGPixelWidth = getSVGPixelWidth; + + function accessorize(accessor) { + if (typeof (accessor) === "function") { + return accessor; + } else if (typeof (accessor) === "string" && accessor[0] !== "#") { + return function (d, i, s) { + return d[accessor]; + }; + } else { + return function (d, i, s) { + return accessor; + }; + } + ; + } + Utils.accessorize = accessorize; + + function applyAccessor(accessor, dataSource) { + var activatedAccessor = accessorize(accessor); + return function (d, i) { + return activatedAccessor(d, i, dataSource.metadata()); + }; + } + Utils.applyAccessor = applyAccessor; + + function uniq(strings) { + var seen = {}; + strings.forEach(function (s) { + return seen[s] = true; + }); + return d3.keys(seen); + } + Utils.uniq = uniq; + + /** + * An associative array that can be keyed by anything (inc objects). + * Uses pointer equality checks which is why this works. + * This power has a price: everything is linear time since it is actually backed by an array... + */ + var StrictEqualityAssociativeArray = (function () { + function StrictEqualityAssociativeArray() { + this.keyValuePairs = []; + } + /** + * Set a new key/value pair in the store. + * + * @param {any} Key to set in the store + * @param {any} Value to set in the store + * @return {boolean} True if key already in store, false otherwise + */ + StrictEqualityAssociativeArray.prototype.set = function (key, value) { + for (var i = 0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + this.keyValuePairs[i][1] = value; + return true; + } + } + this.keyValuePairs.push([key, value]); + return false; + }; + + StrictEqualityAssociativeArray.prototype.get = function (key) { + for (var i = 0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + return this.keyValuePairs[i][1]; + } + } + return undefined; + }; + + StrictEqualityAssociativeArray.prototype.has = function (key) { + for (var i = 0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + return true; + } + } + return false; + }; + + StrictEqualityAssociativeArray.prototype.values = function () { + return this.keyValuePairs.map(function (x) { + return x[1]; + }); + }; + + StrictEqualityAssociativeArray.prototype.delete = function (key) { + for (var i = 0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + this.keyValuePairs.splice(i, 1); + return true; + } + } + return false; + }; + return StrictEqualityAssociativeArray; + })(); + Utils.StrictEqualityAssociativeArray = StrictEqualityAssociativeArray; + + var IDCounter = (function () { + function IDCounter() { + this.counter = {}; + } + IDCounter.prototype.setDefault = function (id) { + if (this.counter[id] == null) { + this.counter[id] = 0; + } + }; + + IDCounter.prototype.increment = function (id) { + this.setDefault(id); + return ++this.counter[id]; + }; + + IDCounter.prototype.decrement = function (id) { + this.setDefault(id); + return --this.counter[id]; + }; + + IDCounter.prototype.get = function (id) { + this.setDefault(id); + return this.counter[id]; + }; + return IDCounter; + })(); + Utils.IDCounter = IDCounter; })(Plottable.Utils || (Plottable.Utils = {})); var Utils = Plottable.Utils; })(Plottable || (Plottable = {})); @@ -135,8 +280,87 @@ var Plottable; /// var Plottable; (function (Plottable) { - var Component = (function () { + var PlottableObject = (function () { + function PlottableObject() { + this._plottableID = PlottableObject.nextID++; + } + PlottableObject.nextID = 0; + return PlottableObject; + })(); + Plottable.PlottableObject = PlottableObject; +})(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) { + var Broadcaster = (function (_super) { + __extends(Broadcaster, _super); + function Broadcaster() { + _super.apply(this, arguments); + this.listener2Callback = new Plottable.Utils.StrictEqualityAssociativeArray(); + } + /** + * Registers a callback to be called when the broadcast method is called. Also takes a listener which + * is used to support deregistering the same callback later, by passing in the same listener. + * If there is already a callback associated with that listener, then the callback will be replaced. + * + * @param listener The listener associated with the callback. + * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. + * @returns {Broadcaster} this object + */ + Broadcaster.prototype.registerListener = function (listener, callback) { + this.listener2Callback.set(listener, callback); + return this; + }; + + /** + * Call all listening callbacks, optionally with arguments passed through. + * + * @param ...args A variable number of optional arguments + * @returns {Broadcaster} this object + */ + Broadcaster.prototype._broadcast = function () { + var _this = this; + var args = []; + for (var _i = 0; _i < (arguments.length - 0); _i++) { + args[_i] = arguments[_i + 0]; + } + this.listener2Callback.values().forEach(function (callback) { + return callback(_this, args); + }); + return this; + }; + + /** + * Registers deregister the callback associated with a listener. + * + * @param listener The listener to deregister. + * @returns {Broadcaster} this object + */ + Broadcaster.prototype.deregisterListener = function (listener) { + var listenerWasFound = this.listener2Callback.delete(listener); + if (listenerWasFound) { + return this; + } else { + throw new Error("Attempted to deregister listener, but listener not found"); + } + }; + return Broadcaster; + })(Plottable.PlottableObject); + Plottable.Broadcaster = Broadcaster; +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var Component = (function (_super) { + __extends(Component, _super); function Component() { + _super.apply(this, arguments); this.interactionsToRegister = []; this.boxes = []; this.clipPathEnabled = false; @@ -166,6 +390,9 @@ var Plottable; // svg node gets the "plottable" CSS class this.rootSVG = element; this.rootSVG.classed("plottable", true); + + // visible overflow for firefox https://stackoverflow.com/questions/5926986/why-does-firefox-appear-to-truncate-embedded-svgs + this.rootSVG.style("overflow", "visible"); this.element = element.append("g"); this.isTopLevelComponent = true; } else { @@ -216,8 +443,10 @@ var Plottable; // we are the root node, retrieve height/width from root SVG xOrigin = 0; yOrigin = 0; - availableWidth = parseFloat(this.rootSVG.attr("width")); - availableHeight = parseFloat(this.rootSVG.attr("height")); + + var elem = this.rootSVG.node(); + availableWidth = Plottable.Utils.getElementWidth(elem); + availableHeight = Plottable.Utils.getElementHeight(elem); } else { throw new Error("null arguments cannot be passed to _computeLayout() on a non-root node"); } @@ -348,9 +577,8 @@ var Plottable; Component.prototype.generateClipPath = function () { // The clip path will prevent content from overflowing its component space. - var clipPathId = Component.clipPathId++; - this.element.attr("clip-path", "url(#clipPath" + clipPathId + ")"); - var clipPathParent = this.boxContainer.append("clipPath").attr("id", "clipPath" + clipPathId); + this.element.attr("clip-path", "url(#clipPath" + this._plottableID + ")"); + var clipPathParent = this.boxContainer.append("clipPath").attr("id", "clipPath" + this._plottableID); this.addBox("clip-rect", clipPathParent); }; @@ -463,21 +691,15 @@ var Plottable; return cg; } }; - Component.clipPathId = 0; return Component; - })(); + })(Plottable.PlottableObject); Plottable.Component = Component; })(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) { - var Scale = (function () { + var Scale = (function (_super) { + __extends(Scale, _super); /** * Creates a new Scale. * @@ -485,9 +707,74 @@ var Plottable; * @param {D3.Scale.Scale} scale The D3 scale backing the Scale. */ function Scale(scale) { - this._broadcasterCallbacks = []; + _super.call(this); + this._autoDomain = true; + this.rendererID2Perspective = {}; + this.dataSourceReferenceCounter = new Plottable.Utils.IDCounter(); + this.isAutorangeUpToDate = false; + this._autoNice = false; + this._autoPad = false; this._d3Scale = scale; } + Scale.prototype._getCombinedExtent = function () { + var perspectives = d3.values(this.rendererID2Perspective); + var extents = perspectives.map(function (p) { + var source = p.dataSource; + var accessor = p.accessor; + return source._getExtent(accessor); + }); + return extents; + }; + + /** + * Modify the domain on the scale so that it includes the extent of all + * perspectives it depends on. Extent: The (min, max) pair for a + * QuantitiativeScale, all covered strings for an OrdinalScale. + * Perspective: A combination of a DataSource and an Accessor that + * represents a view in to the data. + */ + Scale.prototype.autorangeDomain = function () { + this.isAutorangeUpToDate = true; + this._setDomain(this._getCombinedExtent()); + return this; + }; + + Scale.prototype._autoDomainIfNeeded = function () { + if (!this.isAutorangeUpToDate && this._autoDomain) { + this.autorangeDomain(); + } + }; + + Scale.prototype._addPerspective = function (rendererIDAttr, dataSource, accessor) { + var _this = this; + if (this.rendererID2Perspective[rendererIDAttr] != null) { + this._removePerspective(rendererIDAttr); + } + this.rendererID2Perspective[rendererIDAttr] = { dataSource: dataSource, accessor: accessor }; + + var dataSourceID = dataSource._plottableID; + if (this.dataSourceReferenceCounter.increment(dataSourceID) === 1) { + dataSource.registerListener(this, function () { + return _this.isAutorangeUpToDate = false; + }); + } + + this.isAutorangeUpToDate = false; + return this; + }; + + Scale.prototype._removePerspective = function (rendererIDAttr) { + var dataSource = this.rendererID2Perspective[rendererIDAttr].dataSource; + var dataSourceID = dataSource._plottableID; + if (this.dataSourceReferenceCounter.decrement(dataSourceID) === 0) { + dataSource.deregisterListener(this); + } + + delete this.rendererID2Perspective[rendererIDAttr]; + this.isAutorangeUpToDate = false; + return this; + }; + /** * Returns the range value corresponding to a given domain value. * @@ -495,24 +782,29 @@ var Plottable; * @returns {any} The range value corresponding to the supplied domain value. */ Scale.prototype.scale = function (value) { + this._autoDomainIfNeeded(); return this._d3Scale(value); }; Scale.prototype.domain = function (values) { - var _this = this; if (values == null) { + this._autoDomainIfNeeded(); return this._d3Scale.domain(); } else { - this._d3Scale.domain(values); - this._broadcasterCallbacks.forEach(function (b) { - return b(_this); - }); + this._autoDomain = false; + this._setDomain(values); return this; } }; + Scale.prototype._setDomain = function (values) { + this._d3Scale.domain(values); + this._broadcast(); + }; + Scale.prototype.range = function (values) { if (values == null) { + this._autoDomainIfNeeded(); return this._d3Scale.range(); } else { this._d3Scale.range(values); @@ -528,104 +820,13 @@ var Plottable; Scale.prototype.copy = function () { return new Scale(this._d3Scale.copy()); }; - - /** - * Registers a callback to be called when the scale's domain is changed. - * - * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. - * @returns {Scale} The Calling Scale. - */ - Scale.prototype.registerListener = function (callback) { - this._broadcasterCallbacks.push(callback); - return this; - }; - - /** - * Expands the Scale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data - * @returns {Scale} The Scale. - */ - Scale.prototype.widenDomainOnData = function (data, accessor) { - // no-op; implementation is sublcass-dependent - return this; - }; return Scale; - })(); + })(Plottable.Broadcaster); Plottable.Scale = Scale; - - var OrdinalScale = (function (_super) { - __extends(OrdinalScale, _super); - /** - * Creates a new OrdinalScale. Domain and Range are set later. - * - * @constructor - */ - function OrdinalScale() { - _super.call(this, d3.scale.ordinal()); - this.END_PADDING = 0.5; - this._range = [0, 1]; - } - OrdinalScale.prototype.domain = function (values) { - var _this = this; - if (values == null) { - return this._d3Scale.domain(); - } else { - this._d3Scale.domain(values); - this._broadcasterCallbacks.forEach(function (b) { - return b(_this); - }); - this._d3Scale.rangePoints(this.range(), 2 * this.END_PADDING); // d3 scale takes total padding - return this; - } - }; - - OrdinalScale.prototype.range = function (values) { - if (values == null) { - return this._range; - } else { - this._range = values; - this._d3Scale.rangePoints(values, 2 * this.END_PADDING); // d3 scale takes total padding - return this; - } - }; - - OrdinalScale.prototype.widenDomainOnData = function (data, accessor) { - var changed = false; - var newDomain = this.domain(); - var a; - if (accessor == null) { - a = function (d, i) { - return d; - }; - } else if (typeof (accessor) === "string") { - a = function (d, i) { - return d[accessor]; - }; - } else if (typeof (accessor) === "function") { - a = accessor; - } else { - a = function (d, i) { - return accessor; - }; - } - data.map(a).forEach(function (d) { - if (newDomain.indexOf(d) === -1) { - newDomain.push(d); - changed = true; - } - }); - if (changed) { - this.domain(newDomain); - } - return this; - }; - return OrdinalScale; - })(Scale); - Plottable.OrdinalScale = OrdinalScale; - +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { var QuantitiveScale = (function (_super) { __extends(QuantitiveScale, _super); /** @@ -638,6 +839,28 @@ var Plottable; _super.call(this, scale); this.lastRequestedTickCount = 10; } + QuantitiveScale.prototype._getCombinedExtent = function () { + var extents = _super.prototype._getCombinedExtent.call(this); + var starts = extents.map(function (e) { + return e[0]; + }); + var ends = extents.map(function (e) { + return e[1]; + }); + return [d3.min(starts), d3.max(ends)]; + }; + + QuantitiveScale.prototype.autorangeDomain = function () { + _super.prototype.autorangeDomain.call(this); + if (this._autoPad) { + this.padDomain(); + } + if (this._autoNice) { + this.nice(); + } + return this; + }; + /** * Retrieves the domain value corresponding to a supplied range value. * @@ -657,31 +880,8 @@ var Plottable; return new QuantitiveScale(this._d3Scale.copy()); }; - /** - * Expands the QuantitiveScale's domain to cover the new region. - * - * @param {number[]} newDomain The additional domain to be covered by the QuantitiveScale. - * @returns {QuantitiveScale} The scale. - */ - QuantitiveScale.prototype.widenDomain = function (newDomain) { - var currentDomain = this.domain(); - var wideDomain = [Math.min(newDomain[0], currentDomain[0]), Math.max(newDomain[1], currentDomain[1])]; - this.domain(wideDomain); - return this; - }; - - /** - * Expands the QuantitiveScale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data. - * @returns {QuantitiveScale} The scale. - */ - QuantitiveScale.prototype.widenDomainOnData = function (data, accessor) { - var extent = d3.extent(data, accessor); - this.widenDomain(extent); - return this; + QuantitiveScale.prototype.domain = function (values) { + return _super.prototype.domain.call(this, values); }; QuantitiveScale.prototype.interpolate = function (factory) { @@ -717,7 +917,7 @@ var Plottable; */ QuantitiveScale.prototype.nice = function (count) { this._d3Scale.nice(count); - this.domain(this._d3Scale.domain()); // nice() can change the domain, so update all listeners + this._setDomain(this._d3Scale.domain()); // nice() can change the domain, so update all listeners return this; }; @@ -756,18 +956,20 @@ var Plottable; var currentDomain = this.domain(); var extent = currentDomain[1] - currentDomain[0]; var newDomain = [currentDomain[0] - padProportion / 2 * extent, currentDomain[1] + padProportion / 2 * extent]; - this.domain(newDomain); + this._setDomain(newDomain); return this; }; return QuantitiveScale; - })(Scale); + })(Plottable.Scale); Plottable.QuantitiveScale = QuantitiveScale; - +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { var LinearScale = (function (_super) { __extends(LinearScale, _super); function LinearScale(scale) { _super.call(this, scale == null ? d3.scale.linear() : scale); - this.domain([Infinity, -Infinity]); } /** * Creates a copy of the LinearScale with the same domain and range but without any registered listeners. @@ -778,16 +980,100 @@ var Plottable; return new LinearScale(this._d3Scale.copy()); }; return LinearScale; - })(QuantitiveScale); + })(Plottable.QuantitiveScale); Plottable.LinearScale = LinearScale; +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var OrdinalScale = (function (_super) { + __extends(OrdinalScale, _super); + /** + * Creates a new OrdinalScale. Domain and Range are set later. + * + * @constructor + */ + function OrdinalScale() { + _super.call(this, d3.scale.ordinal()); + this._range = [0, 1]; + this._rangeType = "points"; + // Padding as a proportion of the spacing between domain values + this._innerPadding = 0.3; + this._outerPadding = 0.5; + } + OrdinalScale.prototype._getCombinedExtent = function () { + var extents = _super.prototype._getCombinedExtent.call(this); + var concatenatedExtents = []; + extents.forEach(function (e) { + concatenatedExtents = concatenatedExtents.concat(e); + }); + return Plottable.Utils.uniq(concatenatedExtents); + }; + + OrdinalScale.prototype.domain = function (values) { + return _super.prototype.domain.call(this, values); + }; + + OrdinalScale.prototype._setDomain = function (values) { + _super.prototype._setDomain.call(this, values); + this.range(this.range()); // update range + }; + + OrdinalScale.prototype.range = function (values) { + if (values == null) { + this._autoDomainIfNeeded(); + return this._range; + } else { + this._range = values; + if (this._rangeType === "points") { + this._d3Scale.rangePoints(values, 2 * this._outerPadding); // d3 scale takes total padding + } else if (this._rangeType === "bands") { + this._d3Scale.rangeBands(values, this._innerPadding, this._outerPadding); + } + return this; + } + }; + + /** + * Returns the width of the range band. Only valid when rangeType is set to "bands". + * + * @returns {number} The range band width or 0 if rangeType isn't "bands". + */ + OrdinalScale.prototype.rangeBand = function () { + this._autoDomainIfNeeded(); + return this._d3Scale.rangeBand(); + }; + OrdinalScale.prototype.rangeType = function (rangeType, outerPadding, innerPadding) { + if (rangeType == null) { + return this._rangeType; + } else { + if (!(rangeType === "points" || rangeType === "bands")) { + throw new Error("Unsupported range type: " + rangeType); + } + this._rangeType = rangeType; + if (outerPadding != null) + this._outerPadding = outerPadding; + if (innerPadding != null) + this._innerPadding = innerPadding; + return this; + } + }; + return OrdinalScale; + })(Plottable.Scale); + Plottable.OrdinalScale = OrdinalScale; +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { var ColorScale = (function (_super) { __extends(ColorScale, _super); /** * Creates a ColorScale. * * @constructor - * @param {string} [scaleType] the type of color scale to create (Category10/Category20/Category20b/Category20c) + * @param {string} [scaleType] the type of color scale to create + * (Category10/Category20/Category20b/Category20c). */ function ColorScale(scaleType) { var scale; @@ -822,9 +1108,186 @@ var Plottable; _super.call(this, scale); } return ColorScale; - })(Scale); + })(Plottable.Scale); Plottable.ColorScale = ColorScale; })(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var InterpolatedColorScale = (function (_super) { + __extends(InterpolatedColorScale, _super); + /** + * Creates a InterpolatedColorScale. + * + * @constructor + * @param {string|string[]} [scaleType] the type of color scale to create + * (reds/blues/posneg). Default is "reds". An array of color values + * with at least 2 values may also be passed (e.g. ["#FF00FF", "red", + * "dodgerblue"], in which case the resulting scale will interpolate + * linearly between the color values across the domain. + */ + function InterpolatedColorScale(scaleType) { + var scale; + if (scaleType instanceof Array) { + scale = InterpolatedColorScale.INTERPOLATE_COLORS(scaleType); + } else { + switch (scaleType) { + case "blues": + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["blues"]); + break; + case "posneg": + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["posneg"]); + break; + case "reds": + default: + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["reds"]); + break; + } + } + _super.call(this, scale); + } + /** + * Converts the string array into a linear d3 scale. + * + * d3 doesn't accept more than 2 range values unless we use a ordinal + * scale. So, in order to interpolate smoothly between the full color + * range, we must override the interpolator and compute the color values + * manually. + * + * @param {string[]} [colors] an array of strings representing color + * values in hex ("#FFFFFF") or keywords ("white"). + * @returns a linear d3 scale. + */ + InterpolatedColorScale.INTERPOLATE_COLORS = function (colors) { + if (colors.length < 2) + throw new Error("Color scale arrays must have at least two elements."); + return d3.scale.linear().range([0, 1]).interpolate(function (ignored) { + return function (t) { + // Clamp t parameter to [0,1] + t = Math.max(0, Math.min(1, t)); + + // Determine indices for colors + var tScaled = t * (colors.length - 1); + var i0 = Math.floor(tScaled); + var i1 = Math.ceil(tScaled); + var frac = (tScaled - i0); + + // Interpolate in the L*a*b color space + return d3.interpolateLab(colors[i0], colors[i1])(frac); + }; + }); + }; + InterpolatedColorScale.COLOR_SCALES = { + reds: [ + "#FFFFFF", + "#FFF6E1", + "#FEF4C0", + "#FED976", + "#FEB24C", + "#FD8D3C", + "#FC4E2A", + "#E31A1C", + "#B10026" + ], + blues: [ + "#FFFFFF", + "#CCFFFF", + "#A5FFFD", + "#85F7FB", + "#6ED3EF", + "#55A7E0", + "#417FD0", + "#2545D3", + "#0B02E1" + ], + posneg: [ + "#0B02E1", + "#2545D3", + "#417FD0", + "#55A7E0", + "#6ED3EF", + "#85F7FB", + "#A5FFFD", + "#CCFFFF", + "#FFFFFF", + "#FFF6E1", + "#FEF4C0", + "#FED976", + "#FEB24C", + "#FD8D3C", + "#FC4E2A", + "#E31A1C", + "#B10026" + ] + }; + return InterpolatedColorScale; + })(Plottable.LinearScale); + Plottable.InterpolatedColorScale = InterpolatedColorScale; +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var DataSource = (function (_super) { + __extends(DataSource, _super); + /** + * Creates a new DataSource. + * + * @constructor + * @param {any[]} data + * @param {any} metadata An object containing additional information. + */ + function DataSource(data, metadata) { + if (typeof data === "undefined") { data = []; } + if (typeof metadata === "undefined") { metadata = {}; } + _super.call(this); + this._data = data; + this._metadata = metadata; + this.accessor2cachedExtent = new Plottable.Utils.StrictEqualityAssociativeArray(); + } + DataSource.prototype.data = function (data) { + if (data == null) { + return this._data; + } else { + this._data = data; + this.accessor2cachedExtent = new Plottable.Utils.StrictEqualityAssociativeArray(); + this._broadcast(); + return this; + } + }; + + DataSource.prototype.metadata = function (metadata) { + if (metadata == null) { + return this._metadata; + } else { + this._metadata = metadata; + this.accessor2cachedExtent = new Plottable.Utils.StrictEqualityAssociativeArray(); + this._broadcast(); + return this; + } + }; + + DataSource.prototype._getExtent = function (accessor) { + var cachedExtent = this.accessor2cachedExtent.get(accessor); + if (cachedExtent === undefined) { + cachedExtent = this.computeExtent(accessor); + this.accessor2cachedExtent.set(accessor, cachedExtent); + } + return cachedExtent; + }; + + DataSource.prototype.computeExtent = function (accessor) { + var appliedAccessor = Plottable.Utils.applyAccessor(accessor, this); + var mappedData = this._data.map(appliedAccessor); + if (typeof (appliedAccessor(this._data[0], 0)) === "string") { + return Plottable.Utils.uniq(mappedData); + } else { + return d3.extent(mappedData); + } + }; + return DataSource; + })(Plottable.Broadcaster); + Plottable.DataSource = DataSource; +})(Plottable || (Plottable = {})); /// var Plottable; (function (Plottable) { @@ -1051,73 +1514,6 @@ var Plottable; })(Interaction); Plottable.AreaInteraction = AreaInteraction; - var ZoomCallbackGenerator = (function () { - function ZoomCallbackGenerator() { - this.xScaleMappings = []; - this.yScaleMappings = []; - } - /** - * Adds listen-update pair of X scales. - * - * @param {QuantitiveScale} listenerScale An X scale to listen for events on. - * @param {QuantitiveScale} [targetScale] An X scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - ZoomCallbackGenerator.prototype.addXScale = function (listenerScale, targetScale) { - if (targetScale == null) { - targetScale = listenerScale; - } - this.xScaleMappings.push([listenerScale, targetScale]); - return this; - }; - - /** - * Adds listen-update pair of Y scales. - * - * @param {QuantitiveScale} listenerScale A Y scale to listen for events on. - * @param {QuantitiveScale} [targetScale] A Y scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - ZoomCallbackGenerator.prototype.addYScale = function (listenerScale, targetScale) { - if (targetScale == null) { - targetScale = listenerScale; - } - this.yScaleMappings.push([listenerScale, targetScale]); - return this; - }; - - ZoomCallbackGenerator.prototype.updateScale = function (referenceScale, targetScale, pixelMin, pixelMax) { - var originalDomain = referenceScale.domain(); - var newDomain = [referenceScale.invert(pixelMin), referenceScale.invert(pixelMax)]; - var sameDirection = (newDomain[0] < newDomain[1]) === (originalDomain[0] < originalDomain[1]); - if (!sameDirection) { - newDomain.reverse(); - } - targetScale.domain(newDomain); - }; - - /** - * Generates a callback that can be passed to Interactions. - * - * @returns {(area: SelectionArea) => void} A callback that updates the scales previously specified. - */ - ZoomCallbackGenerator.prototype.getCallback = function () { - var _this = this; - return function (area) { - _this.xScaleMappings.forEach(function (sm) { - _this.updateScale(sm[0], sm[1], area.xMin, area.xMax); - }); - _this.yScaleMappings.forEach(function (sm) { - _this.updateScale(sm[0], sm[1], area.yMin, area.yMax); - }); - }; - }; - return ZoomCallbackGenerator; - })(); - Plottable.ZoomCallbackGenerator = ZoomCallbackGenerator; - var MousemoveInteraction = (function (_super) { __extends(MousemoveInteraction, _super); function MousemoveInteraction(componentToListenTo) { @@ -1219,60 +1615,6 @@ var Plottable; return KeyInteraction; })(Interaction); Plottable.KeyInteraction = KeyInteraction; - - var CrosshairsInteraction = (function (_super) { - __extends(CrosshairsInteraction, _super); - function CrosshairsInteraction(renderer) { - var _this = this; - _super.call(this, renderer); - this.renderer = renderer; - renderer.xScale.registerListener(function () { - return _this.rescale(); - }); - renderer.yScale.registerListener(function () { - return _this.rescale(); - }); - } - CrosshairsInteraction.prototype._anchor = function (hitBox) { - _super.prototype._anchor.call(this, hitBox); - var container = this.renderer.foregroundContainer.append("g").classed("crosshairs", true); - this.circle = container.append("circle").classed("centerpoint", true); - this.xLine = container.append("path").classed("x-line", true); - this.yLine = container.append("path").classed("y-line", true); - this.circle.attr("r", 5); - }; - - CrosshairsInteraction.prototype.mousemove = function (x, y) { - this.lastx = x; - this.lasty = y; - var domainX = this.renderer.xScale.invert(x); - var data = this.renderer._data; - var xA = this.renderer._getAppliedAccessor(this.renderer._xAccessor); - var yA = this.renderer._getAppliedAccessor(this.renderer._yAccessor); - var dataIndex = Plottable.OSUtils.sortedIndex(domainX, data, xA); - dataIndex = dataIndex > 0 ? dataIndex - 1 : 0; - var dataPoint = data[dataIndex]; - - var dataX = xA(dataPoint, dataIndex); - var dataY = yA(dataPoint, dataIndex); - var pixelX = this.renderer.xScale.scale(dataX); - var pixelY = this.renderer.yScale.scale(dataY); - this.circle.attr("cx", pixelX).attr("cy", pixelY); - - var width = this.renderer.availableWidth; - var height = this.renderer.availableHeight; - this.xLine.attr("d", "M 0 " + pixelY + " L " + width + " " + pixelY); - this.yLine.attr("d", "M " + pixelX + " 0 L " + pixelX + " " + height); - }; - - CrosshairsInteraction.prototype.rescale = function () { - if (this.lastx != null) { - this.mousemove(this.lastx, this.lasty); - } - }; - return CrosshairsInteraction; - })(MousemoveInteraction); - Plottable.CrosshairsInteraction = CrosshairsInteraction; })(Plottable || (Plottable = {})); /// var Plottable; @@ -1401,24 +1743,17 @@ var Plottable; })(Label); Plottable.AxisLabel = AxisLabel; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var Renderer = (function (_super) { __extends(Renderer, _super); - // A perf-efficient approach to rendering scale changes would be to transform - // the container rather than re-render. In the event that the data is changed, - // it will be necessary to do a regular rerender. - /** - * Creates a Renderer. - * - * @constructor - * @param {IDataset} [dataset] The dataset associated with the Renderer. - */ function Renderer(dataset) { + var _this = this; _super.call(this); this._animate = false; this._hasRendered = false; + this._projectors = {}; this._rerenderUpdateSelection = false; // A perf-efficient manner of rendering would be to calculate attributes only // on new nodes, and assume that old nodes (ie the update selection) can @@ -1430,58 +1765,81 @@ var Plottable; this._fixedWidth = false; this._fixedHeight = false; this.classed("renderer", true); + if (dataset != null) { - if (dataset.data == null) { - this.data(dataset); + if (typeof dataset.data === "function") { + this._dataSource = dataset; } else { - this.data(dataset.data); - if (dataset.metadata != null) { - this.metadata(dataset.metadata); - } + this._dataSource = new Plottable.DataSource(dataset); } + this._dataSource.registerListener(this, function () { + return _this._render(); + }); + } else { + this._dataSource = new Plottable.DataSource(); } - this.colorAccessor(Renderer.defaultColorAccessor); } - /** - * Sets a new dataset on the Renderer. - * - * @param {IDataset} dataset The new dataset to be associated with the Renderer. - * @returns {Renderer} The calling Renderer. - */ - Renderer.prototype.dataset = function (dataset) { - this.data(dataset.data); - this.metadata(dataset.metadata); - return this; + Renderer.prototype.dataSource = function (source) { + var _this = this; + if (source == null) { + return this._dataSource; + } else if (this._dataSource == null) { + this._dataSource = source; + this._dataSource.registerListener(this, function () { + return _this._render(); + }); + return this; + } else { + throw new Error("Can't set a new DataSource on the Renderer if it already has one."); + } }; - Renderer.prototype.metadata = function (metadata) { - var oldCSSClass = this._metadata != null ? this._metadata.cssClass : null; - this.classed(oldCSSClass, false); - this._metadata = metadata; - this.classed(this._metadata.cssClass, true); - this._rerenderUpdateSelection = true; + Renderer.prototype.project = function (attrToSet, accessor, scale) { + var _this = this; + var rendererIDAttr = this._plottableID + attrToSet; + var currentProjection = this._projectors[attrToSet]; + var existingScale = (currentProjection != null) ? currentProjection.scale : null; + if (scale == null) { + scale = existingScale; + } + if (existingScale != null) { + existingScale._removePerspective(rendererIDAttr); + existingScale.deregisterListener(this); + } + if (scale != null) { + scale._addPerspective(rendererIDAttr, this.dataSource(), accessor); + scale.registerListener(this, function () { + return _this._render(); + }); + } + this._projectors[attrToSet] = { accessor: accessor, scale: scale }; this._requireRerender = true; + this._rerenderUpdateSelection = true; return this; }; - Renderer.prototype.data = function (data) { - this._data = data; - this._requireRerender = true; - return this; + Renderer.prototype._generateAttrToProjector = function () { + var _this = this; + var h = {}; + d3.keys(this._projectors).forEach(function (a) { + var projector = _this._projectors[a]; + var accessor = Plottable.Utils.applyAccessor(projector.accessor, _this.dataSource()); + var scale = projector.scale; + var fn = scale == null ? accessor : function (d, i) { + return scale.scale(accessor(d, i)); + }; + h[a] = fn; + }); + return h; }; Renderer.prototype._render = function () { - this._hasRendered = true; - this._paint(); - this._requireRerender = false; - this._rerenderUpdateSelection = false; - return this; - }; - - Renderer.prototype.colorAccessor = function (a) { - this._colorAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; + if (this.element != null) { + this._hasRendered = true; + this._paint(); + this._requireRerender = false; + this._rerenderUpdateSelection = false; + } return this; }; @@ -1489,33 +1847,11 @@ var Plottable; // no-op }; - Renderer.prototype.autorange = function () { - // no-op - return this; - }; - Renderer.prototype._anchor = function (element) { _super.prototype._anchor.call(this, element); this.renderArea = this.content.append("g").classed("render-area", true); return this; }; - - Renderer.prototype._getAppliedAccessor = function (accessor) { - var _this = this; - if (typeof (accessor) === "function") { - return function (d, i) { - return accessor(d, i, _this._metadata); - }; - } else if (typeof (accessor) === "string") { - return function (d, i) { - return d[accessor]; - }; - } else { - return function (d, i) { - return accessor; - }; - } - }; Renderer.defaultColorAccessor = function (d) { return "#1f77b4"; }; @@ -1523,7 +1859,7 @@ var Plottable; })(Plottable.Component); Plottable.Renderer = Renderer; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var XYRenderer = (function (_super) { @@ -1532,42 +1868,38 @@ var Plottable; * Creates an XYRenderer. * * @constructor - * @param {IDataset} dataset The dataset to render. + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Renderer. * @param {Scale} xScale The x scale to use. * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. */ function XYRenderer(dataset, xScale, yScale, xAccessor, yAccessor) { - var _this = this; + if (typeof xAccessor === "undefined") { xAccessor = "x"; } + if (typeof yAccessor === "undefined") { yAccessor = "y"; } _super.call(this, dataset); - this.autorangeDataOnLayout = true; this.classed("xy-renderer", true); - this._xAccessor = (xAccessor != null) ? xAccessor : "x"; // default - this._yAccessor = (yAccessor != null) ? yAccessor : "y"; // default - - this.xScale = xScale; - this.yScale = yScale; - - this.xScale.registerListener(function () { - return _this.rescale(); - }); - this.yScale.registerListener(function () { - return _this.rescale(); - }); + this.project("x", xAccessor, xScale); + this.project("y", yAccessor, yScale); } - XYRenderer.prototype.xAccessor = function (accessor) { - this._xAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - }; + XYRenderer.prototype.project = function (attrToSet, accessor, scale) { + _super.prototype.project.call(this, attrToSet, accessor, scale); - XYRenderer.prototype.yAccessor = function (accessor) { - this._yAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; + // We only want padding and nice-ing on scales that will correspond to axes / pixel layout. + // So when we get an "x" or "y" scale, enable autoNiceing and autoPadding. + if (attrToSet === "x") { + this._xAccessor = this._projectors["x"].accessor; + this.xScale = this._projectors["x"].scale; + this.xScale._autoNice = true; + this.xScale._autoPad = true; + } + if (attrToSet === "y") { + this._yAccessor = this._projectors["y"].accessor; + this.yScale = this._projectors["y"].scale; + this.yScale._autoNice = true; + this.yScale._autoPad = true; + } return this; }; @@ -1576,119 +1908,19 @@ var Plottable; _super.prototype._computeLayout.call(this, xOffset, yOffset, availableWidth, availableHeight); this.xScale.range([0, this.availableWidth]); this.yScale.range([this.availableHeight, 0]); - if (this.autorangeDataOnLayout) { - this.autorange(); - } - return this; - }; - - /** - * Autoranges the scales over the data. - * Actual behavior is dependent on the scales. - */ - XYRenderer.prototype.autorange = function () { - var _this = this; - _super.prototype.autorange.call(this); - var data = this._data; - var xA = function (d) { - return _this._getAppliedAccessor(_this._xAccessor)(d, null); - }; - this.xScale.widenDomainOnData(data, xA); - - var yA = function (d) { - return _this._getAppliedAccessor(_this._yAccessor)(d, null); - }; - this.yScale.widenDomainOnData(data, yA); return this; }; - - XYRenderer.prototype.rescale = function () { - if (this.element != null && this._hasRendered) { - this._render(); - } - }; - return XYRenderer; - })(Plottable.Renderer); - Plottable.XYRenderer = XYRenderer; -})(Plottable || (Plottable = {})); -/// -var Plottable; -(function (Plottable) { - var NumericXYRenderer = (function (_super) { - __extends(NumericXYRenderer, _super); - /** - * Creates an NumericXYRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - function NumericXYRenderer(dataset, xScale, yScale, xAccessor, yAccessor) { - _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); - this.autorangeDataOnLayout = true; - this.classed("numeric-xy-renderer", true); - } - /** - * Converts a SelectionArea with pixel ranges to one with data ranges. - * - * @param {SelectionArea} pixelArea The selected area, in pixels. - * @returns {SelectionArea} The corresponding selected area in the domains of the scales. - */ - NumericXYRenderer.prototype.invertXYSelectionArea = function (pixelArea) { - var xMin = this.xScale.invert(pixelArea.xMin); - var xMax = this.xScale.invert(pixelArea.xMax); - var yMin = this.yScale.invert(pixelArea.yMin); - var yMax = this.yScale.invert(pixelArea.yMax); - var dataArea = { xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax }; - return dataArea; - }; - - NumericXYRenderer.prototype.getDataFilterFunction = function (dataArea) { - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var filterFunction = function (d, i) { - var x = xA(d, i); - var y = yA(d, i); - return Plottable.Utils.inRange(x, dataArea.xMin, dataArea.xMax) && Plottable.Utils.inRange(y, dataArea.yMin, dataArea.yMax); - }; - return filterFunction; - }; - - /** - * Gets the data in a selected area. - * - * @param {SelectionArea} dataArea The selected area. - * @returns {D3.UpdateSelection} The data in the selected area. - */ - NumericXYRenderer.prototype.getSelectionFromArea = function (dataArea) { - var filterFunction = this.getDataFilterFunction(dataArea); - return this.dataSelection.filter(filterFunction); - }; - - /** - * Gets the indices of data in a selected area - * - * @param {SelectionArea} dataArea The selected area. - * @returns {number[]} An array of the indices of datapoints in the selected area. - */ - NumericXYRenderer.prototype.getDataIndicesFromArea = function (dataArea) { - var filterFunction = this.getDataFilterFunction(dataArea); - var results = []; - this._data.forEach(function (d, i) { - if (filterFunction(d, i)) { - results.push(i); - } - }); - return results; + + XYRenderer.prototype.rescale = function () { + if (this.element != null && this._hasRendered) { + this._render(); + } }; - return NumericXYRenderer; - })(Plottable.XYRenderer); - Plottable.NumericXYRenderer = NumericXYRenderer; + return XYRenderer; + })(Plottable.Renderer); + Plottable.XYRenderer = XYRenderer; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var CircleRenderer = (function (_super) { @@ -1698,46 +1930,45 @@ var Plottable; * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. */ function CircleRenderer(dataset, xScale, yScale, xAccessor, yAccessor, rAccessor) { _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); - this._rAccessor = (rAccessor != null) ? rAccessor : CircleRenderer.defaultRAccessor; + + /* this._rAccessor = (rAccessor != null) ? rAccessor : CircleRenderer.defaultRAccessor;*/ this.classed("circle-renderer", true); + this.project("r", 3); + this.project("fill", "#00ffaa"); } - CircleRenderer.prototype.rAccessor = function (a) { - this._rAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; + CircleRenderer.prototype.project = function (attrToSet, accessor, scale) { + attrToSet = attrToSet === "cx" ? "x" : attrToSet; + attrToSet = attrToSet === "cy" ? "y" : attrToSet; + _super.prototype.project.call(this, attrToSet, accessor, scale); return this; }; CircleRenderer.prototype._paint = function () { - var _this = this; _super.prototype._paint.call(this); - var cx = function (d, i) { - return _this.xScale.scale(_this._getAppliedAccessor(_this._xAccessor)(d, i)); - }; - var cy = function (d, i) { - return _this.yScale.scale(_this._getAppliedAccessor(_this._yAccessor)(d, i)); - }; - var r = this._getAppliedAccessor(this._rAccessor); - var color = this._getAppliedAccessor(this._colorAccessor); - this.dataSelection = this.renderArea.selectAll("circle").data(this._data); + var attrToProjector = this._generateAttrToProjector(); + attrToProjector["cx"] = attrToProjector["x"]; + attrToProjector["cy"] = attrToProjector["y"]; + delete attrToProjector["x"]; + delete attrToProjector["y"]; + + this.dataSelection = this.renderArea.selectAll("circle").data(this._dataSource.data()); this.dataSelection.enter().append("circle"); - this.dataSelection.attr("cx", cx).attr("cy", cy).attr("r", r).attr("fill", color); + this.dataSelection.attr(attrToProjector); this.dataSelection.exit().remove(); }; - CircleRenderer.defaultRAccessor = 3; return CircleRenderer; - })(Plottable.NumericXYRenderer); + })(Plottable.XYRenderer); Plottable.CircleRenderer = CircleRenderer; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var LineRenderer = (function (_super) { @@ -1747,8 +1978,8 @@ var Plottable; * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. */ @@ -1763,24 +1994,19 @@ var Plottable; }; LineRenderer.prototype._paint = function () { - var _this = this; _super.prototype._paint.call(this); - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var cA = this._getAppliedAccessor(this._colorAccessor); - this.line = d3.svg.line().x(function (d, i) { - return _this.xScale.scale(xA(d, i)); - }).y(function (d, i) { - return _this.yScale.scale(yA(d, i)); - }); - this.dataSelection = this.path.datum(this._data); - this.path.attr("d", this.line).attr("stroke", cA); + var attrToProjector = this._generateAttrToProjector(); + this.line = d3.svg.line().x(attrToProjector["x"]).y(attrToProjector["y"]); + this.dataSelection = this.path.datum(this._dataSource.data()); + delete attrToProjector["x"]; + delete attrToProjector["y"]; + this.path.attr("d", this.line).attr(attrToProjector); }; return LineRenderer; - })(Plottable.NumericXYRenderer); + })(Plottable.XYRenderer); Plottable.LineRenderer = LineRenderer; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var BarRenderer = (function (_super) { @@ -1790,8 +2016,8 @@ var Plottable; * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. * @param {IAccessor} [dxAccessor] A function for extracting the width of each bar from the data. * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. @@ -1800,67 +2026,46 @@ var Plottable; _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); this.barPaddingPx = 1; this.classed("bar-renderer", true); - - this.dxAccessor = (dxAccessor != null) ? dxAccessor : BarRenderer.defaultDxAccessor; + this.project("dx", "dx"); } - BarRenderer.prototype.autorange = function () { - _super.prototype.autorange.call(this); - var xA = this._getAppliedAccessor(this._xAccessor); - var dxA = this._getAppliedAccessor(this.dxAccessor); - var x2Accessor = function (d) { - return xA(d, null) + dxA(d, null); - }; - var x2Extent = d3.extent(this._data, x2Accessor); - this.xScale.widenDomain(x2Extent); - return this; - }; - BarRenderer.prototype._paint = function () { var _this = this; _super.prototype._paint.call(this); var yRange = this.yScale.range(); var maxScaledY = Math.max(yRange[0], yRange[1]); - this.dataSelection = this.renderArea.selectAll("rect").data(this._data); + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); var xdr = this.xScale.domain()[1] - this.xScale.domain()[0]; var xrr = this.xScale.range()[1] - this.xScale.range()[0]; this.dataSelection.enter().append("rect"); - var xA = this._getAppliedAccessor(this._xAccessor); - var xFunction = function (d, i) { - var x = xA(d, i); - var scaledX = _this.xScale.scale(x); - return scaledX + _this.barPaddingPx; - }; + var attrToProjector = this._generateAttrToProjector(); - var yA = this._getAppliedAccessor(this._yAccessor); - var yFunction = function (d, i) { - var y = yA(d, i); - var scaledY = _this.yScale.scale(y); - return scaledY; + var xF = attrToProjector["x"]; + attrToProjector["x"] = function (d, i) { + return xF(d, i) + _this.barPaddingPx; }; - var dxA = this._getAppliedAccessor(this.dxAccessor); - var widthFunction = function (d, i) { + var dxA = Plottable.Utils.applyAccessor(this._projectors["dx"].accessor, this.dataSource()); + attrToProjector["width"] = function (d, i) { var dx = dxA(d, i); var scaledDx = _this.xScale.scale(dx); var scaledOffset = _this.xScale.scale(0); return scaledDx - scaledOffset - 2 * _this.barPaddingPx; }; - var heightFunction = function (d, i) { - return maxScaledY - yFunction(d, i); + attrToProjector["height"] = function (d, i) { + return maxScaledY - attrToProjector["y"](d, i); }; - this.dataSelection.attr("x", xFunction).attr("y", yFunction).attr("width", widthFunction).attr("height", heightFunction).attr("fill", this._getAppliedAccessor(this._colorAccessor)); + this.dataSelection.attr(attrToProjector); this.dataSelection.exit().remove(); }; - BarRenderer.defaultDxAccessor = "dx"; return BarRenderer; - })(Plottable.NumericXYRenderer); + })(Plottable.XYRenderer); Plottable.BarRenderer = BarRenderer; })(Plottable || (Plottable = {})); -/// +/// var Plottable; (function (Plottable) { var SquareRenderer = (function (_super) { @@ -1870,49 +2075,199 @@ var Plottable; * * @constructor * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting x values from the data. * @param {IAccessor} [yAccessor] A function for extracting y values from the data. * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. */ function SquareRenderer(dataset, xScale, yScale, xAccessor, yAccessor, rAccessor) { _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); - this._rAccessor = (rAccessor != null) ? rAccessor : SquareRenderer.defaultRAccessor; + this.project("r", 3); this.classed("square-renderer", true); } - SquareRenderer.prototype.rAccessor = function (a) { - this._rAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - }; - SquareRenderer.prototype._paint = function () { - var _this = this; _super.prototype._paint.call(this); - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var rA = this._getAppliedAccessor(this._rAccessor); - var cA = this._getAppliedAccessor(this._colorAccessor); - var xFn = function (d, i) { - return _this.xScale.scale(xA(d, i)) - rA(d, i); + var attrToProjector = this._generateAttrToProjector(); + var xF = attrToProjector["x"]; + var yF = attrToProjector["y"]; + var rF = attrToProjector["r"]; + attrToProjector["x"] = function (d, i) { + return xF(d, i) - rF(d, i); }; - - var yFn = function (d, i) { - return _this.yScale.scale(yA(d, i)) - rA(d, i); + attrToProjector["y"] = function (d, i) { + return yF(d, i) - rF(d, i); }; - this.dataSelection = this.renderArea.selectAll("rect").data(this._data); + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); this.dataSelection.enter().append("rect"); - this.dataSelection.attr("x", xFn).attr("y", yFn).attr("width", rA).attr("height", rA).attr("fill", cA); + this.dataSelection.attr(attrToProjector); this.dataSelection.exit().remove(); }; SquareRenderer.defaultRAccessor = 3; return SquareRenderer; - })(Plottable.NumericXYRenderer); + })(Plottable.XYRenderer); Plottable.SquareRenderer = SquareRenderer; })(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var CategoryBarRenderer = (function (_super) { + __extends(CategoryBarRenderer, _super); + /** + * Creates a CategoryBarRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {OrdinalScale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. + * @param {IAccessor} [widthAccessor] A function for extracting the width position of each bar, in pixels, from the data. + * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. + */ + function CategoryBarRenderer(dataset, xScale, yScale, xAccessor, widthAccessor, yAccessor) { + _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); + this.classed("bar-renderer", true); + this._animate = true; + this.project("width", 10); + } + CategoryBarRenderer.prototype._paint = function () { + var _this = this; + _super.prototype._paint.call(this); + var yRange = this.yScale.range(); + var maxScaledY = Math.max(yRange[0], yRange[1]); + var xA = Plottable.Utils.applyAccessor(this._xAccessor, this.dataSource()); + + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data(), xA); + this.dataSelection.enter().append("rect"); + + var attrToProjector = this._generateAttrToProjector(); + + var rangeType = this.xScale.rangeType(); + if (rangeType === "points") { + var xF = attrToProjector["x"]; + var widthF = attrToProjector["width"]; + attrToProjector["x"] = function (d, i) { + return xF(d, i) - widthF(d, i) / 2; + }; + } else { + attrToProjector["width"] = function (d, i) { + return _this.xScale.rangeBand(); + }; + } + + var heightFunction = function (d, i) { + return maxScaledY - attrToProjector["y"](d, i); + }; + attrToProjector["height"] = heightFunction; + + var updateSelection = this.dataSelection; + if (this._animate) { + updateSelection = updateSelection.transition(); + } + updateSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + }; + + /** + * Selects the bar under the given pixel position. + * + * @param {number} x The pixel x position. + * @param {number} y The pixel y position. + * @param {boolean} [select] Whether or not to select the bar (by classing it "selected"); + * @return {D3.Selection} The selected bar, or null if no bar was selected. + */ + CategoryBarRenderer.prototype.selectBar = function (x, y, select) { + if (typeof select === "undefined") { select = true; } + var selectedBar = null; + + this.dataSelection.each(function (d) { + var bbox = this.getBBox(); + if (bbox.x <= x && x <= bbox.x + bbox.width && bbox.y <= y && y <= bbox.y + bbox.height) { + selectedBar = d3.select(this); + } + }); + + if (selectedBar != null) { + selectedBar.classed("selected", select); + } + + return selectedBar; + }; + + /** + * Deselects all bars. + */ + CategoryBarRenderer.prototype.deselectAll = function () { + this.dataSelection.classed("selected", false); + }; + return CategoryBarRenderer; + })(Plottable.XYRenderer); + Plottable.CategoryBarRenderer = CategoryBarRenderer; +})(Plottable || (Plottable = {})); +/// +var Plottable; +(function (Plottable) { + var GridRenderer = (function (_super) { + __extends(GridRenderer, _super); + /** + * Creates a GridRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {OrdinalScale} xScale The x scale to use. + * @param {OrdinalScale} yScale The y scale to use. + * @param {ColorScale|InterpolatedColorScale} colorScale The color scale to use for each grid + * cell. + * @param {IAccessor|string|number} [xAccessor] An accessor for extracting + * the x position of each grid cell from the data. + * @param {IAccessor|string|number} [yAccessor] An accessor for extracting + * the y position of each grid cell from the data. + * @param {IAccessor|string|number} [valueAccessor] An accessor for + * extracting value of each grid cell from the data. This value will + * be pass through the color scale to determine the color of the cell. + */ + function GridRenderer(dataset, xScale, yScale, colorScale, xAccessor, yAccessor, valueAccessor) { + _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); + this.classed("grid-renderer", true); + + // The x and y scales should render in bands with no padding + this.xScale.rangeType("bands", 0, 0); + this.yScale.rangeType("bands", 0, 0); + + this.colorScale = colorScale; + this.project("fill", valueAccessor, colorScale); + } + GridRenderer.prototype._paint = function () { + _super.prototype._paint.call(this); + + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); + this.dataSelection.enter().append("rect"); + + var xStep = this.xScale.rangeBand(); + var yr = this.yScale.range(); + var yStep = this.yScale.rangeBand(); + var yMax = Math.max(yr[0], yr[1]) - yStep; + + var attrToProjector = this._generateAttrToProjector(); + attrToProjector["width"] = function () { + return xStep; + }; + attrToProjector["height"] = function () { + return yStep; + }; + var yAttr = attrToProjector["y"]; + attrToProjector["y"] = function (d, i) { + return yMax - yAttr(d, i); + }; + + this.dataSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + }; + return GridRenderer; + })(Plottable.XYRenderer); + Plottable.GridRenderer = GridRenderer; +})(Plottable || (Plottable = {})); /// var Plottable; (function (Plottable) { @@ -2201,7 +2556,7 @@ var Plottable; this.rescaleInProgress = false; this.scales = scales; this.scales.forEach(function (s) { - return s.registerListener(function (sx) { + return s.registerListener(_this, function (sx) { return _this.rescale(sx); }); }); @@ -2411,169 +2766,35 @@ var Plottable; })(Plottable.Table); Plottable.StandardChart = StandardChart; })(Plottable || (Plottable = {})); -/// -var Plottable; -(function (Plottable) { - var CategoryXYRenderer = (function (_super) { - __extends(CategoryXYRenderer, _super); - /** - * Creates a CategoryXYRenderer with an Ordinal x scale and Quantitive y scale. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - function CategoryXYRenderer(dataset, xScale, yScale, xAccessor, yAccessor) { - _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("category-renderer", true); - } - return CategoryXYRenderer; - })(Plottable.XYRenderer); - Plottable.CategoryXYRenderer = CategoryXYRenderer; -})(Plottable || (Plottable = {})); -/// -var Plottable; -(function (Plottable) { - var CategoryBarRenderer = (function (_super) { - __extends(CategoryBarRenderer, _super); - /** - * Creates a CategoryBarRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. - * @param {IAccessor} [widthAccessor] A function for extracting the width position of each bar, in pixels, from the data. - * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. - */ - function CategoryBarRenderer(dataset, xScale, yScale, xAccessor, widthAccessor, yAccessor) { - _super.call(this, dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("bar-renderer", true); - this._animate = true; - this._widthAccessor = (widthAccessor != null) ? widthAccessor : 10; // default width is 10px - } - /** - * Sets the width accessor. - * - * @param {any} accessor The new width accessor. - * @returns {CategoryBarRenderer} The calling CategoryBarRenderer. - */ - CategoryBarRenderer.prototype.widthAccessor = function (accessor) { - this._widthAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - }; - - CategoryBarRenderer.prototype._paint = function () { - var _this = this; - _super.prototype._paint.call(this); - var yRange = this.yScale.range(); - var maxScaledY = Math.max(yRange[0], yRange[1]); - var xA = this._getAppliedAccessor(this._xAccessor); - - this.dataSelection = this.renderArea.selectAll("rect").data(this._data, xA); - this.dataSelection.enter().append("rect"); - - var widthFunction = this._getAppliedAccessor(this._widthAccessor); - - var xFunction = function (d, i) { - var x = xA(d, i); - var scaledX = _this.xScale.scale(x); - return scaledX - widthFunction(d, i) / 2; - }; - - var yA = this._getAppliedAccessor(this._yAccessor); - var yFunction = function (d, i) { - var y = yA(d, i); - var scaledY = _this.yScale.scale(y); - return scaledY; - }; - - var heightFunction = function (d, i) { - return maxScaledY - yFunction(d, i); - }; - - var updateSelection = this.dataSelection.attr("fill", this._getAppliedAccessor(this._colorAccessor)); - if (this._animate) { - updateSelection = updateSelection.transition(); - } - updateSelection.attr("x", xFunction).attr("y", yFunction).attr("width", widthFunction).attr("height", heightFunction); - this.dataSelection.exit().remove(); - }; - - CategoryBarRenderer.prototype.autorange = function () { - _super.prototype.autorange.call(this); - var yDomain = this.yScale.domain(); - if (yDomain[1] < 0 || yDomain[0] > 0) { - var newDomain = [Math.min(0, yDomain[0]), Math.max(0, yDomain[1])]; - this.yScale.domain(newDomain); - } - return this; - }; - - /** - * Selects the bar under the given pixel position. - * - * @param {number} x The pixel x position. - * @param {number} y The pixel y position. - * @param {boolean} [select] Whether or not to select the bar (by classing it "selected"); - * @return {D3.Selection} The selected bar, or null if no bar was selected. - */ - CategoryBarRenderer.prototype.selectBar = function (x, y, select) { - if (typeof select === "undefined") { select = true; } - var selectedBar = null; - - this.dataSelection.each(function (d) { - var bbox = this.getBBox(); - if (bbox.x <= x && x <= bbox.x + bbox.width && bbox.y <= y && y <= bbox.y + bbox.height) { - selectedBar = d3.select(this); - } - }); - - if (selectedBar != null) { - selectedBar.classed("selected", select); - } - - return selectedBar; - }; - - /** - * Deselects all bars. - */ - CategoryBarRenderer.prototype.deselectAll = function () { - this.dataSelection.classed("selected", false); - }; - return CategoryBarRenderer; - })(Plottable.CategoryXYRenderer); - Plottable.CategoryBarRenderer = CategoryBarRenderer; -})(Plottable || (Plottable = {})); /// /// +/// +/// /// -/// +/// +/// +/// +/// +/// +/// +/// //grunt-start /// /// /// /// -/// -/// -/// -/// -/// -/// -/// +/// +/// +/// +/// +/// +/// +/// +/// /// /// /// /// -/// -/// //grunt-end /// var Plottable; @@ -2601,7 +2822,7 @@ var Plottable; formatter = d3.format(".3s"); } this.d3Axis.tickFormat(formatter); - this.axisScale.registerListener(function () { + this.axisScale.registerListener(this, function () { return _this.rescale(); }); } @@ -3062,12 +3283,12 @@ var Plottable; this.xScale = xScale; this.yScale = yScale; if (this.xScale != null) { - this.xScale.registerListener(function () { + this.xScale.registerListener(this, function () { return _this.redrawXLines(); }); } if (this.yScale != null) { - this.yScale.registerListener(function () { + this.yScale.registerListener(this, function () { return _this.redrawYLines(); }); } diff --git a/src/axis.ts b/src/axis.ts index 54d36d248a..846f687f0e 100644 --- a/src/axis.ts +++ b/src/axis.ts @@ -28,7 +28,7 @@ module Plottable { formatter = d3.format(".3s"); } this.d3Axis.tickFormat(formatter); - this.axisScale.registerListener(() => this.rescale()); + this.axisScale.registerListener(this, () => this.rescale()); } public _anchor(element: D3.Selection) { diff --git a/src/barRenderer.ts b/src/barRenderer.ts deleted file mode 100644 index 692f51e219..0000000000 --- a/src/barRenderer.ts +++ /dev/null @@ -1,88 +0,0 @@ -/// - -module Plottable { - export class BarRenderer extends NumericXYRenderer { - private static defaultDxAccessor = "dx"; - public barPaddingPx = 1; - - public dxAccessor: any; - - /** - * Creates a BarRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. - * @param {IAccessor} [dxAccessor] A function for extracting the width of each bar from the data. - * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. - */ - constructor(dataset: any, - xScale: QuantitiveScale, - yScale: QuantitiveScale, - xAccessor?: IAccessor, - dxAccessor?: IAccessor, - yAccessor?: IAccessor) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("bar-renderer", true); - - this.dxAccessor = (dxAccessor != null) ? dxAccessor : BarRenderer.defaultDxAccessor; - } - - public autorange() { - super.autorange(); - var xA = this._getAppliedAccessor(this._xAccessor); - var dxA = this._getAppliedAccessor(this.dxAccessor); - var x2Accessor = (d: any) => xA(d, null) + dxA(d, null); - var x2Extent: number[] = d3.extent(this._data, x2Accessor); - this.xScale.widenDomain(x2Extent); - return this; - } - - public _paint() { - super._paint(); - var yRange = this.yScale.range(); - var maxScaledY = Math.max(yRange[0], yRange[1]); - - this.dataSelection = this.renderArea.selectAll("rect").data(this._data); - var xdr = this.xScale.domain()[1] - this.xScale.domain()[0]; - var xrr = this.xScale.range()[1] - this.xScale.range()[0]; - this.dataSelection.enter().append("rect"); - - var xA = this._getAppliedAccessor(this._xAccessor); - var xFunction = (d: any, i: number) => { - var x = xA(d, i); - var scaledX = this.xScale.scale(x); - return scaledX + this.barPaddingPx; - }; - - var yA = this._getAppliedAccessor(this._yAccessor); - var yFunction = (d: any, i: number) => { - var y = yA(d, i); - var scaledY = this.yScale.scale(y); - return scaledY; - }; - - var dxA = this._getAppliedAccessor(this.dxAccessor); - var widthFunction = (d: any, i: number) => { - var dx = dxA(d, i); - var scaledDx = this.xScale.scale(dx); - var scaledOffset = this.xScale.scale(0); - return scaledDx - scaledOffset - 2 * this.barPaddingPx; - }; - - var heightFunction = (d: any, i: number) => { - return maxScaledY - yFunction(d, i); - }; - - this.dataSelection - .attr("x", xFunction) - .attr("y", yFunction) - .attr("width", widthFunction) - .attr("height", heightFunction) - .attr("fill", this._getAppliedAccessor(this._colorAccessor)); - this.dataSelection.exit().remove(); - } - } -} diff --git a/src/broadcaster.ts b/src/broadcaster.ts new file mode 100644 index 0000000000..013a6b59df --- /dev/null +++ b/src/broadcaster.ts @@ -0,0 +1,51 @@ +/// + +module Plottable { + interface IListenerCallbackPair { + l: any; + c: IBroadcasterCallback; + } + export class Broadcaster extends PlottableObject { + private listener2Callback = new Utils.StrictEqualityAssociativeArray(); + + /** + * Registers a callback to be called when the broadcast method is called. Also takes a listener which + * is used to support deregistering the same callback later, by passing in the same listener. + * If there is already a callback associated with that listener, then the callback will be replaced. + * + * @param listener The listener associated with the callback. + * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. + * @returns {Broadcaster} this object + */ + public registerListener(listener: any, callback: IBroadcasterCallback) { + this.listener2Callback.set(listener, callback); + return this; + } + + /** + * Call all listening callbacks, optionally with arguments passed through. + * + * @param ...args A variable number of optional arguments + * @returns {Broadcaster} this object + */ + public _broadcast(...args: any[]) { + this.listener2Callback.values().forEach((callback) => callback(this, args)); + return this; + } + + /** + * Registers deregister the callback associated with a listener. + * + * @param listener The listener to deregister. + * @returns {Broadcaster} this object + */ + public deregisterListener(listener: any) { + var listenerWasFound = this.listener2Callback.delete(listener); + if (listenerWasFound) { + return this; + } else { + throw new Error("Attempted to deregister listener, but listener not found"); + } + } + } +} diff --git a/src/categoryXYRenderer.ts b/src/categoryXYRenderer.ts deleted file mode 100644 index bb7714b0dc..0000000000 --- a/src/categoryXYRenderer.ts +++ /dev/null @@ -1,30 +0,0 @@ -/// - -module Plottable { - export class CategoryXYRenderer extends XYRenderer { - public dataSelection: D3.UpdateSelection; - public xScale: OrdinalScale; - public yScale: QuantitiveScale; - public _xAccessor: any; - public _yAccessor: any; - - /** - * Creates a CategoryXYRenderer with an Ordinal x scale and Quantitive y scale. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, - xScale: OrdinalScale, - yScale: QuantitiveScale, - xAccessor?: IAccessor, - yAccessor?: IAccessor) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("category-renderer", true); - } - } -} diff --git a/src/circleRenderer.ts b/src/circleRenderer.ts deleted file mode 100644 index 821bb68ca3..0000000000 --- a/src/circleRenderer.ts +++ /dev/null @@ -1,48 +0,0 @@ -/// - -module Plottable { - export class CircleRenderer extends NumericXYRenderer { - private _rAccessor: any; - private static defaultRAccessor = 3; - - /** - * Creates a CircleRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. - */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, - xAccessor?: any, yAccessor?: any, rAccessor?: any) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this._rAccessor = (rAccessor != null) ? rAccessor : CircleRenderer.defaultRAccessor; - this.classed("circle-renderer", true); - } - - public rAccessor(a: any) { - this._rAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - } - - public _paint() { - super._paint(); - var cx = (d: any, i: number) => this.xScale.scale(this._getAppliedAccessor(this._xAccessor)(d, i)); - var cy = (d: any, i: number) => this.yScale.scale(this._getAppliedAccessor(this._yAccessor)(d, i)); - var r = this._getAppliedAccessor(this._rAccessor); - var color = this._getAppliedAccessor(this._colorAccessor); - this.dataSelection = this.renderArea.selectAll("circle").data(this._data); - this.dataSelection.enter().append("circle"); - this.dataSelection.attr("cx", cx) - .attr("cy", cy) - .attr("r", r) - .attr("fill", color); - this.dataSelection.exit().remove(); - } - } -} diff --git a/src/component.ts b/src/component.ts index 3526f2ceb2..ad913706ee 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,8 +1,7 @@ /// module Plottable { - export class Component { - private static clipPathId = 0; // Used for unique namespacing for the clipPaths + export class Component extends PlottableObject { public element: D3.Selection; public content: D3.Selection; private hitBox: D3.Selection; @@ -222,10 +221,9 @@ module Plottable { private generateClipPath() { // The clip path will prevent content from overflowing its component space. - var clipPathId = Component.clipPathId++; - this.element.attr("clip-path", "url(#clipPath" + clipPathId + ")"); + this.element.attr("clip-path", "url(#clipPath" + this._plottableID + ")"); var clipPathParent = this.boxContainer.append("clipPath") - .attr("id", "clipPath" + clipPathId); + .attr("id", "clipPath" + this._plottableID); this.addBox("clip-rect", clipPathParent); } diff --git a/src/coordinator.ts b/src/coordinator.ts index 8d820703a4..8435287cd6 100644 --- a/src/coordinator.ts +++ b/src/coordinator.ts @@ -17,7 +17,7 @@ module Plottable { */ constructor(scales: Scale[]) { this.scales = scales; - this.scales.forEach((s) => s.registerListener((sx: Scale) => this.rescale(sx))); + this.scales.forEach((s) => s.registerListener(this, (sx: Scale) => this.rescale(sx))); } public rescale(scale: Scale) { diff --git a/src/dataSource.ts b/src/dataSource.ts new file mode 100644 index 0000000000..827065c34c --- /dev/null +++ b/src/dataSource.ts @@ -0,0 +1,83 @@ +/// + +module Plottable { + interface ICachedExtent { + accessor: IAccessor; + extent: any[]; + } + export class DataSource extends Broadcaster { + private _data: any[]; + private _metadata: any; + private accessor2cachedExtent: Utils.StrictEqualityAssociativeArray; + /** + * Creates a new DataSource. + * + * @constructor + * @param {any[]} data + * @param {any} metadata An object containing additional information. + */ + constructor(data: any[] = [], metadata: any = {}) { + super(); + this._data = data; + this._metadata = metadata; + this.accessor2cachedExtent = new Utils.StrictEqualityAssociativeArray(); + } + + /** + * Retrieves the current data from the DataSource, or sets the data. + * + * @param {any[]} [data] The new data. + * @returns {any[]|DataSource} The current data, or the calling DataSource. + */ + public data(): any[]; + public data(data: any[]): DataSource; + public data(data?: any[]): any { + if (data == null) { + return this._data; + } else { + this._data = data; + this.accessor2cachedExtent = new Utils.StrictEqualityAssociativeArray(); + this._broadcast(); + return this; + } + } + + /** + * Retrieves the current metadata from the DataSource, or sets the metadata. + * + * @param {any[]} [metadata] The new metadata. + * @returns {any[]|DataSource} The current metadata, or the calling DataSource. + */ + public metadata(): any; + public metadata(metadata: any): DataSource; + public metadata(metadata?: any): any { + if (metadata == null) { + return this._metadata; + } else { + this._metadata = metadata; + this.accessor2cachedExtent = new Utils.StrictEqualityAssociativeArray(); + this._broadcast(); + return this; + } + } + + public _getExtent(accessor: IAccessor): any[] { + var cachedExtent = this.accessor2cachedExtent.get(accessor); + if (cachedExtent === undefined) { + cachedExtent = this.computeExtent(accessor); + this.accessor2cachedExtent.set(accessor, cachedExtent); + } + return cachedExtent; + } + + private computeExtent(accessor: IAccessor): any[] { + var appliedAccessor = Utils.applyAccessor(accessor, this); + var mappedData = this._data.map(appliedAccessor); + if (typeof(appliedAccessor(this._data[0], 0)) === "string") { + return Utils.uniq(mappedData); + } else { + return d3.extent(mappedData); + } + } + } +} diff --git a/src/gridlines.ts b/src/gridlines.ts index 56d541da07..bb908eef44 100644 --- a/src/gridlines.ts +++ b/src/gridlines.ts @@ -20,10 +20,10 @@ module Plottable { this.xScale = xScale; this.yScale = yScale; if (this.xScale != null) { - this.xScale.registerListener(() => this.redrawXLines()); + this.xScale.registerListener(this, () => this.redrawXLines()); } if (this.yScale != null) { - this.yScale.registerListener(() => this.redrawYLines()); + this.yScale.registerListener(this, () => this.redrawYLines()); } } diff --git a/src/interaction.ts b/src/interaction.ts index 0041c1277a..b1557d178a 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -184,67 +184,6 @@ module Plottable { } } - export class ZoomCallbackGenerator { - private xScaleMappings: QuantitiveScale[][] = []; - private yScaleMappings: QuantitiveScale[][] = []; - - /** - * Adds listen-update pair of X scales. - * - * @param {QuantitiveScale} listenerScale An X scale to listen for events on. - * @param {QuantitiveScale} [targetScale] An X scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - public addXScale(listenerScale: QuantitiveScale, targetScale?: QuantitiveScale): ZoomCallbackGenerator { - if (targetScale == null) { - targetScale = listenerScale; - } - this.xScaleMappings.push([listenerScale, targetScale]); - return this; - } - - /** - * Adds listen-update pair of Y scales. - * - * @param {QuantitiveScale} listenerScale A Y scale to listen for events on. - * @param {QuantitiveScale} [targetScale] A Y scale to update when events occur. - * If not supplied, listenerScale will be updated when an event occurs. - * @returns {ZoomCallbackGenerator} The calling ZoomCallbackGenerator. - */ - public addYScale(listenerScale: QuantitiveScale, targetScale?: QuantitiveScale): ZoomCallbackGenerator { - if (targetScale == null) { - targetScale = listenerScale; - } - this.yScaleMappings.push([listenerScale, targetScale]); - return this; - } - - private updateScale(referenceScale: QuantitiveScale, targetScale: QuantitiveScale, pixelMin: number, pixelMax: number) { - var originalDomain = referenceScale.domain(); - var newDomain = [referenceScale.invert(pixelMin), referenceScale.invert(pixelMax)]; - var sameDirection = (newDomain[0] < newDomain[1]) === (originalDomain[0] < originalDomain[1]); - if (!sameDirection) {newDomain.reverse();} - targetScale.domain(newDomain); - } - - /** - * Generates a callback that can be passed to Interactions. - * - * @returns {(area: SelectionArea) => void} A callback that updates the scales previously specified. - */ - public getCallback() { - return (area: SelectionArea) => { - this.xScaleMappings.forEach((sm: QuantitiveScale[]) => { - this.updateScale(sm[0], sm[1], area.xMin, area.xMax); - }); - this.yScaleMappings.forEach((sm: QuantitiveScale[]) => { - this.updateScale(sm[0], sm[1], area.yMin, area.yMax); - }); - }; - } - } - export class MousemoveInteraction extends Interaction { constructor(componentToListenTo: Component) { super(componentToListenTo); @@ -343,60 +282,4 @@ module Plottable { return this; } } - - - export class CrosshairsInteraction extends MousemoveInteraction { - private renderer: NumericXYRenderer; - - private circle: D3.Selection; - private xLine: D3.Selection; - private yLine: D3.Selection; - private lastx: number; - private lasty: number; - - constructor(renderer: NumericXYRenderer) { - super(renderer); - this.renderer = renderer; - renderer.xScale.registerListener(() => this.rescale()); - renderer.yScale.registerListener(() => this.rescale()); - } - - public _anchor(hitBox: D3.Selection) { - super._anchor(hitBox); - var container = this.renderer.foregroundContainer.append("g").classed("crosshairs", true); - this.circle = container.append("circle").classed("centerpoint", true); - this.xLine = container.append("path").classed("x-line", true); - this.yLine = container.append("path").classed("y-line", true); - this.circle.attr("r", 5); - } - - public mousemove(x: number, y: number) { - this.lastx = x; - this.lasty = y; - var domainX = this.renderer.xScale.invert(x); - var data = this.renderer._data; - var xA = this.renderer._getAppliedAccessor(this.renderer._xAccessor); - var yA = this.renderer._getAppliedAccessor(this.renderer._yAccessor); - var dataIndex = OSUtils.sortedIndex(domainX, data, xA); - dataIndex = dataIndex > 0 ? dataIndex - 1 : 0; - var dataPoint = data[dataIndex]; - - var dataX = xA(dataPoint, dataIndex); - var dataY = yA(dataPoint, dataIndex); - var pixelX = this.renderer.xScale.scale(dataX); - var pixelY = this.renderer.yScale.scale(dataY); - this.circle.attr("cx", pixelX).attr("cy", pixelY); - - var width = this.renderer.availableWidth; - var height = this.renderer.availableHeight; - this.xLine.attr("d", "M 0 " + pixelY + " L " + width + " " + pixelY); - this.yLine.attr("d", "M " + pixelX + " 0 L " + pixelX + " " + height); - } - - public rescale() { - if (this.lastx != null) { - this.mousemove(this.lastx, this.lasty); - } - } - } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 84c4fc2cca..138c3ae6a0 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -13,6 +13,10 @@ module Plottable { (datum: any, index?: number, metadata?: any): any; }; + export interface IAppliedAccessor { + (datum: any, index: number) : any; + } + export interface SelectionArea { xMin: number; xMax: number; @@ -26,10 +30,6 @@ module Plottable { } export interface IBroadcasterCallback { - (broadcaster: IBroadcaster, ...args: any[]): any; - } - - export interface IBroadcaster { - registerListener: (cb: IBroadcasterCallback) => IBroadcaster; + (broadcaster: Broadcaster, ...args: any[]): any; } } diff --git a/src/lineRenderer.ts b/src/lineRenderer.ts deleted file mode 100644 index 9d039fdf54..0000000000 --- a/src/lineRenderer.ts +++ /dev/null @@ -1,41 +0,0 @@ -/// - -module Plottable { - export class LineRenderer extends NumericXYRenderer { - private path: D3.Selection; - private line: D3.Svg.Line; - -/** - * Creates a LineRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("line-renderer", true); - } - - public _anchor(element: D3.Selection) { - super._anchor(element); - this.path = this.renderArea.append("path").classed("line", true); - return this; - } - - public _paint() { - super._paint(); - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var cA = this._getAppliedAccessor(this._colorAccessor); - this.line = d3.svg.line() - .x((d: any, i: number) => this.xScale.scale(xA(d, i))) - .y((d: any, i: number) => this.yScale.scale(yA(d, i))); - this.dataSelection = this.path.datum(this._data); - this.path.attr("d", this.line).attr("stroke", cA); - } - } -} diff --git a/src/numericXYRenderer.ts b/src/numericXYRenderer.ts deleted file mode 100644 index 903a7d5947..0000000000 --- a/src/numericXYRenderer.ts +++ /dev/null @@ -1,81 +0,0 @@ -/// - -module Plottable { - export class NumericXYRenderer extends XYRenderer { - public dataSelection: D3.UpdateSelection; - public xScale: QuantitiveScale; - public yScale: QuantitiveScale; - public _xAccessor: any; - public _yAccessor: any; - public autorangeDataOnLayout = true; - - /** - * Creates an NumericXYRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, xAccessor?: IAccessor, yAccessor?: IAccessor) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this.classed("numeric-xy-renderer", true); - } - - /** - * Converts a SelectionArea with pixel ranges to one with data ranges. - * - * @param {SelectionArea} pixelArea The selected area, in pixels. - * @returns {SelectionArea} The corresponding selected area in the domains of the scales. - */ - public invertXYSelectionArea(pixelArea: SelectionArea): SelectionArea { - var xMin = this.xScale.invert(pixelArea.xMin); - var xMax = this.xScale.invert(pixelArea.xMax); - var yMin = this.yScale.invert(pixelArea.yMin); - var yMax = this.yScale.invert(pixelArea.yMax); - var dataArea = {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}; - return dataArea; - } - - private getDataFilterFunction(dataArea: SelectionArea): (d: any, i: number) => boolean { - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var filterFunction = (d: any, i: number) => { - var x: number = xA(d, i); - var y: number = yA(d, i); - return Utils.inRange(x, dataArea.xMin, dataArea.xMax) && Utils.inRange(y, dataArea.yMin, dataArea.yMax); - }; - return filterFunction; - } - - /** - * Gets the data in a selected area. - * - * @param {SelectionArea} dataArea The selected area. - * @returns {D3.UpdateSelection} The data in the selected area. - */ - public getSelectionFromArea(dataArea: SelectionArea) { - var filterFunction = this.getDataFilterFunction(dataArea); - return this.dataSelection.filter(filterFunction); - } - - /** - * Gets the indices of data in a selected area - * - * @param {SelectionArea} dataArea The selected area. - * @returns {number[]} An array of the indices of datapoints in the selected area. - */ - public getDataIndicesFromArea(dataArea: SelectionArea): number[] { - var filterFunction = this.getDataFilterFunction(dataArea); - var results: number[] = []; - this._data.forEach((d, i) => { - if (filterFunction(d, i)) { - results.push(i); - } - }); - return results; - } - } -} diff --git a/src/plottableObject.ts b/src/plottableObject.ts new file mode 100644 index 0000000000..3c928c3a6f --- /dev/null +++ b/src/plottableObject.ts @@ -0,0 +1,8 @@ +/// + +module Plottable { + export class PlottableObject { + private static nextID = 0; + public _plottableID = PlottableObject.nextID++; + } +} diff --git a/src/reference.ts b/src/reference.ts index 164cc3edc4..bcc7503775 100644 --- a/src/reference.ts +++ b/src/reference.ts @@ -1,27 +1,30 @@ /// /// +/// +/// /// /// /// /// /// /// +/// +/// //grunt-start /// /// /// /// -/// -/// -/// -/// -/// -/// -/// +/// +/// +/// +/// +/// +/// +/// +/// /// /// /// /// -/// -/// //grunt-end diff --git a/src/renderer.ts b/src/renderer.ts deleted file mode 100644 index 4345eff799..0000000000 --- a/src/renderer.ts +++ /dev/null @@ -1,120 +0,0 @@ -/// - -module Plottable { - export class Renderer extends Component { - public _data: any[]; - public _metadata: IMetadata; - public renderArea: D3.Selection; - public element: D3.Selection; - public scales: Scale[]; - public _colorAccessor: IAccessor; - public _animate = false; - public _hasRendered = false; - private static defaultColorAccessor = (d: any) => "#1f77b4"; - - public _rerenderUpdateSelection = false; - // A perf-efficient manner of rendering would be to calculate attributes only - // on new nodes, and assume that old nodes (ie the update selection) can - // maintain their current attributes. If we change the metadata or an - // accessor function, then this property will not be true, and we will need - // to recompute attributes on the entire update selection. - - public _requireRerender = false; - // A perf-efficient approach to rendering scale changes would be to transform - // the container rather than re-render. In the event that the data is changed, - // it will be necessary to do a regular rerender. - - /** - * Creates a Renderer. - * - * @constructor - * @param {IDataset} [dataset] The dataset associated with the Renderer. - */ - constructor(dataset?: any) { - super(); - this.clipPathEnabled = true; - this._fixedWidth = false; - this._fixedHeight = false; - this.classed("renderer", true); - if (dataset != null) { - if (dataset.data == null) { - this.data(dataset); - } else { - this.data(dataset.data); - if (dataset.metadata != null) { - this.metadata(dataset.metadata); - } - } - } - this.colorAccessor(Renderer.defaultColorAccessor); - } - - /** - * Sets a new dataset on the Renderer. - * - * @param {IDataset} dataset The new dataset to be associated with the Renderer. - * @returns {Renderer} The calling Renderer. - */ - public dataset(dataset: IDataset): Renderer { - this.data(dataset.data); - this.metadata(dataset.metadata); - return this; - } - - public metadata(metadata: IMetadata): Renderer { - var oldCSSClass = this._metadata != null ? this._metadata.cssClass : null; - this.classed(oldCSSClass, false); - this._metadata = metadata; - this.classed(this._metadata.cssClass, true); - this._rerenderUpdateSelection = true; - this._requireRerender = true; - return this; - } - - public data(data: any[]): Renderer { - this._data = data; - this._requireRerender = true; - return this; - } - - public _render(): Renderer { - this._hasRendered = true; - this._paint(); - this._requireRerender = false; - this._rerenderUpdateSelection = false; - return this; - } - - public colorAccessor(a: IAccessor): Renderer { - this._colorAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - } - - public _paint() { - // no-op - } - - public autorange() { - // no-op - return this; - } - - public _anchor(element: D3.Selection) { - super._anchor(element); - this.renderArea = this.content.append("g").classed("render-area", true); - return this; - } - - public _getAppliedAccessor(accessor: any): (d: any, i: number) => any { - if (typeof(accessor) === "function") { - return (d: any, i: number) => accessor(d, i, this._metadata); - } else if (typeof(accessor) === "string") { - return (d: any, i: number) => d[accessor]; - } else { - return (d: any, i: number) => accessor; - } - } - } -} diff --git a/src/renderers/barRenderer.ts b/src/renderers/barRenderer.ts new file mode 100644 index 0000000000..7c4f27d053 --- /dev/null +++ b/src/renderers/barRenderer.ts @@ -0,0 +1,62 @@ +/// + +module Plottable { + export class BarRenderer extends XYRenderer { + public barPaddingPx = 1; + + public dxAccessor: any; + + /** + * Creates a BarRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. + * @param {IAccessor} [dxAccessor] A function for extracting the width of each bar from the data. + * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. + */ + constructor(dataset: any, + xScale: Scale, + yScale: Scale, + xAccessor?: IAccessor, + dxAccessor?: IAccessor, + yAccessor?: IAccessor) { + super(dataset, xScale, yScale, xAccessor, yAccessor); + this.classed("bar-renderer", true); + this.project("dx", "dx"); + } + + public _paint() { + super._paint(); + var yRange = this.yScale.range(); + var maxScaledY = Math.max(yRange[0], yRange[1]); + + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); + var xdr = this.xScale.domain()[1] - this.xScale.domain()[0]; + var xrr = this.xScale.range()[1] - this.xScale.range()[0]; + this.dataSelection.enter().append("rect"); + + var attrToProjector = this._generateAttrToProjector(); + + var xF = attrToProjector["x"]; + attrToProjector["x"] = (d: any, i: number) => xF(d, i) + this.barPaddingPx; + + var dxA = Utils.applyAccessor(this._projectors["dx"].accessor, this.dataSource()); + attrToProjector["width"] = (d: any, i: number) => { + var dx = dxA(d, i); + var scaledDx = this.xScale.scale(dx); + var scaledOffset = this.xScale.scale(0); + return scaledDx - scaledOffset - 2 * this.barPaddingPx; + }; + + attrToProjector["height"] = (d: any, i: number) => { + return maxScaledY - attrToProjector["y"](d, i); + }; + + this.dataSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + } + } +} diff --git a/src/categoryBarRenderer.ts b/src/renderers/categoryBarRenderer.ts similarity index 53% rename from src/categoryBarRenderer.ts rename to src/renderers/categoryBarRenderer.ts index 63b21d3fd7..56de9cc6da 100644 --- a/src/categoryBarRenderer.ts +++ b/src/renderers/categoryBarRenderer.ts @@ -1,109 +1,64 @@ -/// +/// module Plottable { - export class CategoryBarRenderer extends CategoryXYRenderer { - private _widthAccessor: any; - + export class CategoryBarRenderer extends XYRenderer { + public xScale: OrdinalScale; /** * Creates a CategoryBarRenderer. * * @constructor * @param {IDataset} dataset The dataset to render. * @param {OrdinalScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. + * @param {Scale} yScale The y scale to use. * @param {IAccessor} [xAccessor] A function for extracting the start position of each bar from the data. * @param {IAccessor} [widthAccessor] A function for extracting the width position of each bar, in pixels, from the data. * @param {IAccessor} [yAccessor] A function for extracting height of each bar from the data. */ constructor(dataset: any, xScale: OrdinalScale, - yScale: QuantitiveScale, + yScale: Scale, xAccessor?: IAccessor, widthAccessor?: IAccessor, yAccessor?: IAccessor) { super(dataset, xScale, yScale, xAccessor, yAccessor); this.classed("bar-renderer", true); this._animate = true; - this._widthAccessor = (widthAccessor != null) ? widthAccessor : 10; // default width is 10px - } - - /** - * Sets the width accessor. - * - * @param {any} accessor The new width accessor. - * @returns {CategoryBarRenderer} The calling CategoryBarRenderer. - */ - public widthAccessor(accessor: any) { - this._widthAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; + this.project("width", 10); } public _paint() { super._paint(); var yRange = this.yScale.range(); var maxScaledY = Math.max(yRange[0], yRange[1]); - var xA = this._getAppliedAccessor(this._xAccessor); + var xA = Utils.applyAccessor(this._xAccessor, this.dataSource()); - this.dataSelection = this.renderArea.selectAll("rect").data(this._data, xA); + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data(), xA); this.dataSelection.enter().append("rect"); - var rangeType = this.xScale.rangeType(); + var attrToProjector = this._generateAttrToProjector(); - var widthFunction: (d:any, i: number) => number; + var rangeType = this.xScale.rangeType(); if (rangeType === "points"){ - widthFunction = this._getAppliedAccessor(this._widthAccessor); + var xF = attrToProjector["x"]; + var widthF = attrToProjector["width"]; + attrToProjector["x"] = (d: any, i: number) => xF(d, i) - widthF(d, i) / 2; } else { - widthFunction = (d:any, i: number) => this.xScale.rangeBand(); + attrToProjector["width"] = (d: any, i: number) => this.xScale.rangeBand(); } - var xFunction = (d: any, i: number) => { - var x = xA(d, i); - var scaledX = this.xScale.scale(x); - if (rangeType === "points") { - return scaledX - widthFunction(d, i)/2; - } else { - return scaledX; - } - }; - - var yA = this._getAppliedAccessor(this._yAccessor); - var yFunction = (d: any, i: number) => { - var y = yA(d, i); - var scaledY = this.yScale.scale(y); - return scaledY; - }; - var heightFunction = (d: any, i: number) => { - return maxScaledY - yFunction(d, i); + return maxScaledY - attrToProjector["y"](d, i); }; + attrToProjector["height"] = heightFunction; - var updateSelection: any = this.dataSelection - .attr("fill", this._getAppliedAccessor(this._colorAccessor)); - + var updateSelection: any = this.dataSelection; if (this._animate) { updateSelection = updateSelection.transition(); } - - updateSelection - .attr("x", xFunction) - .attr("y", yFunction) - .attr("width", widthFunction) - .attr("height", heightFunction); + updateSelection.attr(attrToProjector); this.dataSelection.exit().remove(); } - public autorange() { - super.autorange(); - var yDomain = this.yScale.domain(); - if (yDomain[1] < 0 || yDomain[0] > 0) { // domain does not include 0 - var newDomain = [Math.min(0, yDomain[0]), Math.max(0, yDomain[1])]; - this.yScale.domain(newDomain); - } - return this; - } - /** * Selects the bar under the given pixel position. * diff --git a/src/renderers/circleRenderer.ts b/src/renderers/circleRenderer.ts new file mode 100644 index 0000000000..d792188220 --- /dev/null +++ b/src/renderers/circleRenderer.ts @@ -0,0 +1,47 @@ +/// + +module Plottable { + export class CircleRenderer extends XYRenderer { + + /** + * Creates a CircleRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting x values from the data. + * @param {IAccessor} [yAccessor] A function for extracting y values from the data. + * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. + */ + constructor(dataset: any, xScale: Scale, yScale: Scale, + xAccessor?: any, yAccessor?: any, rAccessor?: any) { + super(dataset, xScale, yScale, xAccessor, yAccessor); +/* this._rAccessor = (rAccessor != null) ? rAccessor : CircleRenderer.defaultRAccessor;*/ + this.classed("circle-renderer", true); + this.project("r", 3); + this.project("fill", "#00ffaa"); + } + + public project(attrToSet: string, accessor: any, scale?: Scale) { + attrToSet = attrToSet === "cx" ? "x" : attrToSet; + attrToSet = attrToSet === "cy" ? "y" : attrToSet; + super.project(attrToSet, accessor, scale); + return this; + } + + public _paint() { + super._paint(); + var attrToProjector = this._generateAttrToProjector(); + attrToProjector["cx"] = attrToProjector["x"]; + attrToProjector["cy"] = attrToProjector["y"]; + delete attrToProjector["x"]; + delete attrToProjector["y"]; + + this.dataSelection = this.renderArea.selectAll("circle").data(this._dataSource.data()); + this.dataSelection.enter().append("circle"); + this.dataSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + } + } +} diff --git a/src/renderers/gridRenderer.ts b/src/renderers/gridRenderer.ts new file mode 100644 index 0000000000..71d793829f --- /dev/null +++ b/src/renderers/gridRenderer.ts @@ -0,0 +1,65 @@ +/// + +module Plottable { + export class GridRenderer extends XYRenderer { + public colorScale: Scale; + public xScale: OrdinalScale; + public yScale: OrdinalScale; + + /** + * Creates a GridRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {OrdinalScale} xScale The x scale to use. + * @param {OrdinalScale} yScale The y scale to use. + * @param {ColorScale|InterpolatedColorScale} colorScale The color scale to use for each grid + * cell. + * @param {IAccessor|string|number} [xAccessor] An accessor for extracting + * the x position of each grid cell from the data. + * @param {IAccessor|string|number} [yAccessor] An accessor for extracting + * the y position of each grid cell from the data. + * @param {IAccessor|string|number} [valueAccessor] An accessor for + * extracting value of each grid cell from the data. This value will + * be pass through the color scale to determine the color of the cell. + */ + constructor(dataset: any, + xScale: OrdinalScale, + yScale: OrdinalScale, + colorScale: Scale, + xAccessor?: any, + yAccessor?: any, + valueAccessor?: any) { + super(dataset, xScale, yScale, xAccessor, yAccessor); + this.classed("grid-renderer", true); + + // The x and y scales should render in bands with no padding + this.xScale.rangeType("bands", 0, 0); + this.yScale.rangeType("bands", 0, 0); + + this.colorScale = colorScale; + this.project("fill", valueAccessor, colorScale); + } + + public _paint() { + super._paint(); + + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); + this.dataSelection.enter().append("rect"); + + var xStep = this.xScale.rangeBand(); + var yr = this.yScale.range(); + var yStep = this.yScale.rangeBand(); + var yMax = Math.max(yr[0], yr[1]) - yStep; + + var attrToProjector = this._generateAttrToProjector(); + attrToProjector["width"] = () => xStep; + attrToProjector["height"] = () => yStep; + var yAttr = attrToProjector["y"]; + attrToProjector["y"] = (d: any, i: number) => yMax - yAttr(d,i); + + this.dataSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + } + } +} diff --git a/src/renderers/lineRenderer.ts b/src/renderers/lineRenderer.ts new file mode 100644 index 0000000000..9f20d24367 --- /dev/null +++ b/src/renderers/lineRenderer.ts @@ -0,0 +1,41 @@ +/// + +module Plottable { + export class LineRenderer extends XYRenderer { + private path: D3.Selection; + private line: D3.Svg.Line; + +/** + * Creates a LineRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting x values from the data. + * @param {IAccessor} [yAccessor] A function for extracting y values from the data. + */ + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor) { + super(dataset, xScale, yScale, xAccessor, yAccessor); + this.classed("line-renderer", true); + } + + public _anchor(element: D3.Selection) { + super._anchor(element); + this.path = this.renderArea.append("path").classed("line", true); + return this; + } + + public _paint() { + super._paint(); + var attrToProjector = this._generateAttrToProjector(); + this.line = d3.svg.line() + .x(attrToProjector["x"]) + .y(attrToProjector["y"]); + this.dataSelection = this.path.datum(this._dataSource.data()); + delete attrToProjector["x"]; + delete attrToProjector["y"]; + this.path.attr("d", this.line).attr(attrToProjector); + } + } +} diff --git a/src/renderers/renderer.ts b/src/renderers/renderer.ts new file mode 100644 index 0000000000..2e6fe3aef0 --- /dev/null +++ b/src/renderers/renderer.ts @@ -0,0 +1,134 @@ +/// + +module Plottable { + export interface _IProjector { + accessor: IAccessor; + scale?: Scale; + } + + export class Renderer extends Component { + public _dataSource: DataSource; + + public renderArea: D3.Selection; + public element: D3.Selection; + public scales: Scale[]; + public _colorAccessor: IAccessor; + public _animate = false; + public _hasRendered = false; + private static defaultColorAccessor = (d: any) => "#1f77b4"; + public _projectors: { [attrToSet: string]: _IProjector; } = {}; + + public _rerenderUpdateSelection = false; + // A perf-efficient manner of rendering would be to calculate attributes only + // on new nodes, and assume that old nodes (ie the update selection) can + // maintain their current attributes. If we change the metadata or an + // accessor function, then this property will not be true, and we will need + // to recompute attributes on the entire update selection. + + public _requireRerender = false; + // A perf-efficient approach to rendering scale changes would be to transform + // the container rather than re-render. In the event that the data is changed, + // it will be necessary to do a regular rerender. + + /** + * Creates a Renderer. + * + * @constructor + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Renderer. + */ + constructor(); + constructor(dataset: any[]); + constructor(dataset: DataSource); + constructor(dataset?: any) { + super(); + this.clipPathEnabled = true; + this._fixedWidth = false; + this._fixedHeight = false; + this.classed("renderer", true); + + if (dataset != null) { + if (typeof dataset.data === "function") { // DataSource + this._dataSource = dataset; + } else { + this._dataSource = new DataSource(dataset); + } + this._dataSource.registerListener(this, () => this._render()); + } else { + this._dataSource = new DataSource(); + } + } + + /** + * Retrieves the current DataSource, or sets a DataSource if the Renderer doesn't yet have one. + * + * @param {DataSource} [source] The DataSource the Renderer should use, if it doesn't yet have one. + * @return {DataSource|Renderer} The current DataSource or the calling Renderer. + */ + public dataSource(): DataSource; + public dataSource(source: DataSource): Renderer; + public dataSource(source?: DataSource): any { + if (source == null) { + return this._dataSource; + } else if (this._dataSource == null) { + this._dataSource = source; + this._dataSource.registerListener(this, () => this._render()); + return this; + } else { + throw new Error("Can't set a new DataSource on the Renderer if it already has one."); + } + } + + public project(attrToSet: string, accessor: any, scale?: Scale) { + var rendererIDAttr = this._plottableID + attrToSet; + var currentProjection = this._projectors[attrToSet]; + var existingScale = (currentProjection != null) ? currentProjection.scale : null; + if (scale == null) { + scale = existingScale; + } + if (existingScale != null) { + existingScale._removePerspective(rendererIDAttr); + existingScale.deregisterListener(this); + } + if (scale != null) { + scale._addPerspective(rendererIDAttr, this.dataSource(), accessor); + scale.registerListener(this, () => this._render()); + } + this._projectors[attrToSet] = {accessor: accessor, scale: scale}; + this._requireRerender = true; + this._rerenderUpdateSelection = true; + return this; + } + + public _generateAttrToProjector(): { [attrToSet: string]: IAppliedAccessor; } { + var h: { [attrName: string]: IAppliedAccessor; } = {}; + d3.keys(this._projectors).forEach((a) => { + var projector = this._projectors[a]; + var accessor = Utils.applyAccessor(projector.accessor, this.dataSource()); + var scale = projector.scale; + var fn = scale == null ? accessor : (d: any, i: number) => scale.scale(accessor(d, i)); + h[a] = fn; + }); + return h; + } + + public _render(): Renderer { + if (this.element != null) { + this._hasRendered = true; + this._paint(); + this._requireRerender = false; + this._rerenderUpdateSelection = false; + } + return this; + } + + public _paint() { + // no-op + } + + public _anchor(element: D3.Selection) { + super._anchor(element); + this.renderArea = this.content.append("g").classed("render-area", true); + return this; + } + } +} diff --git a/src/renderers/squareRenderer.ts b/src/renderers/squareRenderer.ts new file mode 100644 index 0000000000..27193ac240 --- /dev/null +++ b/src/renderers/squareRenderer.ts @@ -0,0 +1,41 @@ +/// + +module Plottable { + export class SquareRenderer extends XYRenderer { + private _rAccessor: any; + private static defaultRAccessor = 3; + + /** + * Creates a SquareRenderer. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting x values from the data. + * @param {IAccessor} [yAccessor] A function for extracting y values from the data. + * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. + */ + constructor(dataset: any, xScale: Scale, yScale: Scale, + xAccessor?: IAccessor, yAccessor?: IAccessor, rAccessor?: IAccessor) { + super(dataset, xScale, yScale, xAccessor, yAccessor); + this.project("r", 3); + this.classed("square-renderer", true); + } + + public _paint() { + super._paint(); + var attrToProjector = this._generateAttrToProjector(); + var xF = attrToProjector["x"]; + var yF = attrToProjector["y"]; + var rF = attrToProjector["r"]; + attrToProjector["x"] = (d: any, i: number) => xF(d, i) - rF(d, i); + attrToProjector["y"] = (d: any, i: number) => yF(d, i) - rF(d, i); + + this.dataSelection = this.renderArea.selectAll("rect").data(this._dataSource.data()); + this.dataSelection.enter().append("rect"); + this.dataSelection.attr(attrToProjector); + this.dataSelection.exit().remove(); + } + } +} diff --git a/src/renderers/xyRenderer.ts b/src/renderers/xyRenderer.ts new file mode 100644 index 0000000000..7cd4f4de6e --- /dev/null +++ b/src/renderers/xyRenderer.ts @@ -0,0 +1,62 @@ +/// + +module Plottable { + export class XYRenderer extends Renderer { + public dataSelection: D3.UpdateSelection; + public xScale: Scale; + public yScale: Scale; + public _xAccessor: any; + public _yAccessor: any; + + /** + * Creates an XYRenderer. + * + * @constructor + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Renderer. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + * @param {IAccessor} [xAccessor] A function for extracting x values from the data. + * @param {IAccessor} [yAccessor] A function for extracting y values from the data. + */ + constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor: any = "x", yAccessor: any = "y") { + super(dataset); + this.classed("xy-renderer", true); + + this.project("x", xAccessor, xScale); + this.project("y", yAccessor, yScale); + } + + public project(attrToSet: string, accessor: any, scale?: Scale) { + super.project(attrToSet, accessor, scale); + // We only want padding and nice-ing on scales that will correspond to axes / pixel layout. + // So when we get an "x" or "y" scale, enable autoNiceing and autoPadding. + if (attrToSet === "x") { + this._xAccessor = this._projectors["x"].accessor; + this.xScale = this._projectors["x"].scale; + this.xScale._autoNice = true; + this.xScale._autoPad = true; + } + if (attrToSet === "y") { + this._yAccessor = this._projectors["y"].accessor; + this.yScale = this._projectors["y"].scale; + this.yScale._autoNice = true; + this.yScale._autoPad = true; + } + return this; + } + + public _computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight? :number) { + this._hasRendered = false; + super._computeLayout(xOffset, yOffset, availableWidth, availableHeight); + this.xScale.range([0, this.availableWidth]); + this.yScale.range([this.availableHeight, 0]); + return this; + } + + private rescale() { + if (this.element != null && this._hasRendered) { + this._render(); + } + } + } +} diff --git a/src/scales/colorScale.ts b/src/scales/colorScale.ts index ab26570bd4..bf8a76fd0c 100644 --- a/src/scales/colorScale.ts +++ b/src/scales/colorScale.ts @@ -2,11 +2,13 @@ module Plottable { export class ColorScale extends Scale { + /** * Creates a ColorScale. * * @constructor - * @param {string} [scaleType] the type of color scale to create (Category10/Category20/Category20b/Category20c) + * @param {string} [scaleType] the type of color scale to create + * (Category10/Category20/Category20b/Category20c). */ constructor(scaleType?: string) { var scale: D3.Scale.Scale; diff --git a/src/scales/interpolatedColorScale.ts b/src/scales/interpolatedColorScale.ts new file mode 100644 index 0000000000..f0e8fb400b --- /dev/null +++ b/src/scales/interpolatedColorScale.ts @@ -0,0 +1,113 @@ +/// + +module Plottable { + export class InterpolatedColorScale extends LinearScale { + private static COLOR_SCALES = { + reds : [ + "#FFFFFF", // white + "#FFF6E1", + "#FEF4C0", + "#FED976", + "#FEB24C", + "#FD8D3C", + "#FC4E2A", + "#E31A1C", + "#B10026" // red + ], + blues : [ + "#FFFFFF", // white + "#CCFFFF", + "#A5FFFD", + "#85F7FB", + "#6ED3EF", + "#55A7E0", + "#417FD0", + "#2545D3", + "#0B02E1" // blue + ], + posneg : [ + "#0B02E1", // blue + "#2545D3", + "#417FD0", + "#55A7E0", + "#6ED3EF", + "#85F7FB", + "#A5FFFD", + "#CCFFFF", + "#FFFFFF", // white + "#FFF6E1", + "#FEF4C0", + "#FED976", + "#FEB24C", + "#FD8D3C", + "#FC4E2A", + "#E31A1C", + "#B10026" // red + ] + }; + + /** + * Converts the string array into a linear d3 scale. + * + * d3 doesn't accept more than 2 range values unless we use a ordinal + * scale. So, in order to interpolate smoothly between the full color + * range, we must override the interpolator and compute the color values + * manually. + * + * @param {string[]} [colors] an array of strings representing color + * values in hex ("#FFFFFF") or keywords ("white"). + * @returns a linear d3 scale. + */ + private static INTERPOLATE_COLORS(colors:string[]): D3.Scale.LinearScale { + if (colors.length < 2) throw new Error("Color scale arrays must have at least two elements."); + return d3.scale.linear() + .range([0, 1]) + .interpolate((ignored:any): any => { + return (t: any): any => { + // Clamp t parameter to [0,1] + t = Math.max(0, Math.min(1, t)); + + // Determine indices for colors + var tScaled = t*(colors.length - 1); + var i0 = Math.floor(tScaled); + var i1 = Math.ceil(tScaled); + var frac = (tScaled - i0); + + // Interpolate in the L*a*b color space + return d3.interpolateLab(colors[i0], colors[i1])(frac); + }; + }); + } + + /** + * Creates a InterpolatedColorScale. + * + * @constructor + * @param {string|string[]} [scaleType] the type of color scale to create + * (reds/blues/posneg). Default is "reds". An array of color values + * with at least 2 values may also be passed (e.g. ["#FF00FF", "red", + * "dodgerblue"], in which case the resulting scale will interpolate + * linearly between the color values across the domain. + */ + constructor(scaleType?: any) { + var scale: D3.Scale.LinearScale; + if (scaleType instanceof Array){ + scale = InterpolatedColorScale.INTERPOLATE_COLORS(scaleType); + } else { + switch (scaleType) { + case "blues": + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["blues"]); + break; + case "posneg": + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["posneg"]); + break; + case "reds": + default: + scale = InterpolatedColorScale.INTERPOLATE_COLORS(InterpolatedColorScale.COLOR_SCALES["reds"]); + break; + } + } + super(scale); + } + } +} diff --git a/src/scales/linearScale.ts b/src/scales/linearScale.ts index 7f72350c8b..38d15a89ad 100644 --- a/src/scales/linearScale.ts +++ b/src/scales/linearScale.ts @@ -13,7 +13,6 @@ module Plottable { constructor(scale: D3.Scale.LinearScale); constructor(scale?: any) { super(scale == null ? d3.scale.linear() : scale); - this.domain([Infinity, -Infinity]); } /** diff --git a/src/scales/ordinalScale.ts b/src/scales/ordinalScale.ts index e84bfaf632..9a44a21b39 100644 --- a/src/scales/ordinalScale.ts +++ b/src/scales/ordinalScale.ts @@ -3,12 +3,13 @@ module Plottable { export class OrdinalScale extends Scale { public _d3Scale: D3.Scale.OrdinalScale; - // Padding as a proportion of the spacing between domain values - private INNER_PADDING = 0.3; - private OUTER_PADDING = 0.5; private _range = [0, 1]; private _rangeType: string = "points"; + // Padding as a proportion of the spacing between domain values + private _innerPadding: number = 0.3; + private _outerPadding: number = 0.5; + /** * Creates a new OrdinalScale. Domain and Range are set later. * @@ -18,17 +19,30 @@ module Plottable { super(d3.scale.ordinal()); } + public _getCombinedExtent(): any [] { + var extents = super._getCombinedExtent(); + var concatenatedExtents: string[] = []; + extents.forEach((e) => { + concatenatedExtents = concatenatedExtents.concat(e); + }); + return Utils.uniq(concatenatedExtents); + } + + /** + * Retrieves the current domain, or sets the Scale's domain to the specified values. + * + * @param {any[]} [values] The new values for the domain. This array may contain more than 2 values. + * @returns {any[]|Scale} The current domain, or the calling Scale (if values is supplied). + */ public domain(): any[]; - public domain(values: any[]): Scale; + public domain(values: any[]): OrdinalScale; public domain(values?: any[]): any { - if (values == null) { - return this._d3Scale.domain(); - } else { - this._d3Scale.domain(values); - this._broadcasterCallbacks.forEach((b) => b(this)); - this.range(this.range()); // update range - return this; - } + return super.domain(values); + } + + public _setDomain(values: any[]) { + super._setDomain(values); + this.range(this.range()); // update range } /** @@ -41,13 +55,14 @@ module Plottable { public range(values: number[]): OrdinalScale; public range(values?: number[]): any { if (values == null) { + this._autoDomainIfNeeded(); return this._range; } else { this._range = values; - if (this._rangeType === "points"){ - this._d3Scale.rangePoints(values, 2*this.OUTER_PADDING); // d3 scale takes total padding + if (this._rangeType === "points") { + this._d3Scale.rangePoints(values, 2*this._outerPadding); // d3 scale takes total padding } else if (this._rangeType === "bands") { - this._d3Scale.rangeBands(values, this.INNER_PADDING, this.OUTER_PADDING); + this._d3Scale.rangeBands(values, this._innerPadding, this._outerPadding); } return this; } @@ -59,56 +74,37 @@ module Plottable { * @returns {number} The range band width or 0 if rangeType isn't "bands". */ public rangeBand() : number { - if (this._rangeType === "bands") { - return this._d3Scale.rangeBand(); - } else { - return 0; - } + this._autoDomainIfNeeded(); + return this._d3Scale.rangeBand(); } /** * Returns the range type, or sets the range type. * - * @param {string} [rangeType] Either "points" or "bands" indicating the d3 method used to generate range bounds. - * @returns {string|OrdinalScale} The current range type, or the calling OrdinalScale. + * @param {string} [rangeType] Either "points" or "bands" indicating the + * d3 method used to generate range bounds. + * @param {number} [outerPadding] The padding outside the range, + * proportional to the range step. + * @param {number} [innerPadding] The padding between bands in the range, + * proportional to the range step. This parameter is only used in + * "bands" type ranges. + * @returns {string|OrdinalScale} The current range type, or the calling + * OrdinalScale. */ public rangeType() : string; - public rangeType(rangeType: string) : OrdinalScale; - public rangeType(rangeType?: string) : any { - if (rangeType == null){ + public rangeType(rangeType: string, outerPadding?: number, innerPadding?: number) : OrdinalScale; + public rangeType(rangeType?: string, outerPadding?: number, innerPadding?: number) : any { + if (rangeType == null) { return this._rangeType; } else { - if(!(rangeType === "points" || rangeType === "bands")){ + if(!(rangeType === "points" || rangeType === "bands")) { throw new Error("Unsupported range type: " + rangeType); } this._rangeType = rangeType; + if(outerPadding != null) this._outerPadding = outerPadding; + if(innerPadding != null) this._innerPadding = innerPadding; return this; } } - - public widenDomainOnData(data: any[], accessor?: IAccessor): OrdinalScale { - var changed = false; - var newDomain = this.domain(); - var a: (d: any, i: number) => any; - if (accessor == null) { - a = (d, i) => d; - } else if (typeof(accessor) === "string") { - a = (d, i) => d[accessor]; - } else if (typeof(accessor) === "function") { - a = accessor; - } else { - a = (d, i) => accessor; - } - data.map(a).forEach((d) => { - if (newDomain.indexOf(d) === -1) { - newDomain.push(d); - changed = true; - } - }); - if (changed) { - this.domain(newDomain); - } - return this; - } } } diff --git a/src/scales/quantitiveScale.ts b/src/scales/quantitiveScale.ts index ff164bc034..66b468c2fe 100644 --- a/src/scales/quantitiveScale.ts +++ b/src/scales/quantitiveScale.ts @@ -15,6 +15,24 @@ module Plottable { super(scale); } + public _getCombinedExtent(): any [] { + var extents = super._getCombinedExtent(); + var starts: number[] = extents.map((e) => e[0]); + var ends: number[] = extents.map((e) => e[1]); + return [d3.min(starts), d3.max(ends)]; + } + + public autorangeDomain() { + super.autorangeDomain(); + if (this._autoPad) { + this.padDomain(); + } + if (this._autoNice) { + this.nice(); + } + return this; + } + /** * Retrieves the domain value corresponding to a supplied range value. * @@ -34,31 +52,10 @@ module Plottable { return new QuantitiveScale(this._d3Scale.copy()); } - /** - * Expands the QuantitiveScale's domain to cover the new region. - * - * @param {number[]} newDomain The additional domain to be covered by the QuantitiveScale. - * @returns {QuantitiveScale} The scale. - */ - public widenDomain(newDomain: number[]) { - var currentDomain = this.domain(); - var wideDomain = [Math.min(newDomain[0], currentDomain[0]), Math.max(newDomain[1], currentDomain[1])]; - this.domain(wideDomain); - return this; - } - - /** - * Expands the QuantitiveScale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data. - * @returns {QuantitiveScale} The scale. - */ - public widenDomainOnData(data: any[], accessor?: IAccessor) { - var extent = d3.extent(data, accessor); - this.widenDomain(extent); - return this; + public domain(): any[]; + public domain(values: any[]): QuantitiveScale; + public domain(values?: any[]): any { + return super.domain(values); // need to override type sig to enable method chaining :/ } /** @@ -110,7 +107,7 @@ module Plottable { */ public nice(count?: number) { this._d3Scale.nice(count); - this.domain(this._d3Scale.domain()); // nice() can change the domain, so update all listeners + this._setDomain(this._d3Scale.domain()); // nice() can change the domain, so update all listeners return this; } @@ -148,7 +145,7 @@ module Plottable { var currentDomain = this.domain(); var extent = currentDomain[1]-currentDomain[0]; var newDomain = [currentDomain[0] - padProportion/2 * extent, currentDomain[1] + padProportion/2 * extent]; - this.domain(newDomain); + this._setDomain(newDomain); return this; } } diff --git a/src/scales/scale.ts b/src/scales/scale.ts index 1d0fcb7ebb..e9d4200a95 100644 --- a/src/scales/scale.ts +++ b/src/scales/scale.ts @@ -1,10 +1,18 @@ /// module Plottable { - export class Scale implements IBroadcaster { + interface IPerspective { + dataSource: DataSource; + accessor: IAccessor; + } + export class Scale extends Broadcaster { public _d3Scale: D3.Scale.Scale; - public _broadcasterCallbacks: IBroadcasterCallback[] = []; - + public _autoDomain = true; + private rendererID2Perspective: {[rendererID: string]: IPerspective} = {}; + private dataSourceReferenceCounter = new Utils.IDCounter(); + private isAutorangeUpToDate = false; + public _autoNice = false; + public _autoPad = false; /** * Creates a new Scale. * @@ -12,9 +20,66 @@ module Plottable { * @param {D3.Scale.Scale} scale The D3 scale backing the Scale. */ constructor(scale: D3.Scale.Scale) { + super(); this._d3Scale = scale; } + public _getCombinedExtent(): any[] { + var perspectives: IPerspective[] = d3.values(this.rendererID2Perspective); + var extents = perspectives.map((p) => { + var source = p.dataSource; + var accessor = p.accessor; + return source._getExtent(accessor); + }); + return extents; + } + + /** + * Modify the domain on the scale so that it includes the extent of all + * perspectives it depends on. Extent: The (min, max) pair for a + * QuantitiativeScale, all covered strings for an OrdinalScale. + * Perspective: A combination of a DataSource and an Accessor that + * represents a view in to the data. + */ + public autorangeDomain() { + this.isAutorangeUpToDate = true; + this._setDomain(this._getCombinedExtent()); + return this; + } + + public _autoDomainIfNeeded() { + if (!this.isAutorangeUpToDate && this._autoDomain) { + this.autorangeDomain(); + } + } + + public _addPerspective(rendererIDAttr: string, dataSource: DataSource, accessor: any) { + if (this.rendererID2Perspective[rendererIDAttr] != null) { + this._removePerspective(rendererIDAttr); + } + this.rendererID2Perspective[rendererIDAttr] = {dataSource: dataSource, accessor: accessor}; + + var dataSourceID = dataSource._plottableID; + if (this.dataSourceReferenceCounter.increment(dataSourceID) === 1 ) { + dataSource.registerListener(this, () => this.isAutorangeUpToDate = false ); + } + + this.isAutorangeUpToDate = false; + return this; + } + + public _removePerspective(rendererIDAttr: string) { + var dataSource = this.rendererID2Perspective[rendererIDAttr].dataSource; + var dataSourceID = dataSource._plottableID; + if (this.dataSourceReferenceCounter.decrement(dataSourceID) === 0) { + dataSource.deregisterListener(this); + } + + delete this.rendererID2Perspective[rendererIDAttr]; + this.isAutorangeUpToDate = false; + return this; + } + /** * Returns the range value corresponding to a given domain value. * @@ -22,27 +87,37 @@ module Plottable { * @returns {any} The range value corresponding to the supplied domain value. */ public scale(value: any) { + this._autoDomainIfNeeded(); return this._d3Scale(value); } /** * Retrieves the current domain, or sets the Scale's domain to the specified values. * - * @param {any[]} [values] The new value for the domain. + * @param {any[]} [values] The new value for the domain. This array may + * contain more than 2 values if the scale type allows it (e.g. + * ordinal scales). Other scales such as quantitative scales accept + * only a 2-value extent array. * @returns {any[]|Scale} The current domain, or the calling Scale (if values is supplied). */ public domain(): any[]; public domain(values: any[]): Scale; public domain(values?: any[]): any { if (values == null) { + this._autoDomainIfNeeded(); return this._d3Scale.domain(); } else { - this._d3Scale.domain(values); - this._broadcasterCallbacks.forEach((b) => b(this)); + this._autoDomain = false; + this._setDomain(values); return this; } } + public _setDomain(values: any[]) { + this._d3Scale.domain(values); + this._broadcast(); + } + /** * Retrieves the current range, or sets the Scale's range to the specified values. * @@ -53,6 +128,7 @@ module Plottable { public range(values: any[]): Scale; public range(values?: any[]): any { if (values == null) { + this._autoDomainIfNeeded(); return this._d3Scale.range(); } else { this._d3Scale.range(values); @@ -68,29 +144,5 @@ module Plottable { public copy(): Scale { return new Scale(this._d3Scale.copy()); } - - /** - * Registers a callback to be called when the scale's domain is changed. - * - * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. - * @returns {Scale} The Calling Scale. - */ - public registerListener(callback: IBroadcasterCallback) { - this._broadcasterCallbacks.push(callback); - return this; - } - - /** - * Expands the Scale's domain to cover the data given. - * Passes an accessor through to the native d3 code. - * - * @param data The data to operate on. - * @param [accessor] The accessor to get values out of the data - * @returns {Scale} The Scale. - */ - public widenDomainOnData(data: any[], accessor?: IAccessor): Scale { - // no-op; implementation is sublcass-dependent - return this; - } } } diff --git a/src/squareRenderer.ts b/src/squareRenderer.ts deleted file mode 100644 index 201b78841c..0000000000 --- a/src/squareRenderer.ts +++ /dev/null @@ -1,56 +0,0 @@ -/// - -module Plottable { - export class SquareRenderer extends NumericXYRenderer { - private _rAccessor: any; - private static defaultRAccessor = 3; - - /** - * Creates a SquareRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {QuantitiveScale} xScale The x scale to use. - * @param {QuantitiveScale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - * @param {IAccessor} [rAccessor] A function for extracting radius values from the data. - */ - constructor(dataset: any, xScale: QuantitiveScale, yScale: QuantitiveScale, - xAccessor?: IAccessor, yAccessor?: IAccessor, rAccessor?: IAccessor) { - super(dataset, xScale, yScale, xAccessor, yAccessor); - this._rAccessor = (rAccessor != null) ? rAccessor : SquareRenderer.defaultRAccessor; - this.classed("square-renderer", true); - } - - - public rAccessor(a: any) { - this._rAccessor = a; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - } - - public _paint() { - super._paint(); - var xA = this._getAppliedAccessor(this._xAccessor); - var yA = this._getAppliedAccessor(this._yAccessor); - var rA = this._getAppliedAccessor(this._rAccessor); - var cA = this._getAppliedAccessor(this._colorAccessor); - var xFn = (d: any, i: number) => - this.xScale.scale(xA(d, i)) - rA(d, i); - - var yFn = (d: any, i: number) => - this.yScale.scale(yA(d, i)) - rA(d, i); - - this.dataSelection = this.renderArea.selectAll("rect").data(this._data); - this.dataSelection.enter().append("rect"); - this.dataSelection.attr("x", xFn) - .attr("y", yFn) - .attr("width", rA) - .attr("height", rA) - .attr("fill", cA); - this.dataSelection.exit().remove(); - } - } -} diff --git a/src/utils.ts b/src/utils.ts index 8e076cadbc..d82cff7853 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -116,5 +116,109 @@ module Plottable { return width; } + export function accessorize(accessor: any): IAccessor { + if (typeof(accessor) === "function") { + return ( accessor); + } else if (typeof(accessor) === "string" && accessor[0] !== "#") { + return (d: any, i: number, s: any) => d[accessor]; + } else { + return (d: any, i: number, s: any) => accessor; + }; + } + + export function applyAccessor(accessor: IAccessor, dataSource: DataSource) { + var activatedAccessor = accessorize(accessor); + return (d: any, i: number) => activatedAccessor(d, i, dataSource.metadata()); + } + + export function uniq(strings: string[]): string[] { + var seen: {[s: string]: boolean} = {}; + strings.forEach((s) => seen[s] = true); + return d3.keys(seen); + } + + /** + * An associative array that can be keyed by anything (inc objects). + * Uses pointer equality checks which is why this works. + * This power has a price: everything is linear time since it is actually backed by an array... + */ + export class StrictEqualityAssociativeArray { + private keyValuePairs: any[][] = []; + + /** + * Set a new key/value pair in the store. + * + * @param {any} Key to set in the store + * @param {any} Value to set in the store + * @return {boolean} True if key already in store, false otherwise + */ + public set(key: any, value: any) { + for (var i=0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + this.keyValuePairs[i][1] = value; + return true; + } + } + this.keyValuePairs.push([key, value]); + return false; + } + + public get(key: any): any { + for (var i=0; i x[1]); + } + + public delete(key: any): boolean { + for (var i=0; i < this.keyValuePairs.length; i++) { + if (this.keyValuePairs[i][0] === key) { + this.keyValuePairs.splice(i, 1); + return true; + } + } + return false; + } + } + + export class IDCounter { + private counter: {[id: string]: number} = {}; + + private setDefault(id: any) { + if (this.counter[id] == null) { + this.counter[id] = 0; + } + } + + public increment(id: any): number { + this.setDefault(id); + return ++this.counter[id]; + } + + public decrement(id: any): number { + this.setDefault(id); + return --this.counter[id]; + } + + public get(id: any): number { + this.setDefault(id); + return this.counter[id]; + } + } } } diff --git a/src/xyRenderer.ts b/src/xyRenderer.ts deleted file mode 100644 index 916bad4bc3..0000000000 --- a/src/xyRenderer.ts +++ /dev/null @@ -1,82 +0,0 @@ -/// - -module Plottable { - export class XYRenderer extends Renderer { - public dataSelection: D3.UpdateSelection; - public xScale: Scale; - public yScale: Scale; - public _xAccessor: any; - public _yAccessor: any; - public autorangeDataOnLayout = true; - - /** - * Creates an XYRenderer. - * - * @constructor - * @param {IDataset} dataset The dataset to render. - * @param {Scale} xScale The x scale to use. - * @param {Scale} yScale The y scale to use. - * @param {IAccessor} [xAccessor] A function for extracting x values from the data. - * @param {IAccessor} [yAccessor] A function for extracting y values from the data. - */ - constructor(dataset: any, xScale: Scale, yScale: Scale, xAccessor?: IAccessor, yAccessor?: IAccessor) { - super(dataset); - this.classed("xy-renderer", true); - - this._xAccessor = (xAccessor != null) ? xAccessor : "x"; // default - this._yAccessor = (yAccessor != null) ? yAccessor : "y"; // default - - this.xScale = xScale; - this.yScale = yScale; - - this.xScale.registerListener(() => this.rescale()); - this.yScale.registerListener(() => this.rescale()); - } - - public xAccessor(accessor: any) { - this._xAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - } - - public yAccessor(accessor: any) { - this._yAccessor = accessor; - this._requireRerender = true; - this._rerenderUpdateSelection = true; - return this; - } - - public _computeLayout(xOffset?: number, yOffset?: number, availableWidth?: number, availableHeight? :number) { - this._hasRendered = false; - super._computeLayout(xOffset, yOffset, availableWidth, availableHeight); - this.xScale.range([0, this.availableWidth]); - this.yScale.range([this.availableHeight, 0]); - if (this.autorangeDataOnLayout) { - this.autorange(); - } - return this; - } - - /** - * Autoranges the scales over the data. - * Actual behavior is dependent on the scales. - */ - public autorange() { - super.autorange(); - var data = this._data; - var xA = (d: any) => this._getAppliedAccessor(this._xAccessor)(d, null); - this.xScale.widenDomainOnData(data, xA); - - var yA = (d: any) => this._getAppliedAccessor(this._yAccessor)(d, null); - this.yScale.widenDomainOnData(data, yA); - return this; - } - - private rescale() { - if (this.element != null && this._hasRendered) { - this._render(); - } - } - } -} diff --git a/test/broadcasterTests.ts b/test/broadcasterTests.ts new file mode 100644 index 0000000000..d63e36fc3a --- /dev/null +++ b/test/broadcasterTests.ts @@ -0,0 +1,57 @@ +/// + +var assert = chai.assert; + +describe("Broadcasters", () => { + var b: Plottable.Broadcaster; + var called: boolean; + var cb: any; + + beforeEach(() => { + b = new Plottable.Broadcaster(); + called = false; + cb = () => {called = true;}; + }); + it("listeners are called by the broadcast method", () => { + b.registerListener(null, cb); + b._broadcast(); + assert.isTrue(called, "callback was called"); + }); + + it("same listener can only be associated with one callback", () => { + var called2 = false; + var cb2 = () => {called2 = true;}; + var listener = {}; + b.registerListener(listener, cb); + b.registerListener(listener, cb2); + b._broadcast(); + assert.isFalse(called, "first (overwritten) callback not called"); + assert.isTrue(called2, "second callback was called"); + }); + + it("listeners can be deregistered", () => { + var listener = {}; + b.registerListener(listener, cb); + b.deregisterListener(listener); + b._broadcast(); + assert.isFalse(called, "callback was never called"); + }); + + it("arguments are passed through to callback", () => { + var g2 = {}; + var g3 = "foo"; + var cb = (a1, rest) => { + assert.equal(b, a1, "broadcaster passed through"); + assert.equal(g2, rest[0], "arg1 passed through"); + assert.equal(g3, rest[1], "arg2 passed through"); + called = true; + }; + b.registerListener(null, cb); + b._broadcast(g2, g3); + assert.isTrue(called, "the cb was called"); + }); + + it("deregistering an unregistered listener throws an error", () => { + assert.throws(() => b.deregisterListener({}) ); + }); +}); diff --git a/test/componentTests.ts b/test/componentTests.ts index 097920e837..2c7197cf7b 100644 --- a/test/componentTests.ts +++ b/test/componentTests.ts @@ -161,7 +161,6 @@ describe("Component behavior", () => { svg.remove(); }); - it("component defaults are as expected", () => { assert.equal(c.rowMinimum(), 0, "rowMinimum defaults to 0"); assert.equal(c.colMinimum(), 0, "colMinimum defaults to 0"); @@ -183,9 +182,8 @@ describe("Component behavior", () => { it("clipPath works as expected", () => { assert.isFalse(c.clipPathEnabled, "clipPathEnabled defaults to false"); c.clipPathEnabled = true; - var expectedClipPathID: number = ( Plottable.Component).clipPathId; + var expectedClipPathID = c._plottableID; c._anchor(svg)._computeLayout(0, 0, 100, 100)._render(); - assert.equal(( Plottable.Component).clipPathId, expectedClipPathID+1, "clipPathId incremented"); var expectedClipPathURL = "url(#clipPath" + expectedClipPathID+ ")"; assert.equal(c.element.attr("clip-path"), expectedClipPathURL, "the element has clip-path url attached"); var clipRect = ( c).boxContainer.select(".clip-rect"); @@ -194,6 +192,15 @@ describe("Component behavior", () => { svg.remove(); }); + it("componentID works as expected", () => { + var expectedID = ( Plottable.PlottableObject).nextID; + var c1 = new Plottable.Component(); + assert.equal(c1._plottableID, expectedID, "component id on next component was as expected"); + var c2 = new Plottable.Component(); + assert.equal(c2._plottableID, expectedID+1, "future components increment appropriately"); + svg.remove(); + }); + it("boxes work as expected", () => { assert.throws(() => ( c).addBox("pre-anchor"), Error, "Adding boxes before anchoring is currently disallowed"); c.renderTo(svg); diff --git a/test/coverage.html b/test/coverage.html new file mode 100644 index 0000000000..062fe10b38 --- /dev/null +++ b/test/coverage.html @@ -0,0 +1,48 @@ + + + + + Plottable.js Tests + + + + + + + + + + + + + + +
+ + + + + + diff --git a/test/dataSourceTests.ts b/test/dataSourceTests.ts new file mode 100644 index 0000000000..843b78b4dd --- /dev/null +++ b/test/dataSourceTests.ts @@ -0,0 +1,53 @@ +/// + +var assert = chai.assert; + +describe("DataSource", () => { + it("Updates listeners when the data is changed", () => { + var ds = new Plottable.DataSource(); + + var newData = [ 1, 2, 3 ]; + + var callbackCalled = false; + var callback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.Broadcaster) => { + assert.equal(broadcaster, ds, "Callback received the DataSource as the first argument"); + assert.deepEqual(ds.data(), newData, "DataSource arrives with correct data"); + callbackCalled = true; + }; + ds.registerListener(null, callback); + + ds.data(newData); + assert.isTrue(callbackCalled, "callback was called when the data was changed"); + }); + + it("Updates listeners when the metadata is changed", () => { + var ds = new Plottable.DataSource(); + + var newMetadata = "blargh"; + + var callbackCalled = false; + var callback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.Broadcaster) => { + assert.equal(broadcaster, ds, "Callback received the DataSource as the first argument"); + assert.deepEqual(ds.metadata(), newMetadata, "DataSource arrives with correct metadata"); + callbackCalled = true; + }; + ds.registerListener(null, callback); + + ds.metadata(newMetadata); + assert.isTrue(callbackCalled, "callback was called when the metadata was changed"); + }); + + it("_getExtent works as expected", () => { + var data = [1,2,3,4,1]; + var metadata = {foo: 11}; + var dataSource = new Plottable.DataSource(data, metadata); + var a1 = (d: number, i: number, m: any) => d + i - 2; + assert.deepEqual(dataSource._getExtent(a1), [-1, 5], "extent for numerical data works properly"); + var a2 = (d: number, i: number, m: any) => d + m.foo; + assert.deepEqual(dataSource._getExtent(a2), [12, 15], "extent uses metadata appropriately"); + dataSource.metadata({foo: -1}); + assert.deepEqual(dataSource._getExtent(a2), [0, 3], "metadata change is reflected in extent results"); + var a3 = (d: number, i: number, m: any) => "_" + d; + assert.deepEqual(dataSource._getExtent(a3), ["_1", "_2", "_3", "_4"], "extent works properly on string domains (no repeats)"); + }); +}); diff --git a/test/interactionTests.ts b/test/interactionTests.ts index fc251c45e0..1461499b36 100644 --- a/test/interactionTests.ts +++ b/test/interactionTests.ts @@ -33,8 +33,8 @@ describe("Interactions", () => { it("Pans properly", () => { // The only difference between pan and zoom is internal to d3 // Simulating zoom events is painful, so panning will suffice here - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); + var xScale = new Plottable.LinearScale().domain([0, 11]); + var yScale = new Plottable.LinearScale().domain([11, 0]); var svg = generateSVG(); var dataset = makeLinearSeries(11); @@ -70,8 +70,8 @@ describe("Interactions", () => { var expectedXDragChange = -dragDistancePixelX * getSlope(xScale); var expectedYDragChange = -dragDistancePixelY * getSlope(yScale); - assert.equal(xDomainAfter[0]-xDomainBefore[0], expectedXDragChange, "x domain changed by the correct amount"); - assert.equal(yDomainAfter[0]-yDomainBefore[0], expectedYDragChange, "y domain changed by the correct amount"); + assert.closeTo(xDomainAfter[0]-xDomainBefore[0], expectedXDragChange, 1, "x domain changed by the correct amount"); + assert.closeTo(yDomainAfter[0]-yDomainBefore[0], expectedYDragChange, 1, "y domain changed by the correct amount"); svg.remove(); }); @@ -81,7 +81,7 @@ describe("Interactions", () => { var svgWidth = 400; var svgHeight = 400; var svg: D3.Selection; - var dataset: Plottable.IDataset; + var dataset: Plottable.DataSource; var xScale: Plottable.QuantitiveScale; var yScale: Plottable.QuantitiveScale; var renderer: Plottable.XYRenderer; @@ -94,7 +94,7 @@ describe("Interactions", () => { before(() => { svg = generateSVG(svgWidth, svgHeight); - dataset = makeLinearSeries(10); + dataset = new Plottable.DataSource(makeLinearSeries(10)); xScale = new Plottable.LinearScale(); yScale = new Plottable.LinearScale(); renderer = new Plottable.CircleRenderer(dataset, xScale, yScale); @@ -150,96 +150,6 @@ describe("Interactions", () => { }); }); - describe("BrushZoomInteraction", () => { - it("Zooms in correctly on drag", () =>{ - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); - - var svgWidth = 400; - var svgHeight = 400; - var svg = generateSVG(svgWidth, svgHeight); - var dataset = makeLinearSeries(11); - var renderer = new Plottable.CircleRenderer(dataset, xScale, yScale); - renderer.renderTo(svg); - - var xDomainBefore = xScale.domain(); - var yDomainBefore = yScale.domain(); - - var dragstartX = 10; - var dragstartY = 210; - var dragendX = 190; - var dragendY = 390; - - var expectedXDomain = [xScale.invert(dragstartX), xScale.invert(dragendX)]; - var expectedYDomain = [yScale.invert(dragendY) , yScale.invert(dragstartY)]; // reversed because Y scale is - - var indicesCallbackCalled = false; - var interaction: any; - var indicesCallback = (indices: number[]) => { - indicesCallbackCalled = true; - interaction.clearBox(); - assert.deepEqual(indices, [1, 2, 3, 4], "the correct points were selected"); - }; - var zoomCallback = new Plottable.ZoomCallbackGenerator().addXScale(xScale).addYScale(yScale).getCallback(); - var callback = (a: Plottable.SelectionArea) => { - var dataArea = renderer.invertXYSelectionArea(a); - var indices = renderer.getDataIndicesFromArea(dataArea); - indicesCallback(indices); - zoomCallback(a); - }; - interaction = new Plottable.AreaInteraction(renderer).callback(callback); - interaction.registerWithComponent(); - - fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); - assert.isTrue(indicesCallbackCalled, "indicesCallback was called"); - assert.deepEqual(xScale.domain(), expectedXDomain, "X scale domain was updated correctly"); - assert.deepEqual(yScale.domain(), expectedYDomain, "Y scale domain was updated correclty"); - - svg.remove(); - }); - }); - - describe("CrosshairsInteraction", () => { - it("Crosshairs manifest basic functionality", () => { - var svg = generateSVG(400, 400); - var dp = (x, y) => { return {x: x, y: y}; }; - var data = [dp(0, 0), dp(20, 10), dp(40, 40)]; - var dataset = {metadata: {cssClass: "foo"}, data: data}; - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); - var circleRenderer = new Plottable.CircleRenderer(dataset, xScale, yScale); - var crosshairs = new Plottable.CrosshairsInteraction(circleRenderer); - crosshairs.registerWithComponent(); - circleRenderer.renderTo(svg); - - var crosshairsG = circleRenderer.foregroundContainer.select(".crosshairs"); - var circle = crosshairsG.select("circle"); - var xLine = crosshairsG.select(".x-line"); - var yLine = crosshairsG.select(".y-line"); - - crosshairs.mousemove(0,0); - assert.equal(circle.attr("cx"), 0, "the crosshairs are at x=0"); - assert.equal(circle.attr("cy"), 400, "the crosshairs are at y=400"); - assert.equal(xLine.attr("d"), "M 0 400 L 400 400", "the xLine behaves properly at y=400"); - assert.equal(yLine.attr("d"), "M 0 0 L 0 400", "the yLine behaves properly at x=0"); - - crosshairs.mousemove(30, 0); - // It should stay in the same position - assert.equal(circle.attr("cx"), 0, "the crosshairs are at x=0 still"); - assert.equal(circle.attr("cy"), 400, "the crosshairs are at y=400 still"); - assert.equal(xLine.attr("d"), "M 0 400 L 400 400", "the xLine behaves properly at y=400"); - assert.equal(yLine.attr("d"), "M 0 0 L 0 400", "the yLine behaves properly at x=0"); - - crosshairs.mousemove(300, 0); - assert.equal(circle.attr("cx"), 200, "the crosshairs are at x=200"); - assert.equal(circle.attr("cy"), 300, "the crosshairs are at y=300"); - assert.equal(xLine.attr("d"), "M 0 300 L 400 300", "the xLine behaves properly at y=300"); - assert.equal(yLine.attr("d"), "M 200 0 L 200 400", "the yLine behaves properly at x=200"); - - svg.remove(); - }); - }); - describe("KeyInteraction", () => { it("Triggers the callback only when the Component is moused over and appropriate key is pressed", () => { var svg = generateSVG(400, 400); diff --git a/test/quicktests/quicktest-categoryRenderer.html b/test/quicktests/quicktest-categoryRenderer.html index 04d9d18eed..1d419f6232 100644 --- a/test/quicktests/quicktest-categoryRenderer.html +++ b/test/quicktests/quicktest-categoryRenderer.html @@ -33,14 +33,14 @@ var xScale = new Plottable.OrdinalScale(); var xAxis = new Plottable.XAxis(xScale, "bottom", function(d) { return d; } ); - var yScale = new Plottable.LinearScale(); + var yScale = new Plottable.LinearScale().domain([0, 15]); var yAxis = new Plottable.YAxis(yScale, "left"); // yAxis.showEndTickLabels(true); var barRenderer = new Plottable.CategoryBarRenderer(data, xScale, yScale); - barRenderer.xAccessor("name"); - barRenderer.widthAccessor(50); - barRenderer.yAccessor("age"); + barRenderer.project("x", "name", xScale); + // barRenderer.widthAccessor(50); + barRenderer.project("y", "age", yScale); var basicTable = new Plottable.Table().addComponent(0, 0, yAxis) .addComponent(0, 1, barRenderer) diff --git a/test/quicktests/quicktest-gridRenderer.html b/test/quicktests/quicktest-gridRenderer.html new file mode 100644 index 0000000000..4217bbdd31 --- /dev/null +++ b/test/quicktests/quicktest-gridRenderer.html @@ -0,0 +1,74 @@ + + + Grid Renderer Quicktest + + + + + + + + + +
+
+
+ + + diff --git a/test/rendererTests.ts b/test/rendererTests.ts index a70581b903..7fa5264dfa 100644 --- a/test/rendererTests.ts +++ b/test/rendererTests.ts @@ -12,63 +12,51 @@ describe("Renderers", () => { assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); }); - it("Base renderer functionality works", () => { + it("Base Renderer functionality works", () => { var svg = generateSVG(400, 300); - var d1 = {data: ["foo"], metadata: {cssClass: "bar"}}; + var d1 = new Plottable.DataSource(["foo"], {cssClass: "bar"}); var r = new Plottable.Renderer(d1); r._anchor(svg)._computeLayout(); var renderArea = r.content.select(".render-area"); assert.isNotNull(renderArea.node(), "there is a render-area"); - assert.isTrue(r.element.classed("bar"), "the element is classed w/ metadata.cssClass"); - assert.deepEqual(r._data, d1.data, "the data is set properly"); - assert.deepEqual(r._metadata, d1.metadata, "the metadata is set properly"); - var d2 = {data: ["bar"], metadata: {cssClass: "boo"}}; - r.dataset(d2); - assert.isFalse(r.element.classed("bar"), "the element is no longer classed bar"); - assert.isTrue (r.element.classed("boo"), "the element is now classed boo"); - assert.deepEqual(r._data, d2.data, "the data is set properly"); - assert.deepEqual(r._metadata, d2.metadata, "the metadata is set properly"); + + var d2 = new Plottable.DataSource(["bar"], {cssClass: "boo"}); + assert.throws(() => r.dataSource(d2), Error); + svg.remove(); }); - it("rerenderUpdateSelection and requireRerender flags updated appropriately", () => { + it("Renderer automatically generates a DataSource if only data is provided", () => { + var data = ["foo", "bar"]; + var r = new Plottable.Renderer(data); + var dataSource = r.dataSource(); + assert.isNotNull(dataSource, "A DataSource was automatically generated"); + assert.deepEqual(dataSource.data(), data, "The generated DataSource has the correct data"); + }); + + it("Renderer.project works as intended", () => { var r = new Plottable.Renderer(); - var svg = generateSVG(); - r.renderTo(svg); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update"); - assert.isFalse(r._requireRerender, "dont require rerender"); - var metadata = {}; - r.metadata(metadata); - assert.isTrue(r._rerenderUpdateSelection, "rerenderingUpdate req after metadata set"); - assert.isTrue(r._requireRerender, "rerender required when metadata set"); - - r.renderTo(svg); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update after render"); - assert.isFalse(r._requireRerender, "dont require rerender after render"); - - var data = []; - r.data(data); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update after setting data"); - assert.isTrue(r._requireRerender, "rerender required when data set"); - svg.remove(); + var s = new Plottable.LinearScale().domain([0, 1]).range([0, 10]); + r.project("attr", "a", s); + var attrToProjector = r._generateAttrToProjector(); + var projector = attrToProjector["attr"]; + assert.equal(projector({"a": 0.5}, 0), 5, "projector works as intended"); }); }); describe("XYRenderer functionality", () => { - it("the accessors properly access data, index, and metadata", () => { var svg = generateSVG(400, 400); var xScale = new Plottable.LinearScale(); var yScale = new Plottable.LinearScale(); + xScale.domain([0, 400]); + yScale.domain([400, 0]); var data = [{x: 0, y: 0}, {x: 1, y: 1}]; var metadata = {foo: 10, bar: 20}; var xAccessor = (d, i?, m?) => d.x + i * m.foo; var yAccessor = (d, i?, m?) => m.bar; - var dataset = {data: data, metadata: metadata}; - var renderer = new Plottable.CircleRenderer(dataset, xScale, yScale, xAccessor, yAccessor); - renderer.autorangeDataOnLayout = false; - xScale.domain([0, 400]); - yScale.domain([400, 0]); + var dataSource = new Plottable.DataSource(data, metadata); + var renderer = new Plottable.CircleRenderer(dataSource, xScale, yScale, xAccessor, yAccessor); renderer.renderTo(svg); var circles = renderer.renderArea.selectAll("circle"); var c1 = d3.select(circles[0][0]); @@ -79,14 +67,14 @@ describe("Renderers", () => { assert.closeTo(parseFloat(c2.attr("cy")), 20, 0.01, "second circle cy is correct"); data = [{x: 2, y:2}, {x:4, y:4}]; - renderer.data(data).renderTo(svg); + dataSource.data(data); assert.closeTo(parseFloat(c1.attr("cx")), 2, 0.01, "first circle cx is correct after data change"); assert.closeTo(parseFloat(c1.attr("cy")), 20, 0.01, "first circle cy is correct after data change"); assert.closeTo(parseFloat(c2.attr("cx")), 14, 0.01, "second circle cx is correct after data change"); assert.closeTo(parseFloat(c2.attr("cy")), 20, 0.01, "second circle cy is correct after data change"); metadata = {foo: 0, bar: 0}; - renderer.metadata(metadata).renderTo(svg); + dataSource.metadata(metadata); assert.closeTo(parseFloat(c1.attr("cx")), 2, 0.01, "first circle cx is correct after metadata change"); assert.closeTo(parseFloat(c1.attr("cy")), 0, 0.01, "first circle cy is correct after metadata change"); assert.closeTo(parseFloat(c2.attr("cx")), 4, 0.01, "second circle cx is correct after metadata change"); @@ -102,19 +90,19 @@ describe("Renderers", () => { var xScale; var yScale; var lineRenderer; - var simpleDataset = {metadata: {cssClass: "simpleDataset"}, data: [{foo: 0, bar:0}, {foo:1, bar:1}]}; + var simpleDataset = new Plottable.DataSource([{foo: 0, bar:0}, {foo:1, bar:1}]); var renderArea; var verifier = new MultiTestVerifier(); before(() => { svg = generateSVG(500, 500); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([0, 1]); + yScale = new Plottable.LinearScale().domain([0, 1]); var xAccessor = (d) => d.foo; var yAccessor = (d) => d.bar; var colorAccessor = (d, i, m) => d3.rgb(d.foo, d.bar, i).toString(); lineRenderer = new Plottable.LineRenderer(simpleDataset, xScale, yScale, xAccessor, yAccessor); - lineRenderer.colorAccessor(colorAccessor); + lineRenderer.project("stroke", colorAccessor); lineRenderer.renderTo(svg); renderArea = lineRenderer.renderArea; }); @@ -196,16 +184,14 @@ describe("Renderers", () => { before(() => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([0, 9]); + yScale = new Plottable.LinearScale().domain([0, 81]); circleRenderer = new Plottable.CircleRenderer(quadraticDataset, xScale, yScale); - circleRenderer.colorAccessor(colorAccessor); + circleRenderer.project("fill", colorAccessor); circleRenderer.renderTo(svg); }); it("setup is handled properly", () => { - assert.deepEqual(xScale.domain(), [0, 9], "xScale domain was set by the renderer"); - assert.deepEqual(yScale.domain(), [0, 81], "yScale domain was set by the renderer"); assert.deepEqual(xScale.range(), [0, SVG_WIDTH], "xScale range was set by the renderer"); assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); @@ -220,37 +206,6 @@ describe("Renderers", () => { verifier.end(); }); - it("invertXYSelectionArea works", () => { - var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull); - assert.deepEqual(actualDataAreaFull, dataAreaFull, "the full data area is as expected"); - - var actualDataAreaPart = circleRenderer.invertXYSelectionArea(pixelAreaPart); - - assert.closeTo(actualDataAreaPart.xMin, dataAreaPart.xMin, 1, "partial xMin is close"); - assert.closeTo(actualDataAreaPart.xMax, dataAreaPart.xMax, 1, "partial xMax is close"); - assert.closeTo(actualDataAreaPart.yMin, dataAreaPart.yMin, 1, "partial yMin is close"); - assert.closeTo(actualDataAreaPart.yMax, dataAreaPart.yMax, 1, "partial yMax is close"); - verifier.end(); - }); - - it("getSelectionFromArea works", () => { - var selectionFull = circleRenderer.getSelectionFromArea(dataAreaFull); - assert.lengthOf(selectionFull[0], 10, "all 10 circles were selected by the full region"); - - var selectionPartial = circleRenderer.getSelectionFromArea(dataAreaPart); - assert.lengthOf(selectionPartial[0], 2, "2 circles were selected by the partial region"); - verifier.end(); - }); - - it("getDataIndicesFromArea works", () => { - var indicesFull = circleRenderer.getDataIndicesFromArea(dataAreaFull); - assert.deepEqual(indicesFull, d3.range(10), "all 10 circles were selected by the full region"); - - var indicesPartial = circleRenderer.getDataIndicesFromArea(dataAreaPart); - assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); - verifier.end(); - }); - describe("after the scale has changed", () => { before(() => { xScale.domain([0, 3]); @@ -259,37 +214,6 @@ describe("Renderers", () => { dataAreaPart = {xMin: 1, xMax: 3, yMin: 6, yMax: 3}; }); - it("invertXYSelectionArea works", () => { - var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull); - assert.deepEqual(actualDataAreaFull, dataAreaFull, "the full data area is as expected"); - - var actualDataAreaPart = circleRenderer.invertXYSelectionArea(pixelAreaPart); - - assert.closeTo(actualDataAreaPart.xMin, dataAreaPart.xMin, 1, "partial xMin is close"); - assert.closeTo(actualDataAreaPart.xMax, dataAreaPart.xMax, 1, "partial xMax is close"); - assert.closeTo(actualDataAreaPart.yMin, dataAreaPart.yMin, 1, "partial yMin is close"); - assert.closeTo(actualDataAreaPart.yMax, dataAreaPart.yMax, 1, "partial yMax is close"); - verifier.end(); - }); - - it("getSelectionFromArea works", () => { - var selectionFull = circleRenderer.getSelectionFromArea(dataAreaFull); - assert.lengthOf(selectionFull[0], 4, "four circles were selected by the full region"); - - var selectionPartial = circleRenderer.getSelectionFromArea(dataAreaPart); - assert.lengthOf(selectionPartial[0], 1, "one circle was selected by the partial region"); - verifier.end(); - }); - - it("getDataIndicesFromArea works", () => { - var indicesFull = circleRenderer.getDataIndicesFromArea(dataAreaFull); - assert.deepEqual(indicesFull, [0,1,2,3], "four circles were selected by the full region"); - - var indicesPartial = circleRenderer.getDataIndicesFromArea(dataAreaPart); - assert.deepEqual(indicesPartial, [2], "circle 2 was selected by the partial region"); - verifier.end(); - }); - it("the circles re-rendered properly", () => { var renderArea = circleRenderer.renderArea; var circles = renderArea.selectAll("circle"); @@ -317,16 +241,14 @@ describe("Renderers", () => { // Choosing data with a negative x value is significant, since there is // a potential failure mode involving the xDomain with an initial // point below 0 - var dataset = {metadata: {cssClass: "sampleBarData"}, data: [d0, d1]}; + var dataset = new Plottable.DataSource([d0, d1]); before(() => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([-2, 4]); + yScale = new Plottable.LinearScale().domain([0, 4]); barRenderer = new Plottable.BarRenderer(dataset, xScale, yScale); barRenderer._anchor(svg)._computeLayout(); - var currentYDomain = yScale.domain(); - yScale.domain([0, currentYDomain[1]]); }); beforeEach(() => { @@ -373,7 +295,7 @@ describe("Renderers", () => { describe("Category Bar Renderer", () => { var verifier = new MultiTestVerifier(); var svg: D3.Selection; - var dataset: Plottable.IDataset; + var dataset: Plottable.DataSource; var xScale: Plottable.OrdinalScale; var yScale: Plottable.LinearScale; var renderer: Plottable.CategoryBarRenderer; @@ -382,15 +304,13 @@ describe("Renderers", () => { before(() => { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.OrdinalScale(); + xScale = new Plottable.OrdinalScale().domain(["A", "B"]); yScale = new Plottable.LinearScale(); - dataset = { - data: [ - {x: "A", y: 1}, - {x: "B", y: 2} - ], - metadata: {cssClass: "letters"} - }; + var data = [ + {x: "A", y: 1}, + {x: "B", y: 2} + ]; + dataset = new Plottable.DataSource(data); renderer = new Plottable.CategoryBarRenderer(dataset, xScale, yScale); renderer._animate = false; @@ -420,5 +340,79 @@ describe("Renderers", () => { if (verifier.passed) {svg.remove();}; }); }); + + describe("Grid Renderer", () => { + var verifier = new MultiTestVerifier(); + var svg: D3.Selection; + var xScale: Plottable.OrdinalScale; + var yScale: Plottable.OrdinalScale; + var colorScale: Plottable.InterpolatedColorScale; + var renderer: Plottable.GridRenderer; + + var SVG_WIDTH = 400; + var SVG_HEIGHT = 200; + + before(() => { + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); + xScale = new Plottable.OrdinalScale(); + yScale = new Plottable.OrdinalScale(); + colorScale = new Plottable.InterpolatedColorScale(["black", "white"]); + var data = [ + {x: "A", y: "U", magnitude: 0}, + {x: "B", y: "U", magnitude: 2}, + {x: "A", y: "V", magnitude: 16}, + {x: "B", y: "V", magnitude: 8}, + ]; + + renderer = new Plottable.GridRenderer(data, xScale, yScale, colorScale, "x", "y", "magnitude"); + renderer.renderTo(svg); + }); + + beforeEach(() => { + verifier.start(); + }); + + it("renders correctly", () => { + var renderArea = renderer.renderArea; + var cells = renderArea.selectAll("rect")[0]; + assert.equal(cells.length, 4); + + var cellAU = d3.select(cells[0]); + var cellBU = d3.select(cells[1]); + var cellAV = d3.select(cells[2]); + var cellBV = d3.select(cells[3]); + + assert.equal(cellAU.attr("height"), "100", "cell 'AU' height is correct"); + assert.equal(cellAU.attr("width"), "200", "cell 'AU' width is correct"); + assert.equal(cellAU.attr("x"), "0", "cell 'AU' x coord is correct"); + assert.equal(cellAU.attr("y"), "0", "cell 'AU' x coord is correct"); + assert.equal(cellAU.attr("fill"), "#000000", "cell 'AU' color is correct"); + + assert.equal(cellBU.attr("height"), "100", "cell 'BU' height is correct"); + assert.equal(cellBU.attr("width"), "200", "cell 'BU' width is correct"); + assert.equal(cellBU.attr("x"), "200", "cell 'BU' x coord is correct"); + assert.equal(cellBU.attr("y"), "0", "cell 'BU' x coord is correct"); + assert.equal(cellBU.attr("fill"), "#212121", "cell 'BU' color is correct"); + + assert.equal(cellAV.attr("height"), "100", "cell 'AV' height is correct"); + assert.equal(cellAV.attr("width"), "200", "cell 'AV' width is correct"); + assert.equal(cellAV.attr("x"), "0", "cell 'AV' x coord is correct"); + assert.equal(cellAV.attr("y"), "100", "cell 'AV' x coord is correct"); + assert.equal(cellAV.attr("fill"), "#ffffff", "cell 'AV' color is correct"); + + assert.equal(cellBV.attr("height"), "100", "cell 'BV' height is correct"); + assert.equal(cellBV.attr("width"), "200", "cell 'BV' width is correct"); + assert.equal(cellBV.attr("x"), "200", "cell 'BV' x coord is correct"); + assert.equal(cellBV.attr("y"), "100", "cell 'BV' x coord is correct"); + assert.equal(cellBV.attr("fill"), "#777777", "cell 'BV' color is correct"); + + verifier.end(); + }); + + after(() => { + if (verifier.passed) {svg.remove();}; + }); + }); + }); }); diff --git a/test/scaleTests.ts b/test/scaleTests.ts index b9124ea23d..70cf81a607 100644 --- a/test/scaleTests.ts +++ b/test/scaleTests.ts @@ -4,26 +4,26 @@ var assert = chai.assert; describe("Scales", () => { it("Scale's copy() works correctly", () => { - var testCallback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.IBroadcaster) => { + var testCallback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.Broadcaster) => { return true; // doesn't do anything }; var scale = new Plottable.Scale(d3.scale.linear()); - scale.registerListener(testCallback); + scale.registerListener(null, testCallback); var scaleCopy = scale.copy(); assert.deepEqual(scale.domain(), scaleCopy.domain(), "Copied scale has the same domain as the original."); assert.deepEqual(scale.range(), scaleCopy.range(), "Copied scale has the same range as the original."); - assert.notDeepEqual(( scale)._broadcasterCallbacks, ( scaleCopy)._broadcasterCallbacks, + assert.notDeepEqual(( scale).listener2Callback, ( scaleCopy).listener2Callback, "Registered callbacks are not copied over"); }); it("Scale alerts listeners when its domain is updated", () => { var scale = new Plottable.QuantitiveScale(d3.scale.linear()); var callbackWasCalled = false; - var testCallback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.IBroadcaster) => { + var testCallback: Plottable.IBroadcasterCallback = (broadcaster: Plottable.Broadcaster) => { assert.equal(broadcaster, scale, "Callback received the calling scale as the first argument"); callbackWasCalled = true; }; - scale.registerListener(testCallback); + scale.registerListener(null, testCallback); scale.domain([0, 10]); assert.isTrue(callbackWasCalled, "The registered callback was called"); @@ -36,22 +36,69 @@ describe("Scales", () => { scale.padDomain(0.2); assert.isTrue(callbackWasCalled, "The registered callback was called when padDomain() is used to set the domain"); }); + describe("autoranging behavior", () => { + var data: any[]; + var dataSource: Plottable.DataSource; + var scale: Plottable.LinearScale; + beforeEach(() => { + data = [{foo: 2, bar: 1}, {foo: 5, bar: -20}, {foo: 0, bar: 0}]; + dataSource = new Plottable.DataSource(data); + scale = new Plottable.LinearScale(); + }); - it("QuantitiveScale.widenDomain() functions correctly", () => { - var scale = new Plottable.QuantitiveScale(d3.scale.linear()); - assert.deepEqual(scale.domain(), [0, 1], "Initial domain is [0, 1]"); - scale.widenDomain([1, 2]); - assert.deepEqual(scale.domain(), [0, 2], "Domain was wided to [0, 2]"); - scale.widenDomain([-1, 1]); - assert.deepEqual(scale.domain(), [-1, 2], "Domain was wided to [-1, 2]"); - scale.widenDomain([0, 1]); - assert.deepEqual(scale.domain(), [-1, 2], "Domain does not get shrink if \"widened\" to a smaller value"); - }); + it("scale autoDomain flag is not overwritten without explicitly setting the domain", () => { + scale._addPerspective("1", dataSource, "foo"); + scale.autorangeDomain().padDomain().nice(); + assert.isTrue(scale._autoDomain, "the autoDomain flag is still set after autoranginging and padding and nice-ing"); + scale.domain([0, 5]); + assert.isFalse(scale._autoDomain, "the autoDomain flag is false after domain explicitly set"); + }); + + it("scale autorange works as expected with single dataSource", () => { + assert.isFalse(( scale).isAutorangeUpToDate, "isAutorangeUpToDate is false by default"); + scale._addPerspective("1x", dataSource, "foo"); + assert.isFalse(( scale).isAutorangeUpToDate, "isAutorangeUpToDate set to false after adding perspective"); + scale.autorangeDomain(); + assert.isTrue(( scale).isAutorangeUpToDate, "isAutorangeUpToDate is true after autoranging"); + assert.deepEqual(scale.domain(), [0, 5], "scale domain was autoranged properly"); + data.push({foo: 100, bar: 200}); + dataSource.data(data); + dataSource._broadcast(); + assert.isFalse(( scale).isAutorangeUpToDate, "isAutorangeUpToDate set to false after modifying data"); + scale.autorangeDomain(); + assert.deepEqual(scale.domain(), [0, 100], "scale domain was autoranged properly"); + }); - it("Linear Scales default to a domain of [Infinity, -Infinity]", () => { - var scale = new Plottable.LinearScale(); - var domain = scale.domain(); - assert.deepEqual(domain, [Infinity, -Infinity]); + it("scale reference counting works as expected", () => { + scale._addPerspective("1x", dataSource, "foo"); + scale._addPerspective("2x", dataSource, "foo"); + scale.autorangeDomain(); + scale._removePerspective("1x"); + scale.autorangeDomain(); + assert.isTrue(( scale).isAutorangeUpToDate, "scale autorange up to date"); + dataSource._broadcast(); + assert.isFalse(( scale).isAutorangeUpToDate, "scale was still listening to dataSource after one perspective deregistered"); + scale._removePerspective("2x"); + scale.autorangeDomain(); + assert.isTrue(( scale).isAutorangeUpToDate, "scale autorange up to date"); + dataSource._broadcast(); + assert.isTrue(( scale).isAutorangeUpToDate, "scale not listening to the dataSource after all perspectives removed"); + }); + + it("scale perspectives can be removed appropriately", () => { + assert.isTrue(scale._autoDomain, "autoDomain enabled1"); + scale._addPerspective("1x", dataSource, "foo"); + scale._addPerspective("2x", dataSource, "bar"); + assert.isTrue(scale._autoDomain, "autoDomain enabled2"); + assert.deepEqual(scale.domain(), [-20, 5], "scale domain includes both perspectives"); + assert.isTrue(scale._autoDomain, "autoDomain enabled3"); + scale._removePerspective("1x"); + assert.isTrue(scale._autoDomain, "autoDomain enabled4"); + assert.deepEqual(scale.domain(), [-20, 1], "only the bar accessor is active"); + scale._addPerspective("2x", dataSource, "foo"); + assert.isTrue(scale._autoDomain, "autoDomain enabled5"); + assert.deepEqual(scale.domain(), [0, 5], "the bar accessor was overwritten"); + }); }); describe("Ordinal Scales", () => { @@ -80,4 +127,73 @@ describe("Scales", () => { }); }); + describe("Color Scales", () => { + it("accepts categorical string types and ordinal domain", () => { + var scale = new Plottable.ColorScale("10"); + scale.domain(["yes", "no", "maybe"]); + assert.equal("#1f77b4", scale.scale("yes")); + assert.equal("#ff7f0e", scale.scale("no")); + assert.equal("#2ca02c", scale.scale("maybe")); + }); + }); + + describe("Interpolated Color Scales", () => { + it("linearly interpolates colors in L*a*b color space", () => { + var scale = new Plottable.InterpolatedColorScale("reds"); + scale.domain([0, 1]); + assert.equal("#b10026", scale.scale(1)); + assert.equal("#d9151f", scale.scale(0.9)); + }); + + it("accepts array types with color hex values", () => { + var scale = new Plottable.InterpolatedColorScale(["#000", "#FFF"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#777777", scale.scale(8)); + }); + + it("accepts array types with color names", () => { + var scale = new Plottable.InterpolatedColorScale(["black", "white"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#777777", scale.scale(8)); + }); + + it("overflow scale values clamp to range", () => { + var scale = new Plottable.InterpolatedColorScale(["black", "white"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#000000", scale.scale(-100)); + assert.equal("#ffffff", scale.scale(100)); + }); + }); + + describe("Ordinal Scales", () => { + it("defaults to \"points\" range type", () => { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + }); + + it("rangeBand returns 0 when in \"points\" mode", () => { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + assert.deepEqual(scale.rangeBand(), 0); + }); + + it("rangeBands are updated when we switch to \"bands\" mode", () => { + var scale = new Plottable.OrdinalScale(); + scale.rangeType("bands"); + assert.deepEqual(scale.rangeType(), "bands"); + scale.range([0, 2679]); + + scale.domain([1,2,3,4]); + assert.deepEqual(scale.rangeBand(), 399); + + scale.domain([1,2,3,4,5]); + assert.deepEqual(scale.rangeBand(), 329); + }); + }); }); diff --git a/test/testUtils.ts b/test/testUtils.ts index 352e9391b6..2380ea1f37 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -50,20 +50,19 @@ function assertWidthHeight(el: D3.Selection, widthExpected, heightExpected, mess assert.equal(height, heightExpected, "height: " + message); } -function makeLinearSeries(n: number): Plottable.IDataset { + +function makeLinearSeries(n: number): {x: number; y:number;}[] { function makePoint(x: number) { return {x: x, y: x}; } - var data = d3.range(n).map(makePoint); - return {data: data, metadata: {cssClass: "linear-series"}}; + return d3.range(n).map(makePoint); } -function makeQuadraticSeries(n: number): Plottable.IDataset { +function makeQuadraticSeries(n: number): {x: number; y:number;}[] { function makeQuadraticPoint(x: number) { return {x: x, y: x*x}; } - var data = d3.range(n).map(makeQuadraticPoint); - return {data: data, metadata: {cssClass: "quadratic-series"}}; + return d3.range(n).map(makeQuadraticPoint); } class MultiTestVerifier { diff --git a/test/tests.html b/test/tests.html index be7066dfdc..dff21a6067 100644 --- a/test/tests.html +++ b/test/tests.html @@ -21,7 +21,7 @@ - +
@@ -33,8 +33,7 @@ mocha.checkLeaks(); if (window.mochaPhantomJS) { mochaPhantomJS.run(); - } - else { + } else { mocha.run(); } diff --git a/test/tests.js b/test/tests.js index 0eec9964ec..d2f0213d6a 100644 --- a/test/tests.js +++ b/test/tests.js @@ -2,17 +2,19 @@ function generateSVG(width, height) { if (typeof width === "undefined") { width = 400; } if (typeof height === "undefined") { height = 400; } - var parent; + var parent = getSVGParent(); + return parent.append("svg").attr("width", width).attr("height", height); +} + +function getSVGParent() { var mocha = d3.select("#mocha-report"); if (mocha.node() != null) { var suites = mocha.selectAll(".suite"); var lastSuite = d3.select(suites[0][suites[0].length - 1]); - parent = lastSuite.selectAll("ul"); + return lastSuite.selectAll("ul"); } else { - parent = d3.select("body"); + return d3.select("body"); } - var svg = parent.append("svg").attr("width", width).attr("height", height); - return svg; } function getTranslate(element) { @@ -53,16 +55,14 @@ function makeLinearSeries(n) { function makePoint(x) { return { x: x, y: x }; } - var data = d3.range(n).map(makePoint); - return { data: data, metadata: { cssClass: "linear-series" } }; + return d3.range(n).map(makePoint); } function makeQuadraticSeries(n) { function makeQuadraticPoint(x) { return { x: x, y: x * x }; } - var data = d3.range(n).map(makeQuadraticPoint); - return { data: data, metadata: { cssClass: "quadratic-series" } }; + return d3.range(n).map(makeQuadraticPoint); } var MultiTestVerifier = (function () { @@ -179,6 +179,68 @@ describe("Axes", function () { /// var assert = chai.assert; +describe("Broadcasters", function () { + var b; + var called; + var cb; + + beforeEach(function () { + b = new Plottable.Broadcaster(); + called = false; + cb = function () { + called = true; + }; + }); + it("listeners are called by the broadcast method", function () { + b.registerListener(null, cb); + b._broadcast(); + assert.isTrue(called, "callback was called"); + }); + + it("same listener can only be associated with one callback", function () { + var called2 = false; + var cb2 = function () { + called2 = true; + }; + var listener = {}; + b.registerListener(listener, cb); + b.registerListener(listener, cb2); + b._broadcast(); + assert.isFalse(called, "first (overwritten) callback not called"); + assert.isTrue(called2, "second callback was called"); + }); + + it("listeners can be deregistered", function () { + var listener = {}; + b.registerListener(listener, cb); + b.deregisterListener(listener); + b._broadcast(); + assert.isFalse(called, "callback was never called"); + }); + + it("arguments are passed through to callback", function () { + var g2 = {}; + var g3 = "foo"; + var cb = function (a1, rest) { + assert.equal(b, a1, "broadcaster passed through"); + assert.equal(g2, rest[0], "arg1 passed through"); + assert.equal(g3, rest[1], "arg2 passed through"); + called = true; + }; + b.registerListener(null, cb); + b._broadcast(g2, g3); + assert.isTrue(called, "the cb was called"); + }); + + it("deregistering an unregistered listener throws an error", function () { + assert.throws(function () { + return b.deregisterListener({}); + }); + }); +}); +/// +var assert = chai.assert; + describe("ComponentGroups", function () { it("components in componentGroups overlap", function () { var c1 = new Plottable.Component().rowMinimum(10).colMinimum(10); @@ -378,6 +440,31 @@ describe("Component behavior", function () { svg.remove(); }); + it("computeLayout works with CSS layouts", function () { + // Manually size parent + var parent = d3.select(svg.node().parentNode); + parent.style("width", "100px").style("height", "200px"); + + // Remove width/height attributes and style with CSS + svg.attr("width", null).attr("height", null); + svg.style("width", "50%").style("height", "50%"); + + c._anchor(svg)._computeLayout(); + assert.equal(c.availableWidth, 50, "computeLayout defaulted width to svg width"); + assert.equal(c.availableHeight, 100, "computeLayout defaulted height to svg height"); + assert.equal(c.xOrigin, 0, "xOrigin defaulted to 0"); + assert.equal(c.yOrigin, 0, "yOrigin defaulted to 0"); + + svg.style("width", "25%").style("height", "25%"); + c._computeLayout(); + assert.equal(c.availableWidth, 25, "computeLayout updated width to new svg width"); + assert.equal(c.availableHeight, 50, "computeLayout updated height to new svg height"); + assert.equal(c.xOrigin, 0, "xOrigin is still 0"); + assert.equal(c.yOrigin, 0, "yOrigin is still 0"); + + svg.remove(); + }); + it("computeLayout will not default when attached to non-root node", function () { var g = svg.append("g"); c._anchor(g); @@ -486,9 +573,8 @@ describe("Component behavior", function () { it("clipPath works as expected", function () { assert.isFalse(c.clipPathEnabled, "clipPathEnabled defaults to false"); c.clipPathEnabled = true; - var expectedClipPathID = Plottable.Component.clipPathId; + var expectedClipPathID = c._plottableID; c._anchor(svg)._computeLayout(0, 0, 100, 100)._render(); - assert.equal(Plottable.Component.clipPathId, expectedClipPathID + 1, "clipPathId incremented"); var expectedClipPathURL = "url(#clipPath" + expectedClipPathID + ")"; assert.equal(c.element.attr("clip-path"), expectedClipPathURL, "the element has clip-path url attached"); var clipRect = c.boxContainer.select(".clip-rect"); @@ -497,6 +583,15 @@ describe("Component behavior", function () { svg.remove(); }); + it("componentID works as expected", function () { + var expectedID = Plottable.PlottableObject.nextID; + var c1 = new Plottable.Component(); + assert.equal(c1._plottableID, expectedID, "component id on next component was as expected"); + var c2 = new Plottable.Component(); + assert.equal(c2._plottableID, expectedID + 1, "future components increment appropriately"); + svg.remove(); + }); + it("boxes work as expected", function () { assert.throws(function () { return c.addBox("pre-anchor"); @@ -621,6 +716,64 @@ describe("Coordinators", function () { /// var assert = chai.assert; +describe("DataSource", function () { + it("Updates listeners when the data is changed", function () { + var ds = new Plottable.DataSource(); + + var newData = [1, 2, 3]; + + var callbackCalled = false; + var callback = function (broadcaster) { + assert.equal(broadcaster, ds, "Callback received the DataSource as the first argument"); + assert.deepEqual(ds.data(), newData, "DataSource arrives with correct data"); + callbackCalled = true; + }; + ds.registerListener(null, callback); + + ds.data(newData); + assert.isTrue(callbackCalled, "callback was called when the data was changed"); + }); + + it("Updates listeners when the metadata is changed", function () { + var ds = new Plottable.DataSource(); + + var newMetadata = "blargh"; + + var callbackCalled = false; + var callback = function (broadcaster) { + assert.equal(broadcaster, ds, "Callback received the DataSource as the first argument"); + assert.deepEqual(ds.metadata(), newMetadata, "DataSource arrives with correct metadata"); + callbackCalled = true; + }; + ds.registerListener(null, callback); + + ds.metadata(newMetadata); + assert.isTrue(callbackCalled, "callback was called when the metadata was changed"); + }); + + it("_getExtent works as expected", function () { + var data = [1, 2, 3, 4, 1]; + var metadata = { foo: 11 }; + var dataSource = new Plottable.DataSource(data, metadata); + var a1 = function (d, i, m) { + return d + i - 2; + }; + assert.deepEqual(dataSource._getExtent(a1), [-1, 5], "extent for numerical data works properly"); + var a2 = function (d, i, m) { + return d + m.foo; + }; + assert.deepEqual(dataSource._getExtent(a2), [12, 15], "extent uses metadata appropriately"); + dataSource.metadata({ foo: -1 }); + assert.deepEqual(dataSource._getExtent(a2), [0, 3], "metadata change is reflected in extent results"); + var a3 = function (d, i, m) { + return "_" + d; + }; + assert.deepEqual(dataSource._getExtent(a3), ["_1", "_2", "_3", "_4"], "extent works properly on string domains (no repeats)"); + }); +}); +/// +var assert = chai.assert; + describe("Gridlines", function () { it("Gridlines and axis tick marks align", function () { var svg = generateSVG(640, 480); @@ -703,8 +856,8 @@ describe("Interactions", function () { it("Pans properly", function () { // The only difference between pan and zoom is internal to d3 // Simulating zoom events is painful, so panning will suffice here - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); + var xScale = new Plottable.LinearScale().domain([0, 11]); + var yScale = new Plottable.LinearScale().domain([11, 0]); var svg = generateSVG(); var dataset = makeLinearSeries(11); @@ -741,8 +894,8 @@ describe("Interactions", function () { var expectedXDragChange = -dragDistancePixelX * getSlope(xScale); var expectedYDragChange = -dragDistancePixelY * getSlope(yScale); - assert.equal(xDomainAfter[0] - xDomainBefore[0], expectedXDragChange, "x domain changed by the correct amount"); - assert.equal(yDomainAfter[0] - yDomainBefore[0], expectedYDragChange, "y domain changed by the correct amount"); + assert.closeTo(xDomainAfter[0] - xDomainBefore[0], expectedXDragChange, 1, "x domain changed by the correct amount"); + assert.closeTo(yDomainAfter[0] - yDomainBefore[0], expectedYDragChange, 1, "y domain changed by the correct amount"); svg.remove(); }); @@ -765,7 +918,7 @@ describe("Interactions", function () { before(function () { svg = generateSVG(svgWidth, svgHeight); - dataset = makeLinearSeries(10); + dataset = new Plottable.DataSource(makeLinearSeries(10)); xScale = new Plottable.LinearScale(); yScale = new Plottable.LinearScale(); renderer = new Plottable.CircleRenderer(dataset, xScale, yScale); @@ -820,99 +973,6 @@ describe("Interactions", function () { }); }); - describe("BrushZoomInteraction", function () { - it("Zooms in correctly on drag", function () { - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); - - var svgWidth = 400; - var svgHeight = 400; - var svg = generateSVG(svgWidth, svgHeight); - var dataset = makeLinearSeries(11); - var renderer = new Plottable.CircleRenderer(dataset, xScale, yScale); - renderer.renderTo(svg); - - var xDomainBefore = xScale.domain(); - var yDomainBefore = yScale.domain(); - - var dragstartX = 10; - var dragstartY = 210; - var dragendX = 190; - var dragendY = 390; - - var expectedXDomain = [xScale.invert(dragstartX), xScale.invert(dragendX)]; - var expectedYDomain = [yScale.invert(dragendY), yScale.invert(dragstartY)]; - - var indicesCallbackCalled = false; - var interaction; - var indicesCallback = function (indices) { - indicesCallbackCalled = true; - interaction.clearBox(); - assert.deepEqual(indices, [1, 2, 3, 4], "the correct points were selected"); - }; - var zoomCallback = new Plottable.ZoomCallbackGenerator().addXScale(xScale).addYScale(yScale).getCallback(); - var callback = function (a) { - var dataArea = renderer.invertXYSelectionArea(a); - var indices = renderer.getDataIndicesFromArea(dataArea); - indicesCallback(indices); - zoomCallback(a); - }; - interaction = new Plottable.AreaInteraction(renderer).callback(callback); - interaction.registerWithComponent(); - - fakeDragSequence(interaction, dragstartX, dragstartY, dragendX, dragendY); - assert.isTrue(indicesCallbackCalled, "indicesCallback was called"); - assert.deepEqual(xScale.domain(), expectedXDomain, "X scale domain was updated correctly"); - assert.deepEqual(yScale.domain(), expectedYDomain, "Y scale domain was updated correclty"); - - svg.remove(); - }); - }); - - describe("CrosshairsInteraction", function () { - it("Crosshairs manifest basic functionality", function () { - var svg = generateSVG(400, 400); - var dp = function (x, y) { - return { x: x, y: y }; - }; - var data = [dp(0, 0), dp(20, 10), dp(40, 40)]; - var dataset = { metadata: { cssClass: "foo" }, data: data }; - var xScale = new Plottable.LinearScale(); - var yScale = new Plottable.LinearScale(); - var circleRenderer = new Plottable.CircleRenderer(dataset, xScale, yScale); - var crosshairs = new Plottable.CrosshairsInteraction(circleRenderer); - crosshairs.registerWithComponent(); - circleRenderer.renderTo(svg); - - var crosshairsG = circleRenderer.foregroundContainer.select(".crosshairs"); - var circle = crosshairsG.select("circle"); - var xLine = crosshairsG.select(".x-line"); - var yLine = crosshairsG.select(".y-line"); - - crosshairs.mousemove(0, 0); - assert.equal(circle.attr("cx"), 0, "the crosshairs are at x=0"); - assert.equal(circle.attr("cy"), 400, "the crosshairs are at y=400"); - assert.equal(xLine.attr("d"), "M 0 400 L 400 400", "the xLine behaves properly at y=400"); - assert.equal(yLine.attr("d"), "M 0 0 L 0 400", "the yLine behaves properly at x=0"); - - crosshairs.mousemove(30, 0); - - // It should stay in the same position - assert.equal(circle.attr("cx"), 0, "the crosshairs are at x=0 still"); - assert.equal(circle.attr("cy"), 400, "the crosshairs are at y=400 still"); - assert.equal(xLine.attr("d"), "M 0 400 L 400 400", "the xLine behaves properly at y=400"); - assert.equal(yLine.attr("d"), "M 0 0 L 0 400", "the yLine behaves properly at x=0"); - - crosshairs.mousemove(300, 0); - assert.equal(circle.attr("cx"), 200, "the crosshairs are at x=200"); - assert.equal(circle.attr("cy"), 300, "the crosshairs are at y=300"); - assert.equal(xLine.attr("d"), "M 0 300 L 400 300", "the xLine behaves properly at y=300"); - assert.equal(yLine.attr("d"), "M 200 0 L 200 400", "the yLine behaves properly at x=200"); - - svg.remove(); - }); - }); - describe("KeyInteraction", function () { it("Triggers the callback only when the Component is moused over and appropriate key is pressed", function () { var svg = generateSVG(400, 400); @@ -1243,45 +1303,37 @@ describe("Renderers", function () { assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); }); - it("Base renderer functionality works", function () { + it("Base Renderer functionality works", function () { var svg = generateSVG(400, 300); - var d1 = { data: ["foo"], metadata: { cssClass: "bar" } }; + var d1 = new Plottable.DataSource(["foo"], { cssClass: "bar" }); var r = new Plottable.Renderer(d1); r._anchor(svg)._computeLayout(); var renderArea = r.content.select(".render-area"); assert.isNotNull(renderArea.node(), "there is a render-area"); - assert.isTrue(r.element.classed("bar"), "the element is classed w/ metadata.cssClass"); - assert.deepEqual(r._data, d1.data, "the data is set properly"); - assert.deepEqual(r._metadata, d1.metadata, "the metadata is set properly"); - var d2 = { data: ["bar"], metadata: { cssClass: "boo" } }; - r.dataset(d2); - assert.isFalse(r.element.classed("bar"), "the element is no longer classed bar"); - assert.isTrue(r.element.classed("boo"), "the element is now classed boo"); - assert.deepEqual(r._data, d2.data, "the data is set properly"); - assert.deepEqual(r._metadata, d2.metadata, "the metadata is set properly"); + + var d2 = new Plottable.DataSource(["bar"], { cssClass: "boo" }); + assert.throws(function () { + return r.dataSource(d2); + }, Error); + svg.remove(); }); - it("rerenderUpdateSelection and requireRerender flags updated appropriately", function () { + it("Renderer automatically generates a DataSource if only data is provided", function () { + var data = ["foo", "bar"]; + var r = new Plottable.Renderer(data); + var dataSource = r.dataSource(); + assert.isNotNull(dataSource, "A DataSource was automatically generated"); + assert.deepEqual(dataSource.data(), data, "The generated DataSource has the correct data"); + }); + + it("Renderer.project works as intended", function () { var r = new Plottable.Renderer(); - var svg = generateSVG(); - r.renderTo(svg); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update"); - assert.isFalse(r._requireRerender, "dont require rerender"); - var metadata = {}; - r.metadata(metadata); - assert.isTrue(r._rerenderUpdateSelection, "rerenderingUpdate req after metadata set"); - assert.isTrue(r._requireRerender, "rerender required when metadata set"); - - r.renderTo(svg); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update after render"); - assert.isFalse(r._requireRerender, "dont require rerender after render"); - - var data = []; - r.data(data); - assert.isFalse(r._rerenderUpdateSelection, "don't need to rerender update after setting data"); - assert.isTrue(r._requireRerender, "rerender required when data set"); - svg.remove(); + var s = new Plottable.LinearScale().domain([0, 1]).range([0, 10]); + r.project("attr", "a", s); + var attrToProjector = r._generateAttrToProjector(); + var projector = attrToProjector["attr"]; + assert.equal(projector({ "a": 0.5 }, 0), 5, "projector works as intended"); }); }); @@ -1290,6 +1342,8 @@ describe("Renderers", function () { var svg = generateSVG(400, 400); var xScale = new Plottable.LinearScale(); var yScale = new Plottable.LinearScale(); + xScale.domain([0, 400]); + yScale.domain([400, 0]); var data = [{ x: 0, y: 0 }, { x: 1, y: 1 }]; var metadata = { foo: 10, bar: 20 }; var xAccessor = function (d, i, m) { @@ -1298,11 +1352,8 @@ describe("Renderers", function () { var yAccessor = function (d, i, m) { return m.bar; }; - var dataset = { data: data, metadata: metadata }; - var renderer = new Plottable.CircleRenderer(dataset, xScale, yScale, xAccessor, yAccessor); - renderer.autorangeDataOnLayout = false; - xScale.domain([0, 400]); - yScale.domain([400, 0]); + var dataSource = new Plottable.DataSource(data, metadata); + var renderer = new Plottable.CircleRenderer(dataSource, xScale, yScale, xAccessor, yAccessor); renderer.renderTo(svg); var circles = renderer.renderArea.selectAll("circle"); var c1 = d3.select(circles[0][0]); @@ -1313,14 +1364,14 @@ describe("Renderers", function () { assert.closeTo(parseFloat(c2.attr("cy")), 20, 0.01, "second circle cy is correct"); data = [{ x: 2, y: 2 }, { x: 4, y: 4 }]; - renderer.data(data).renderTo(svg); + dataSource.data(data); assert.closeTo(parseFloat(c1.attr("cx")), 2, 0.01, "first circle cx is correct after data change"); assert.closeTo(parseFloat(c1.attr("cy")), 20, 0.01, "first circle cy is correct after data change"); assert.closeTo(parseFloat(c2.attr("cx")), 14, 0.01, "second circle cx is correct after data change"); assert.closeTo(parseFloat(c2.attr("cy")), 20, 0.01, "second circle cy is correct after data change"); metadata = { foo: 0, bar: 0 }; - renderer.metadata(metadata).renderTo(svg); + dataSource.metadata(metadata); assert.closeTo(parseFloat(c1.attr("cx")), 2, 0.01, "first circle cx is correct after metadata change"); assert.closeTo(parseFloat(c1.attr("cy")), 0, 0.01, "first circle cy is correct after metadata change"); assert.closeTo(parseFloat(c2.attr("cx")), 4, 0.01, "second circle cx is correct after metadata change"); @@ -1336,14 +1387,14 @@ describe("Renderers", function () { var xScale; var yScale; var lineRenderer; - var simpleDataset = { metadata: { cssClass: "simpleDataset" }, data: [{ foo: 0, bar: 0 }, { foo: 1, bar: 1 }] }; + var simpleDataset = new Plottable.DataSource([{ foo: 0, bar: 0 }, { foo: 1, bar: 1 }]); var renderArea; var verifier = new MultiTestVerifier(); before(function () { svg = generateSVG(500, 500); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([0, 1]); + yScale = new Plottable.LinearScale().domain([0, 1]); var xAccessor = function (d) { return d.foo; }; @@ -1354,7 +1405,7 @@ describe("Renderers", function () { return d3.rgb(d.foo, d.bar, i).toString(); }; lineRenderer = new Plottable.LineRenderer(simpleDataset, xScale, yScale, xAccessor, yAccessor); - lineRenderer.colorAccessor(colorAccessor); + lineRenderer.project("stroke", colorAccessor); lineRenderer.renderTo(svg); renderArea = lineRenderer.renderArea; }); @@ -1443,16 +1494,14 @@ describe("Renderers", function () { before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([0, 9]); + yScale = new Plottable.LinearScale().domain([0, 81]); circleRenderer = new Plottable.CircleRenderer(quadraticDataset, xScale, yScale); - circleRenderer.colorAccessor(colorAccessor); + circleRenderer.project("fill", colorAccessor); circleRenderer.renderTo(svg); }); it("setup is handled properly", function () { - assert.deepEqual(xScale.domain(), [0, 9], "xScale domain was set by the renderer"); - assert.deepEqual(yScale.domain(), [0, 81], "yScale domain was set by the renderer"); assert.deepEqual(xScale.range(), [0, SVG_WIDTH], "xScale range was set by the renderer"); assert.deepEqual(yScale.range(), [SVG_HEIGHT, 0], "yScale range was set by the renderer"); circleRenderer.renderArea.selectAll("circle").each(getCircleRendererVerifier()); @@ -1467,37 +1516,6 @@ describe("Renderers", function () { verifier.end(); }); - it("invertXYSelectionArea works", function () { - var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull); - assert.deepEqual(actualDataAreaFull, dataAreaFull, "the full data area is as expected"); - - var actualDataAreaPart = circleRenderer.invertXYSelectionArea(pixelAreaPart); - - assert.closeTo(actualDataAreaPart.xMin, dataAreaPart.xMin, 1, "partial xMin is close"); - assert.closeTo(actualDataAreaPart.xMax, dataAreaPart.xMax, 1, "partial xMax is close"); - assert.closeTo(actualDataAreaPart.yMin, dataAreaPart.yMin, 1, "partial yMin is close"); - assert.closeTo(actualDataAreaPart.yMax, dataAreaPart.yMax, 1, "partial yMax is close"); - verifier.end(); - }); - - it("getSelectionFromArea works", function () { - var selectionFull = circleRenderer.getSelectionFromArea(dataAreaFull); - assert.lengthOf(selectionFull[0], 10, "all 10 circles were selected by the full region"); - - var selectionPartial = circleRenderer.getSelectionFromArea(dataAreaPart); - assert.lengthOf(selectionPartial[0], 2, "2 circles were selected by the partial region"); - verifier.end(); - }); - - it("getDataIndicesFromArea works", function () { - var indicesFull = circleRenderer.getDataIndicesFromArea(dataAreaFull); - assert.deepEqual(indicesFull, d3.range(10), "all 10 circles were selected by the full region"); - - var indicesPartial = circleRenderer.getDataIndicesFromArea(dataAreaPart); - assert.deepEqual(indicesPartial, [6, 7], "2 circles were selected by the partial region"); - verifier.end(); - }); - describe("after the scale has changed", function () { before(function () { xScale.domain([0, 3]); @@ -1506,37 +1524,6 @@ describe("Renderers", function () { dataAreaPart = { xMin: 1, xMax: 3, yMin: 6, yMax: 3 }; }); - it("invertXYSelectionArea works", function () { - var actualDataAreaFull = circleRenderer.invertXYSelectionArea(pixelAreaFull); - assert.deepEqual(actualDataAreaFull, dataAreaFull, "the full data area is as expected"); - - var actualDataAreaPart = circleRenderer.invertXYSelectionArea(pixelAreaPart); - - assert.closeTo(actualDataAreaPart.xMin, dataAreaPart.xMin, 1, "partial xMin is close"); - assert.closeTo(actualDataAreaPart.xMax, dataAreaPart.xMax, 1, "partial xMax is close"); - assert.closeTo(actualDataAreaPart.yMin, dataAreaPart.yMin, 1, "partial yMin is close"); - assert.closeTo(actualDataAreaPart.yMax, dataAreaPart.yMax, 1, "partial yMax is close"); - verifier.end(); - }); - - it("getSelectionFromArea works", function () { - var selectionFull = circleRenderer.getSelectionFromArea(dataAreaFull); - assert.lengthOf(selectionFull[0], 4, "four circles were selected by the full region"); - - var selectionPartial = circleRenderer.getSelectionFromArea(dataAreaPart); - assert.lengthOf(selectionPartial[0], 1, "one circle was selected by the partial region"); - verifier.end(); - }); - - it("getDataIndicesFromArea works", function () { - var indicesFull = circleRenderer.getDataIndicesFromArea(dataAreaFull); - assert.deepEqual(indicesFull, [0, 1, 2, 3], "four circles were selected by the full region"); - - var indicesPartial = circleRenderer.getDataIndicesFromArea(dataAreaPart); - assert.deepEqual(indicesPartial, [2], "circle 2 was selected by the partial region"); - verifier.end(); - }); - it("the circles re-rendered properly", function () { var renderArea = circleRenderer.renderArea; var circles = renderArea.selectAll("circle"); @@ -1568,16 +1555,14 @@ describe("Renderers", function () { // Choosing data with a negative x value is significant, since there is // a potential failure mode involving the xDomain with an initial // point below 0 - var dataset = { metadata: { cssClass: "sampleBarData" }, data: [d0, d1] }; + var dataset = new Plottable.DataSource([d0, d1]); before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.LinearScale(); - yScale = new Plottable.LinearScale(); + xScale = new Plottable.LinearScale().domain([-2, 4]); + yScale = new Plottable.LinearScale().domain([0, 4]); barRenderer = new Plottable.BarRenderer(dataset, xScale, yScale); barRenderer._anchor(svg)._computeLayout(); - var currentYDomain = yScale.domain(); - yScale.domain([0, currentYDomain[1]]); }); beforeEach(function () { @@ -1636,15 +1621,13 @@ describe("Renderers", function () { before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); - xScale = new Plottable.OrdinalScale(); + xScale = new Plottable.OrdinalScale().domain(["A", "B"]); yScale = new Plottable.LinearScale(); - dataset = { - data: [ - { x: "A", y: 1 }, - { x: "B", y: 2 } - ], - metadata: { cssClass: "letters" } - }; + var data = [ + { x: "A", y: 1 }, + { x: "B", y: 2 } + ]; + dataset = new Plottable.DataSource(data); renderer = new Plottable.CategoryBarRenderer(dataset, xScale, yScale); renderer._animate = false; @@ -1677,6 +1660,82 @@ describe("Renderers", function () { ; }); }); + + describe("Grid Renderer", function () { + var verifier = new MultiTestVerifier(); + var svg; + var xScale; + var yScale; + var colorScale; + var renderer; + + var SVG_WIDTH = 400; + var SVG_HEIGHT = 200; + + before(function () { + svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); + xScale = new Plottable.OrdinalScale(); + yScale = new Plottable.OrdinalScale(); + colorScale = new Plottable.InterpolatedColorScale(["black", "white"]); + var data = [ + { x: "A", y: "U", magnitude: 0 }, + { x: "B", y: "U", magnitude: 2 }, + { x: "A", y: "V", magnitude: 16 }, + { x: "B", y: "V", magnitude: 8 } + ]; + + renderer = new Plottable.GridRenderer(data, xScale, yScale, colorScale, "x", "y", "magnitude"); + renderer.renderTo(svg); + }); + + beforeEach(function () { + verifier.start(); + }); + + it("renders correctly", function () { + var renderArea = renderer.renderArea; + var cells = renderArea.selectAll("rect")[0]; + assert.equal(cells.length, 4); + + var cellAU = d3.select(cells[0]); + var cellBU = d3.select(cells[1]); + var cellAV = d3.select(cells[2]); + var cellBV = d3.select(cells[3]); + + assert.equal(cellAU.attr("height"), "100", "cell 'AU' height is correct"); + assert.equal(cellAU.attr("width"), "200", "cell 'AU' width is correct"); + assert.equal(cellAU.attr("x"), "0", "cell 'AU' x coord is correct"); + assert.equal(cellAU.attr("y"), "0", "cell 'AU' x coord is correct"); + assert.equal(cellAU.attr("fill"), "#000000", "cell 'AU' color is correct"); + + assert.equal(cellBU.attr("height"), "100", "cell 'BU' height is correct"); + assert.equal(cellBU.attr("width"), "200", "cell 'BU' width is correct"); + assert.equal(cellBU.attr("x"), "200", "cell 'BU' x coord is correct"); + assert.equal(cellBU.attr("y"), "0", "cell 'BU' x coord is correct"); + assert.equal(cellBU.attr("fill"), "#212121", "cell 'BU' color is correct"); + + assert.equal(cellAV.attr("height"), "100", "cell 'AV' height is correct"); + assert.equal(cellAV.attr("width"), "200", "cell 'AV' width is correct"); + assert.equal(cellAV.attr("x"), "0", "cell 'AV' x coord is correct"); + assert.equal(cellAV.attr("y"), "100", "cell 'AV' x coord is correct"); + assert.equal(cellAV.attr("fill"), "#ffffff", "cell 'AV' color is correct"); + + assert.equal(cellBV.attr("height"), "100", "cell 'BV' height is correct"); + assert.equal(cellBV.attr("width"), "200", "cell 'BV' width is correct"); + assert.equal(cellBV.attr("x"), "200", "cell 'BV' x coord is correct"); + assert.equal(cellBV.attr("y"), "100", "cell 'BV' x coord is correct"); + assert.equal(cellBV.attr("fill"), "#777777", "cell 'BV' color is correct"); + + verifier.end(); + }); + + after(function () { + if (verifier.passed) { + svg.remove(); + } + ; + }); + }); }); }); /// @@ -1688,11 +1747,11 @@ describe("Scales", function () { return true; }; var scale = new Plottable.Scale(d3.scale.linear()); - scale.registerListener(testCallback); + scale.registerListener(null, testCallback); var scaleCopy = scale.copy(); assert.deepEqual(scale.domain(), scaleCopy.domain(), "Copied scale has the same domain as the original."); assert.deepEqual(scale.range(), scaleCopy.range(), "Copied scale has the same range as the original."); - assert.notDeepEqual(scale._broadcasterCallbacks, scaleCopy._broadcasterCallbacks, "Registered callbacks are not copied over"); + assert.notDeepEqual(scale.listener2Callback, scaleCopy.listener2Callback, "Registered callbacks are not copied over"); }); it("Scale alerts listeners when its domain is updated", function () { @@ -1702,7 +1761,7 @@ describe("Scales", function () { assert.equal(broadcaster, scale, "Callback received the calling scale as the first argument"); callbackWasCalled = true; }; - scale.registerListener(testCallback); + scale.registerListener(null, testCallback); scale.domain([0, 10]); assert.isTrue(callbackWasCalled, "The registered callback was called"); @@ -1715,22 +1774,165 @@ describe("Scales", function () { scale.padDomain(0.2); assert.isTrue(callbackWasCalled, "The registered callback was called when padDomain() is used to set the domain"); }); + describe("autoranging behavior", function () { + var data; + var dataSource; + var scale; + beforeEach(function () { + data = [{ foo: 2, bar: 1 }, { foo: 5, bar: -20 }, { foo: 0, bar: 0 }]; + dataSource = new Plottable.DataSource(data); + scale = new Plottable.LinearScale(); + }); - it("QuantitiveScale.widenDomain() functions correctly", function () { - var scale = new Plottable.QuantitiveScale(d3.scale.linear()); - assert.deepEqual(scale.domain(), [0, 1], "Initial domain is [0, 1]"); - scale.widenDomain([1, 2]); - assert.deepEqual(scale.domain(), [0, 2], "Domain was wided to [0, 2]"); - scale.widenDomain([-1, 1]); - assert.deepEqual(scale.domain(), [-1, 2], "Domain was wided to [-1, 2]"); - scale.widenDomain([0, 1]); - assert.deepEqual(scale.domain(), [-1, 2], "Domain does not get shrink if \"widened\" to a smaller value"); + it("scale autoDomain flag is not overwritten without explicitly setting the domain", function () { + scale._addPerspective("1", dataSource, "foo"); + scale.autorangeDomain().padDomain().nice(); + assert.isTrue(scale._autoDomain, "the autoDomain flag is still set after autoranginging and padding and nice-ing"); + scale.domain([0, 5]); + assert.isFalse(scale._autoDomain, "the autoDomain flag is false after domain explicitly set"); + }); + + it("scale autorange works as expected with single dataSource", function () { + assert.isFalse(scale.isAutorangeUpToDate, "isAutorangeUpToDate is false by default"); + scale._addPerspective("1x", dataSource, "foo"); + assert.isFalse(scale.isAutorangeUpToDate, "isAutorangeUpToDate set to false after adding perspective"); + scale.autorangeDomain(); + assert.isTrue(scale.isAutorangeUpToDate, "isAutorangeUpToDate is true after autoranging"); + assert.deepEqual(scale.domain(), [0, 5], "scale domain was autoranged properly"); + data.push({ foo: 100, bar: 200 }); + dataSource.data(data); + dataSource._broadcast(); + assert.isFalse(scale.isAutorangeUpToDate, "isAutorangeUpToDate set to false after modifying data"); + scale.autorangeDomain(); + assert.deepEqual(scale.domain(), [0, 100], "scale domain was autoranged properly"); + }); + + it("scale reference counting works as expected", function () { + scale._addPerspective("1x", dataSource, "foo"); + scale._addPerspective("2x", dataSource, "foo"); + scale.autorangeDomain(); + scale._removePerspective("1x"); + scale.autorangeDomain(); + assert.isTrue(scale.isAutorangeUpToDate, "scale autorange up to date"); + dataSource._broadcast(); + assert.isFalse(scale.isAutorangeUpToDate, "scale was still listening to dataSource after one perspective deregistered"); + scale._removePerspective("2x"); + scale.autorangeDomain(); + assert.isTrue(scale.isAutorangeUpToDate, "scale autorange up to date"); + dataSource._broadcast(); + assert.isTrue(scale.isAutorangeUpToDate, "scale not listening to the dataSource after all perspectives removed"); + }); + + it("scale perspectives can be removed appropriately", function () { + assert.isTrue(scale._autoDomain, "autoDomain enabled1"); + scale._addPerspective("1x", dataSource, "foo"); + scale._addPerspective("2x", dataSource, "bar"); + assert.isTrue(scale._autoDomain, "autoDomain enabled2"); + assert.deepEqual(scale.domain(), [-20, 5], "scale domain includes both perspectives"); + assert.isTrue(scale._autoDomain, "autoDomain enabled3"); + scale._removePerspective("1x"); + assert.isTrue(scale._autoDomain, "autoDomain enabled4"); + assert.deepEqual(scale.domain(), [-20, 1], "only the bar accessor is active"); + scale._addPerspective("2x", dataSource, "foo"); + assert.isTrue(scale._autoDomain, "autoDomain enabled5"); + assert.deepEqual(scale.domain(), [0, 5], "the bar accessor was overwritten"); + }); }); - it("Linear Scales default to a domain of [Infinity, -Infinity]", function () { - var scale = new Plottable.LinearScale(); - var domain = scale.domain(); - assert.deepEqual(domain, [Infinity, -Infinity]); + describe("Ordinal Scales", function () { + it("defaults to \"points\" range type", function () { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + }); + + it("rangeBand returns 0 when in \"points\" mode", function () { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + assert.deepEqual(scale.rangeBand(), 0); + }); + + it("rangeBands are updated when we switch to \"bands\" mode", function () { + var scale = new Plottable.OrdinalScale(); + scale.rangeType("bands"); + assert.deepEqual(scale.rangeType(), "bands"); + scale.range([0, 2679]); + + scale.domain([1, 2, 3, 4]); + assert.deepEqual(scale.rangeBand(), 399); + + scale.domain([1, 2, 3, 4, 5]); + assert.deepEqual(scale.rangeBand(), 329); + }); + }); + + describe("Color Scales", function () { + it("accepts categorical string types and ordinal domain", function () { + var scale = new Plottable.ColorScale("10"); + scale.domain(["yes", "no", "maybe"]); + assert.equal("#1f77b4", scale.scale("yes")); + assert.equal("#ff7f0e", scale.scale("no")); + assert.equal("#2ca02c", scale.scale("maybe")); + }); + }); + + describe("Interpolated Color Scales", function () { + it("linearly interpolates colors in L*a*b color space", function () { + var scale = new Plottable.InterpolatedColorScale("reds"); + scale.domain([0, 1]); + assert.equal("#b10026", scale.scale(1)); + assert.equal("#d9151f", scale.scale(0.9)); + }); + + it("accepts array types with color hex values", function () { + var scale = new Plottable.InterpolatedColorScale(["#000", "#FFF"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#777777", scale.scale(8)); + }); + + it("accepts array types with color names", function () { + var scale = new Plottable.InterpolatedColorScale(["black", "white"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#777777", scale.scale(8)); + }); + + it("overflow scale values clamp to range", function () { + var scale = new Plottable.InterpolatedColorScale(["black", "white"]); + scale.domain([0, 16]); + assert.equal("#000000", scale.scale(0)); + assert.equal("#ffffff", scale.scale(16)); + assert.equal("#000000", scale.scale(-100)); + assert.equal("#ffffff", scale.scale(100)); + }); + }); + + describe("Ordinal Scales", function () { + it("defaults to \"points\" range type", function () { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + }); + + it("rangeBand returns 0 when in \"points\" mode", function () { + var scale = new Plottable.OrdinalScale(); + assert.deepEqual(scale.rangeType(), "points"); + assert.deepEqual(scale.rangeBand(), 0); + }); + + it("rangeBands are updated when we switch to \"bands\" mode", function () { + var scale = new Plottable.OrdinalScale(); + scale.rangeType("bands"); + assert.deepEqual(scale.rangeType(), "bands"); + scale.range([0, 2679]); + + scale.domain([1, 2, 3, 4]); + assert.deepEqual(scale.rangeBand(), 399); + + scale.domain([1, 2, 3, 4, 5]); + assert.deepEqual(scale.rangeBand(), 329); + }); }); }); /// @@ -2043,4 +2245,89 @@ describe("Utils", function () { assert.equal(textEl.text(), " ", "getTextHeight did not modify the text in the element"); svg.remove(); }); + + it("accessorize works properly", function () { + var datum = { "foo": 2, "bar": 3, "key": 4 }; + + var f = function (d, i, m) { + return d + i; + }; + var a1 = Plottable.Utils.accessorize(f); + assert.equal(f, a1, "function passes through accessorize unchanged"); + + var a2 = Plottable.Utils.accessorize("key"); + assert.equal(a2(datum, 0, null), 4, "key accessor works appropriately"); + + var a3 = Plottable.Utils.accessorize("#aaaa"); + assert.equal(a3(datum, 0, null), "#aaaa", "strings beginning with # are returned as final value"); + + var a4 = Plottable.Utils.accessorize(33); + assert.equal(a4(datum, 0, null), 33, "numbers are return as final value"); + + var a5 = Plottable.Utils.accessorize(datum); + assert.equal(a5(datum, 0, null), datum, "objects are return as final value"); + }); + + it("StrictEqualityAssociativeArray works as expected", function () { + var s = new Plottable.Utils.StrictEqualityAssociativeArray(); + var o1 = {}; + var o2 = {}; + assert.isFalse(s.has(o1)); + assert.isFalse(s.delete(o1)); + assert.isUndefined(s.get(o1)); + assert.isFalse(s.set(o1, "foo")); + assert.equal(s.get(o1), "foo"); + assert.isTrue(s.set(o1, "bar")); + assert.equal(s.get(o1), "bar"); + s.set(o2, "baz"); + s.set(3, "bam"); + s.set("3", "ball"); + assert.equal(s.get(o1), "bar"); + assert.equal(s.get(o2), "baz"); + assert.equal(s.get(3), "bam"); + assert.equal(s.get("3"), "ball"); + assert.isTrue(s.delete(3)); + assert.isUndefined(s.get(3)); + assert.equal(s.get(o2), "baz"); + assert.equal(s.get("3"), "ball"); + }); + + it("uniq works as expected", function () { + var strings = ["foo", "bar", "foo", "foo", "baz", "bam"]; + assert.deepEqual(Plottable.Utils.uniq(strings), ["foo", "bar", "baz", "bam"]); + }); + + it("IDCounter works as expected", function () { + var i = new Plottable.Utils.IDCounter(); + assert.equal(i.get("f"), 0); + assert.equal(i.increment("f"), 1); + assert.equal(i.increment("g"), 1); + assert.equal(i.increment("f"), 2); + assert.equal(i.decrement("f"), 1); + assert.equal(i.get("f"), 1); + assert.equal(i.get("f"), 1); + assert.equal(i.decrement(2), -1); + }); + + it("can get a plain element's size", function () { + var parent = getSVGParent(); + parent.style("width", "300px"); + parent.style("height", "200px"); + var parentElem = parent[0][0]; + + var width = Plottable.Utils.getElementWidth(parentElem); + assert.equal(width, 300, "measured width matches set width"); + var height = Plottable.Utils.getElementHeight(parentElem); + assert.equal(height, 200, "measured height matches set height"); + }); + + it("can get the svg's size", function () { + var svg = generateSVG(450, 120); + var svgElem = svg[0][0]; + + var width = Plottable.Utils.getElementWidth(svgElem); + assert.equal(width, 450, "measured width matches set width"); + var height = Plottable.Utils.getElementHeight(svgElem); + assert.equal(height, 120, "measured height matches set height"); + }); }); diff --git a/test/utilsTests.ts b/test/utilsTests.ts index 031789e108..ab3c5f50b5 100644 --- a/test/utilsTests.ts +++ b/test/utilsTests.ts @@ -61,6 +61,67 @@ describe("Utils", () => { svg.remove(); }); + it("accessorize works properly", () => { + var datum = {"foo": 2, "bar": 3, "key": 4}; + + var f = (d: any, i: number, m: any) => d + i; + var a1 = Plottable.Utils.accessorize(f); + assert.equal(f, a1, "function passes through accessorize unchanged"); + + var a2 = Plottable.Utils.accessorize("key"); + assert.equal(a2(datum, 0, null), 4, "key accessor works appropriately"); + + var a3 = Plottable.Utils.accessorize("#aaaa"); + assert.equal(a3(datum, 0, null), "#aaaa", "strings beginning with # are returned as final value"); + + var a4 = Plottable.Utils.accessorize(33); + assert.equal(a4(datum, 0, null), 33, "numbers are return as final value"); + + var a5 = Plottable.Utils.accessorize(datum); + assert.equal(a5(datum, 0, null), datum, "objects are return as final value"); + }); + + it("StrictEqualityAssociativeArray works as expected", () => { + var s = new Plottable.Utils.StrictEqualityAssociativeArray(); + var o1 = {}; + var o2 = {}; + assert.isFalse(s.has(o1)); + assert.isFalse(s.delete(o1)); + assert.isUndefined(s.get(o1)); + assert.isFalse(s.set(o1, "foo")); + assert.equal(s.get(o1), "foo"); + assert.isTrue(s.set(o1, "bar")); + assert.equal(s.get(o1), "bar"); + s.set(o2, "baz"); + s.set(3, "bam"); + s.set("3", "ball"); + assert.equal(s.get(o1), "bar"); + assert.equal(s.get(o2), "baz"); + assert.equal(s.get(3), "bam"); + assert.equal(s.get("3"), "ball"); + assert.isTrue(s.delete(3)); + assert.isUndefined(s.get(3)); + assert.equal(s.get(o2), "baz"); + assert.equal(s.get("3"), "ball"); + }); + + it("uniq works as expected", () => { + var strings = ["foo", "bar", "foo", "foo", "baz", "bam"]; + assert.deepEqual(Plottable.Utils.uniq(strings), ["foo", "bar", "baz", "bam"]); + }); + + it("IDCounter works as expected", () => { + var i = new Plottable.Utils.IDCounter(); + assert.equal(i.get("f"), 0); + assert.equal(i.increment("f"), 1); + assert.equal(i.increment("g"), 1); + assert.equal(i.increment("f"), 2); + assert.equal(i.decrement("f"), 1); + assert.equal(i.get("f"), 1); + assert.equal(i.get("f"), 1); + assert.equal(i.decrement(2), -1); + }); + it("can get a plain element's size", () => { var parent = getSVGParent(); parent.style("width", "300px"); @@ -83,4 +144,3 @@ describe("Utils", () => { assert.equal(height, 120, "measured height matches set height"); }); }); - diff --git a/tslint.json b/tslint.json index 461cfb1bf5..5da961c424 100644 --- a/tslint.json +++ b/tslint.json @@ -1,6 +1,6 @@ { "rules": { - "class-name": true, + "class-name": false, "curly": true, "eofline": true, "forin": true, diff --git a/typings/d3/d3.d.ts b/typings/d3/d3.d.ts index dc1bdbed71..1259223861 100644 --- a/typings/d3/d3.d.ts +++ b/typings/d3/d3.d.ts @@ -242,7 +242,7 @@ declare module D3 { * * @param map Array of objects to get the values from */ - values(map: any[]): any[]; + values(map: any): any[]; /** * List the key-value entries of an associative array. *