diff --git a/plottable.d.ts b/plottable.d.ts index 49875cbb87..f38fdfb02d 100644 --- a/plottable.d.ts +++ b/plottable.d.ts @@ -2,16 +2,71 @@ declare module Plottable { module Util { module Methods { + /** + * Checks if x is between a and b. + * + * @param {number} x The value to test if in range + * @param {number} a The beginning of the (inclusive) range + * @param {number} b The ending of the (inclusive) range + * @return {boolean} Whether x is in [a, b] + */ function inRange(x: number, a: number, b: number): boolean; + /** Print a warning message to the console, if it is available. + * + * @param {string} The warnings to print + */ function warn(warning: string): void; + /** + * Takes two arrays of numbers and adds them together + * + * @param {number[]} alist The first array of numbers + * @param {number[]} blist The second array of numbers + * @return {number[]} An array of numbers where x[i] = alist[i] + blist[i] + */ function addArrays(alist: number[], blist: number[]): number[]; + /** + * Takes two sets and returns the intersection + * + * @param {D3.Set} set1 The first set + * @param {D3.Set} set2 The second set + * @return {D3.Set} A set that contains elements that appear in both set1 and set2 + */ function intersection(set1: D3.Set, set2: D3.Set): D3.Set; + /** + * Takes two sets and returns the union + * + * @param{D3.Set} set1 The first set + * @param{D3.Set} set2 The second set + * @return{D3.Set} A set that contains elements that appear in either set1 or set2 + */ function union(set1: D3.Set, set2: D3.Set): D3.Set; function uniq(strings: string[]): string[]; function uniqNumbers(a: number[]): number[]; + /** + * Creates an array of length `count`, filled with value or (if value is a function), value() + * + * @param {any} value The value to fill the array with, or, if a function, a generator for values + * @param {number} count The length of the array to generate + * @return {any[]} + */ function createFilledArray(value: any, count: number): any[]; + /** + * @param {T[][]} a The 2D array that will have its elements joined together. + * @return {T[]} Every array in a, concatenated together in the order they appear. + */ function flatten(a: T[][]): T[]; + /** + * Check if two arrays are equal by strict equality. + */ function arrayEq(a: T[], b: T[]): boolean; + /** + * @param {any} a Object to check against b for equality. + * @param {any} b Object to check against a for equality. + * + * @returns {boolean} whether or not two objects share the same keys, and + * values associated with those keys. Values will be compared + * with ===. + */ function objEq(a: any, b: any): boolean; } } @@ -21,6 +76,42 @@ declare module Plottable { declare module Plottable { module Util { module OpenSource { + /** + * Returns the sortedIndex for inserting a value into an array. + * Takes a number and an array of numbers OR an array of objects and an accessor that returns a number. + * @param {number} value: The numerical value to insert + * @param {any[]} arr: Array to find insertion index, can be number[] or any[] (if accessor provided) + * @param {IAccessor} accessor: If provided, this function is called on members of arr to determine insertion index + * @returns {number} The insertion index. + * The behavior is undefined for arrays that are unsorted + * If there are multiple valid insertion indices that maintain sorted order (e.g. addign 1 to [1,1,1,1,1]) then + * the behavior must satisfy that the array is sorted post-insertion, but is otherwise unspecified. + * This is a modified version of Underscore.js's implementation of sortedIndex. + * Underscore.js is released under the MIT License: + * Copyright (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative + * Reporters & Editors + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ function sortedIndex(val: number, arr: number[]): number; function sortedIndex(val: number, arr: any[], accessor: IAccessor): number; } @@ -31,9 +122,9 @@ declare module Plottable { declare module Plottable { module Util { class IDCounter { - increment(id: any): number; - decrement(id: any): number; - get(id: any): number; + public increment(id: any): number; + public decrement(id: any): number; + public get(id: any): number; } } } @@ -41,14 +132,63 @@ declare module Plottable { declare module Plottable { module Util { + /** + * 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(key: any, value: any): boolean; - get(key: any): any; - has(key: any): boolean; - values(): any[]; - keys(): any[]; - map(cb: (key?: any, val?: any, index?: number) => any): any[]; - delete(key: any): boolean; + /** + * Set a new key/value pair in the store. + * + * @param {any} key Key to set in the store + * @param {any} value Value to set in the store + * @return {boolean} True if key already in store, false otherwise + */ + public set(key: any, value: any): boolean; + /** + * Get a value from the store, given a key. + * + * @param {any} key Key associated with value to retrieve + * @return {any} Value if found, undefined otherwise + */ + public get(key: any): any; + /** + * Test whether store has a value associated with given key. + * + * Will return true if there is a key/value entry, + * even if the value is explicitly `undefined`. + * + * @param {any} key Key to test for presence of an entry + * @return {boolean} Whether there was a matching entry for that key + */ + public has(key: any): boolean; + /** + * Return an array of the values in the key-value store + * + * @return {any[]} The values in the store + */ + public values(): any[]; + /** + * Return an array of keys in the key-value store + * + * @return {any[]} The keys in the store + */ + public keys(): any[]; + /** + * Execute a callback for each entry in the array. + * + * @param {(key: any, val?: any, index?: number) => any} callback The callback to eecute + * @return {any[]} The results of mapping the callback over the entries + */ + public map(cb: (key?: any, val?: any, index?: number) => any): any[]; + /** + * Delete a key from the key-value store. Return whether the key was present. + * + * @param {any} The key to remove + * @return {boolean} Whether a matching entry was found and removed + */ + public delete(key: any): boolean; } } } @@ -57,9 +197,36 @@ declare module Plottable { declare module Plottable { module Util { class Cache { + /** + * @constructor + * + * @param {string} compute The function whose results will be cached. + * @param {string} [canonicalKey] If present, when clear() is called, + * this key will be re-computed. If its result hasn't been changed, + * the cache will not be cleared. + * @param {(v: T, w: T) => boolean} [valueEq] + * Used to determine if the value of canonicalKey has changed. + * If omitted, defaults to === comparision. + */ constructor(compute: (k: string) => T, canonicalKey?: string, valueEq?: (v: T, w: T) => boolean); - get(k: string): T; - clear(): Cache; + /** + * Attempt to look up k in the cache, computing the result if it isn't + * found. + * + * @param {string} k The key to look up in the cache. + * @return {T} The value associated with k; the result of compute(k). + */ + public get(k: string): T; + /** + * Reset the cache empty. + * + * If canonicalKey was provided at construction, compute(canonicalKey) + * will be re-run. If the result matches what is already in the cache, + * it will not clear the cache. + * + * @return {Cache} The calling Cache. + */ + public clear(): Cache; } } } @@ -76,12 +243,45 @@ declare module Plottable { interface TextMeasurer { (s: string): Dimensions; } + /** + * Returns a quasi-pure function of typesignature (t: string) => Dimensions which measures height and width of text + * in the given text selection + * + * @param {D3.Selection} selection: A temporary text selection that the string will be placed into for measurement. + * Will be removed on function creation and appended only for measurement. + * @returns {Dimensions} width and height of the text + */ function getTextMeasurer(selection: D3.Selection): TextMeasurer; + /** + * This class will measure text by measuring each character individually, + * then adding up the dimensions. It will also cache the dimensions of each + * letter. + */ class CachingCharacterMeasurer { - measure: TextMeasurer; + /** + * @param {string} s The string to be measured. + * @return {Dimensions} The width and height of the measured text. + */ + public measure: TextMeasurer; + /** + * @param {D3.Selection} textSelection The element that will have text inserted into + * it in order to measure text. The styles present for text in + * this element will to the text being measured. + */ constructor(textSelection: D3.Selection); - clear(): CachingCharacterMeasurer; + /** + * Clear the cache, if it seems that the text has changed size. + */ + public clear(): CachingCharacterMeasurer; } + /** + * Gets a truncated version of a sting that fits in the available space, given the element in which to draw the text + * + * @param {string} text: The string to be truncated + * @param {number} availableWidth: The available width, in pixels + * @param {D3.Selection} element: The text element used to measure the text + * @returns {string} text - the shortened text + */ function getTruncatedText(text: string, availableWidth: number, measurer: TextMeasurer): string; function writeLineHorizontally(line: string, g: D3.Selection, width: number, height: number, xAlign?: string, yAlign?: string): { width: number; @@ -101,6 +301,12 @@ declare module Plottable { xAlign: string; yAlign: string; } + /** + * @param {write} [IWriteOptions] If supplied, the text will be written + * To the given g. Will align the text vertically if it seems like + * that is appropriate. + * Returns an IWriteTextResult with info on whether the text fit, and how much width/height was used. + */ function writeText(text: string, width: number, height: number, tm: TextMeasurer, horizontally?: boolean, write?: IWriteOptions): IWriteTextResult; } } @@ -115,7 +321,16 @@ declare module Plottable { lines: string[]; textFits: boolean; } + /** + * Takes a block of text, a width and height to fit it in, and a 2-d text measurement function. + * Wraps words and fits as much of the text as possible into the given width and height. + */ function breakTextToFitRect(text: string, width: number, height: number, measureText: Text.TextMeasurer): IWrappedText; + /** + * Determines if it is possible to fit a given text within width without breaking any of the words. + * Simple algorithm, split the text up into tokens, and make sure that the widest token doesn't exceed + * allowed width. + */ function canWrapWithoutBreakingWords(text: string, width: number, widthMeasure: (s: string) => number): boolean; } } @@ -124,6 +339,11 @@ declare module Plottable { declare module Plottable { module Util { module DOM { + /** + * Gets the bounding box of an element. + * @param {D3.Selection} element + * @returns {SVGRed} The bounding box. + */ function getBBox(element: D3.Selection): SVGRect; var POLYFILL_TIMEOUT_MSEC: number; function requestAnimationFramePolyfill(fn: () => any): void; @@ -144,13 +364,76 @@ declare module Plottable { } var MILLISECONDS_IN_ONE_DAY: number; class Formatters { + /** + * Creates a formatter for currency values. + * + * @param {number} [precision] The number of decimal places to show (default 2). + * @param {string} [symbol] The currency symbol to use (default "$"). + * @param {boolean} [prefix] Whether to prepend or append the currency symbol (default true). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for currency values. + */ static currency(precision?: number, symbol?: string, prefix?: boolean, onlyShowUnchanged?: boolean): (d: any) => string; + /** + * Creates a formatter that displays exactly [precision] decimal places. + * + * @param {number} [precision] The number of decimal places to show (default 3). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter that displays exactly [precision] decimal places. + */ static fixed(precision?: number, onlyShowUnchanged?: boolean): (d: any) => string; + /** + * Creates a formatter that formats numbers to show no more than + * [precision] decimal places. All other values are stringified. + * + * @param {number} [precision] The number of decimal places to show (default 3). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for general values. + */ static general(precision?: number, onlyShowUnchanged?: boolean): (d: any) => string; + /** + * Creates a formatter that stringifies its input. + * + * @returns {Formatter} A formatter that stringifies its input. + */ static identity(): (d: any) => string; + /** + * Creates a formatter for percentage values. + * Multiplies the input by 100 and appends "%". + * + * @param {number} [precision] The number of decimal places to show (default 0). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for percentage values. + */ static percentage(precision?: number, onlyShowUnchanged?: boolean): (d: any) => string; + /** + * Creates a formatter for values that displays [precision] significant figures + * and puts SI notation. + * + * @param {number} [precision] The number of significant figures to show (default 3). + * + * @returns {Formatter} A formatter for SI values. + */ static siSuffix(precision?: number): (d: any) => string; + /** + * Creates a formatter that displays dates. + * + * @returns {Formatter} A formatter for time/date values. + */ static time(): (d: any) => string; + /** + * Creates a formatter for relative dates. + * + * @param {number} baseValue The start date (as epoch time) used in computing relative dates (default 0) + * @param {number} increment The unit used in calculating relative date values (default MILLISECONDS_IN_ONE_DAY) + * @param {string} label The label to append to the formatted string (default "") + * + * @returns {Formatter} A formatter for time/date values. + */ static relativeDate(baseValue?: number, increment?: number, label?: string): (d: any) => string; } } @@ -190,32 +473,119 @@ declare module Plottable { declare module Plottable { module Core { + /** + * This interface represents anything in Plottable which can have a listener attached. + * Listeners attach by referencing the Listenable's broadcaster, and calling registerListener + * on it. + * + * e.g.: + * listenable: Plottable.IListenable; + * listenable.broadcaster.registerListener(callbackToCallOnBroadcast) + */ interface IListenable { broadcaster: Broadcaster; } + /** + * This interface represents the callback that should be passed to the Broadcaster on a Listenable. + * + * The callback will be called with the attached Listenable as the first object, and optional arguments + * as the subsequent arguments. + * + * The Listenable is passed as the first argument so that it is easy for the callback to reference the + * current state of the Listenable in the resolution logic. + */ interface IBroadcasterCallback { (listenable: IListenable, ...args: any[]): any; } - class Broadcaster extends Plottable.Abstract.PlottableObject { - listenable: IListenable; + /** + * The Broadcaster class is owned by an IListenable. Third parties can register and deregister listeners + * from the broadcaster. When the broadcaster.broadcast method is activated, all registered callbacks are + * called. The registered callbacks are called with the registered Listenable that the broadcaster is attached + * to, along with optional arguments passed to the `broadcast` method. + * + * The listeners are called synchronously. + */ + class Broadcaster extends Abstract.PlottableObject { + public listenable: IListenable; + /** + * Construct a broadcaster, taking the Listenable that the broadcaster will be attached to. + * + * @constructor + * @param {IListenable} listenable The Listenable-object that this broadcaster is attached to. + */ constructor(listenable: IListenable); - registerListener(key: any, callback: IBroadcasterCallback): Broadcaster; - broadcast(...args: any[]): Broadcaster; - deregisterListener(key: any): Broadcaster; - deregisterAllListeners(): void; - } - } -} - - -declare module Plottable { - class DataSource extends Plottable.Abstract.PlottableObject implements Plottable.Core.IListenable { - broadcaster: any; + /** + * Registers a callback to be called when the broadcast method is called. Also takes a key which + * is used to support deregistering the same callback later, by passing in the same key. + * If there is already a callback associated with that key, then the callback will be replaced. + * + * @param key The key associated with the callback. Key uniqueness is determined by deep equality. + * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. + * @returns {Broadcaster} this object + */ + public registerListener(key: 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 + */ + public broadcast(...args: any[]): Broadcaster; + /** + * Deregisters the callback associated with a key. + * + * @param key The key to deregister. + * @returns {Broadcaster} this object + */ + public deregisterListener(key: any): Broadcaster; + /** + * Deregisters all listeners and callbacks associated with the broadcaster. + * + * @returns {Broadcaster} this object + */ + public deregisterAllListeners(): void; + } + } +} + + +declare module Plottable { + class DataSource extends Abstract.PlottableObject implements Core.IListenable { + public broadcaster: Core.Broadcaster; + /** + * Creates a new DataSource. + * + * @constructor + * @param {any[]} data + * @param {any} metadata An object containing additional information. + */ constructor(data?: any[], metadata?: any); - data(): any[]; - data(data: any[]): DataSource; - metadata(): any; - metadata(metadata: any): DataSource; + /** + * Gets the data. + * + * @returns {any[]} The current data. + */ + public data(): any[]; + /** + * Sets new data. + * + * @param {any[]} data The new data. + * @returns {DataSource} The calling DataSource. + */ + public data(data: any[]): DataSource; + /** + * Gets the metadata. + * + * @returns {any} The current metadata. + */ + public metadata(): any; + /** + * Sets the metadata. + * + * @param {any} metadata The new metadata. + * @returns {DataSource} The calling DataSource. + */ + public metadata(metadata: any): DataSource; } } @@ -223,29 +593,107 @@ declare module Plottable { declare module Plottable { module Abstract { class Component extends PlottableObject { - element: D3.Selection; - content: D3.Selection; - backgroundContainer: D3.Selection; - foregroundContainer: D3.Selection; - clipPathEnabled: boolean; - availableWidth: number; - availableHeight: number; - xOrigin: number; - yOrigin: number; + public element: D3.Selection; + public content: D3.Selection; + public backgroundContainer: D3.Selection; + public foregroundContainer: D3.Selection; + public clipPathEnabled: boolean; + public availableWidth: number; + public availableHeight: number; + public xOrigin: number; + public yOrigin: number; static AUTORESIZE_BY_DEFAULT: boolean; - renderTo(element: any): Component; - resize(width?: number, height?: number): Component; - autoResize(flag: boolean): Component; - xAlign(alignment: string): Component; - yAlign(alignment: string): Component; - xOffset(offset: number): Component; - yOffset(offset: number): Component; - registerInteraction(interaction: Interaction): Component; - classed(cssClass: string): boolean; - classed(cssClass: string, addClass: boolean): Component; - merge(c: Component): Plottable.Component.Group; - detach(): Component; - remove(): void; + /** + * Renders the Component into a given DOM element. + * + * @param {String|D3.Selection} element A D3 selection or a selector for getting the element to render into. + * @return {Component} The calling component. + */ + public renderTo(element: any): Component; + /** + * Cause the Component to recompute layout and redraw. If passed arguments, will resize the root SVG it lives in. + * + * @param {number} [availableWidth] - the width of the container element + * @param {number} [availableHeight] - the height of the container element + */ + public resize(width?: number, height?: number): Component; + /** + * Enables and disables auto-resize. + * + * If enabled, window resizes will enqueue this component for a re-layout + * and re-render. Animations are disabled during window resizes when auto- + * resize is enabled. + * + * @param {boolean} flag - Enables (true) or disables (false) auto-resize. + */ + public autoResize(flag: boolean): Component; + /** + * Sets the x alignment of the Component. + * + * @param {string} alignment The x alignment of the Component (one of LEFT/CENTER/RIGHT). + * @returns {Component} The calling Component. + */ + public xAlign(alignment: string): Component; + /** + * Sets the y alignment of the Component. + * + * @param {string} alignment The y alignment of the Component (one of TOP/CENTER/BOTTOM). + * @returns {Component} The calling Component. + */ + public yAlign(alignment: string): Component; + /** + * Sets the x offset of the Component. + * + * @param {number} offset The desired x offset, in pixels. + * @returns {Component} The calling Component. + */ + public xOffset(offset: number): Component; + /** + * Sets the y offset of the Component. + * + * @param {number} offset The desired y offset, in pixels. + * @returns {Component} The calling Component. + */ + public yOffset(offset: number): Component; + /** + * Attaches an Interaction to the Component, so that the Interaction will listen for events on the Component. + * + * @param {Interaction} interaction The Interaction to attach to the Component. + * @return {Component} The calling Component. + */ + public registerInteraction(interaction: Interaction): Component; + /** + * Adds/removes a given CSS class to/from the Component, or checks if the Component has a particular CSS class. + * + * @param {string} cssClass The CSS class to add/remove/check for. + * @param {boolean} [addClass] Whether to add or remove the CSS class. If not supplied, checks for the CSS class. + * @return {boolean|Component} Whether the Component has the given CSS class, or the calling Component (if addClass is supplied). + */ + public classed(cssClass: string): boolean; + public classed(cssClass: string, addClass: boolean): Component; + /** + * Merges this Component with another Component, returning a ComponentGroup. + * There are four cases: + * Component + Component: Returns a ComponentGroup with both components inside it. + * ComponentGroup + Component: Returns the ComponentGroup with the Component appended. + * Component + ComponentGroup: Returns the ComponentGroup with the Component prepended. + * ComponentGroup + ComponentGroup: Returns a new ComponentGroup with two ComponentGroups inside it. + * + * @param {Component} c The component to merge in. + * @return {ComponentGroup} + */ + public merge(c: Component): Component.Group; + /** + * Detaches a Component from the DOM. The component can be reused. + * + * @returns The calling Component. + */ + public detach(): Component; + /** + * Removes a Component from the DOM and disconnects it from everything it's + * listening to (effectively destroying it). + */ + public remove(): void; } } } @@ -254,10 +702,26 @@ declare module Plottable { declare module Plottable { module Abstract { class ComponentContainer extends Component { - components(): Component[]; - empty(): boolean; - detachAll(): ComponentContainer; - remove(): void; + /** + * Returns a list of components in the ComponentContainer + * + * @returns{Component[]} the contained Components + */ + public components(): Component[]; + /** + * Returns true iff the ComponentContainer is empty. + * + * @returns {boolean} Whether the calling ComponentContainer is empty. + */ + public empty(): boolean; + /** + * Detaches all components contained in the ComponentContainer, and + * empties the ComponentContainer. + * + * @returns {ComponentContainer} The calling ComponentContainer + */ + public detachAll(): ComponentContainer; + public remove(): void; } } } @@ -265,9 +729,15 @@ declare module Plottable { declare module Plottable { module Component { - class Group extends Plottable.Abstract.ComponentContainer { - constructor(components?: Plottable.Abstract.Component[]); - merge(c: Plottable.Abstract.Component): Group; + class Group extends Abstract.ComponentContainer { + /** + * Creates a ComponentGroup. + * + * @constructor + * @param {Component[]} [components] The Components in the Group. + */ + constructor(components?: Abstract.Component[]); + public merge(c: Abstract.Component): Group; } } } @@ -283,12 +753,49 @@ declare module Plottable { wantsWidth: boolean; wantsHeight: boolean; } - class Table extends Plottable.Abstract.ComponentContainer { - constructor(rows?: Plottable.Abstract.Component[][]); - addComponent(row: number, col: number, component: Plottable.Abstract.Component): Table; - padding(rowPadding: number, colPadding: number): Table; - rowWeight(index: number, weight: number): Table; - colWeight(index: number, weight: number): Table; + class Table extends Abstract.ComponentContainer { + /** + * Creates a Table. + * + * @constructor + * @param {Component[][]} [rows] A 2-D array of the Components to place in the table. + * null can be used if a cell is empty. + */ + constructor(rows?: Abstract.Component[][]); + /** + * Adds a Component in the specified cell. + * + * @param {number} row The row in which to add the Component. + * @param {number} col The column in which to add the Component. + * @param {Component} component The Component to be added. + */ + public addComponent(row: number, col: number, component: Abstract.Component): Table; + /** + * Sets the row and column padding on the Table. + * + * @param {number} rowPadding The padding above and below each row, in pixels. + * @param {number} colPadding the padding to the left and right of each column, in pixels. + * @returns {Table} The calling Table. + */ + public padding(rowPadding: number, colPadding: number): Table; + /** + * Sets the layout weight of a particular row. + * Space is allocated to rows based on their weight. Rows with higher weights receive proportionally more space. + * + * @param {number} index The index of the row. + * @param {number} weight The weight to be set on the row. + * @returns {Table} The calling Table. + */ + public rowWeight(index: number, weight: number): Table; + /** + * Sets the layout weight of a particular column. + * Space is allocated to columns based on their weight. Columns with higher weights receive proportionally more space. + * + * @param {number} index The index of the column. + * @param {number} weight The weight to be set on the column. + * @returns {Table} The calling Table. + */ + public colWeight(index: number, weight: number): Table; } } } @@ -296,18 +803,77 @@ declare module Plottable { declare module Plottable { module Abstract { - class Scale extends PlottableObject implements Plottable.Core.IListenable { - broadcaster: any; + class Scale extends PlottableObject implements Core.IListenable { + public broadcaster: Core.Broadcaster; + /** + * Creates a new Scale. + * + * @constructor + * @param {D3.Scale.Scale} scale The D3 scale backing the Scale. + */ constructor(scale: D3.Scale.Scale); - autoDomain(): Scale; - scale(value: any): any; - domain(): any[]; - domain(values: any[]): Scale; - range(): any[]; - range(values: any[]): Scale; - copy(): Scale; - updateExtent(plotProvidedKey: string, attr: string, extent: any[]): Scale; - removeExtent(plotProvidedKey: string, attr: string): 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 autoDomain(): Scale; + /** + * Returns the range value corresponding to a given domain value. + * + * @param value {any} A domain value to be scaled. + * @returns {any} The range value corresponding to the supplied domain value. + */ + public scale(value: any): any; + /** + * Gets the domain. + * + * @returns {any[]} The current domain. + */ + public domain(): any[]; + /** + * Sets the Scale's domain to the specified values. + * + * @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 {Scale} The calling Scale. + */ + public domain(values: any[]): Scale; + /** + * Gets the range. + * + * @returns {any[]} The current range. + */ + public range(): any[]; + /** + * Sets the Scale's range to the specified values. + * + * @param {any[]} values The new values for the range. + * @returns {Scale} The calling Scale. + */ + public range(values: any[]): Scale; + /** + * Creates a copy of the Scale with the same domain and range but without any registered listeners. + * + * @returns {Scale} A copy of the calling Scale. + */ + public copy(): Scale; + /** + * When a renderer determines that the extent of a projector has changed, + * it will call this function. This function should ensure that + * the scale has a domain at least large enough to include extent. + * + * @param {number} rendererID A unique indentifier of the renderer sending + * the new extent. + * @param {string} attr The attribute being projected, e.g. "x", "y0", "r" + * @param {any[]} extent The new extent to be included in the scale. + */ + public updateExtent(plotProvidedKey: string, attr: string, extent: any[]): Scale; + public removeExtent(plotProvidedKey: string, attr: string): Scale; } } } @@ -316,19 +882,55 @@ declare module Plottable { declare module Plottable { module Abstract { class Plot extends Component { - renderArea: D3.Selection; - element: D3.Selection; + public renderArea: D3.Selection; + public element: D3.Selection; + /** + * Creates a Plot. + * + * @constructor + * @param {any[]|DataSource} [dataset] The data or DataSource to be associated with this Plot. + */ constructor(); constructor(dataset: any[]); constructor(dataset: DataSource); - remove(): void; - dataSource(): DataSource; - dataSource(source: DataSource): Plot; - project(attrToSet: string, accessor: any, scale?: Scale): Plot; - animate(enabled: boolean): Plot; - detach(): Plot; - animator(animatorKey: string): Plottable.Animator.IPlotAnimator; - animator(animatorKey: string, animator: Plottable.Animator.IPlotAnimator): Plot; + public remove(): void; + /** + * Gets the Plot's DataSource. + * + * @return {DataSource} The current DataSource. + */ + public dataSource(): DataSource; + /** + * Sets the Plot's DataSource. + * + * @param {DataSource} source The DataSource the Plot should use. + * @return {Plot} The calling Plot. + */ + public dataSource(source: DataSource): Plot; + public project(attrToSet: string, accessor: any, scale?: Scale): Plot; + /** + * Enables or disables animation. + * + * @param {boolean} enabled Whether or not to animate. + */ + public animate(enabled: boolean): Plot; + public detach(): Plot; + /** + * Gets the animator associated with the specified Animator key. + * + * @param {string} animatorKey The key for the Animator. + * @return {Animator.IPlotAnimator} The Animator for the specified key. + */ + public animator(animatorKey: string): Animator.IPlotAnimator; + /** + * Sets the animator associated with the specified Animator key. + * + * @param {string} animatorKey The key for the Animator. + * @param {Animator.IPlotAnimator} animator An Animator to be assigned to + * the specified key. + * @return {Plot} The calling Plot. + */ + public animator(animatorKey: string, animator: Animator.IPlotAnimator): Plot; } } } @@ -337,10 +939,18 @@ declare module Plottable { declare module Plottable { module Abstract { class XYPlot extends Plot { - xScale: Scale; - yScale: Scale; + public xScale: Scale; + public yScale: Scale; + /** + * Creates an XYPlot. + * + * @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. + */ constructor(dataset: any, xScale: Scale, yScale: Scale); - project(attrToSet: string, accessor: any, scale?: Scale): XYPlot; + public project(attrToSet: string, accessor: any, scale?: Scale): XYPlot; } } } @@ -349,20 +959,52 @@ declare module Plottable { declare module Plottable { interface DatasetDrawerKey { dataset: DataSource; - drawer: Plottable.Abstract._Drawer; + drawer: Abstract._Drawer; key: string; } module Abstract { class NewStylePlot extends XYPlot { + /** + * Creates a NewStylePlot. + * + * @constructor + * @param [Scale] xScale The x scale to use + * @param [Scale] yScale The y scale to use + */ constructor(xScale?: Scale, yScale?: Scale); - remove(): void; - addDataset(key: string, dataset: DataSource): NewStylePlot; - addDataset(key: string, dataset: any[]): NewStylePlot; - addDataset(dataset: DataSource): NewStylePlot; - addDataset(dataset: any[]): NewStylePlot; - datasetOrder(): string[]; - datasetOrder(order: string[]): NewStylePlot; - removeDataset(key: string): NewStylePlot; + public remove(): void; + /** + * Adds a dataset to this plot. Identify this dataset with a key. + * + * A key is automatically generated if not supplied. + * + * @param {string} [key] The key of the dataset. + * @param {any[]|DataSource} dataset dataset to add. + * @return {NewStylePlot} The calling NewStylePlot. + */ + public addDataset(key: string, dataset: DataSource): NewStylePlot; + public addDataset(key: string, dataset: any[]): NewStylePlot; + public addDataset(dataset: DataSource): NewStylePlot; + public addDataset(dataset: any[]): NewStylePlot; + /** + * Gets the dataset order by key + * + * @return {string[]} a string array of the keys in order + */ + public datasetOrder(): string[]; + /** + * Sets the dataset order by key + * + * @param {string[]} order A string array which represents the order of the keys. This must be a permutation of existing keys. + */ + public datasetOrder(order: string[]): NewStylePlot; + /** + * Removes a dataset + * + * @param {string} key The key of the dataset + * @return {NewStylePlot} The calling NewStylePlot. + */ + public removeDataset(key: string): NewStylePlot; } } } @@ -376,13 +1018,13 @@ declare module Plottable { render(): any; } class Immediate implements IRenderPolicy { - render(): void; + public render(): void; } class AnimationFrame implements IRenderPolicy { - render(): void; + public render(): void; } class Timeout implements IRenderPolicy { - render(): void; + public render(): void; } } } @@ -392,10 +1034,32 @@ declare module Plottable { declare module Plottable { module Core { + /** + * The RenderController is responsible for enqueueing and synchronizing + * layout and render calls for Plottable components. + * + * Layouts and renders occur inside an animation callback + * (window.requestAnimationFrame if available). + * + * If you require immediate rendering, call RenderController.flush() to + * perform enqueued layout and rendering serially. + */ module RenderController { function setRenderPolicy(policy: RenderPolicy.IRenderPolicy): any; - function registerToRender(c: Plottable.Abstract.Component): void; - function registerToComputeLayout(c: Plottable.Abstract.Component): void; + /** + * If the RenderController is enabled, we enqueue the component for + * render. Otherwise, it is rendered immediately. + * + * @param {Abstract.Component} component Any Plottable component. + */ + function registerToRender(c: Abstract.Component): void; + /** + * If the RenderController is enabled, we enqueue the component for + * layout and render. Otherwise, it is rendered immediately. + * + * @param {Abstract.Component} component Any Plottable component. + */ + function registerToComputeLayout(c: Abstract.Component): void; function flush(): void; } } @@ -404,11 +1068,41 @@ declare module Plottable { declare module Plottable { module Core { + /** + * The ResizeBroadcaster will broadcast a notification to any registered + * components when the window is resized. + * + * The broadcaster and single event listener are lazily constructed. + * + * Upon resize, the _resized flag will be set to true until after the next + * flush of the RenderController. This is used, for example, to disable + * animations during resize. + */ module ResizeBroadcaster { + /** + * Returns true if the window has been resized and the RenderController + * has not yet been flushed. + */ function resizing(): boolean; function clearResizing(): any; - function register(c: Plottable.Abstract.Component): void; - function deregister(c: Plottable.Abstract.Component): void; + /** + * Registers a component. + * + * When the window is resized, we invoke ._invalidateLayout() on the + * component, which will enqueue the component for layout and rendering + * with the RenderController. + * + * @param {Abstract.Component} component Any Plottable component. + */ + function register(c: Abstract.Component): void; + /** + * Deregisters the components. + * + * The component will no longer receive updates on window resize. + * + * @param {Abstract.Component} component Any Plottable component. + */ + function deregister(c: Abstract.Component): void; } } } @@ -417,7 +1111,18 @@ declare module Plottable { declare module Plottable { module Animator { interface IPlotAnimator { - animate(selection: any, attrToProjector: IAttributeToProjector, plot: Plottable.Abstract.Plot): any; + /** + * Applies the supplied attributes to a D3.Selection with some animation. + * + * @param {D3.Selection} selection The update selection or transition selection that we wish to animate. + * @param {IAttributeToProjector} attrToProjector The set of + * IAccessors that we will use to set attributes on the selection. + * @param {Abstract.Plot} plot The plot being animated. + * @return {D3.Selection} Animators should return the selection or + * transition object so that plots may chain the transitions between + * animators. + */ + animate(selection: any, attrToProjector: IAttributeToProjector, plot: Abstract.Plot): any; } interface IPlotAnimatorMap { [animatorKey: string]: IPlotAnimator; @@ -442,7 +1147,7 @@ declare module Plottable { } interface _IProjector { accessor: IAccessor; - scale?: Plottable.Abstract.Scale; + scale?: Abstract.Scale; attribute: string; } interface IAttributeToProjector { @@ -464,12 +1169,6 @@ declare module Plottable { wantsWidth: boolean; wantsHeight: boolean; } - interface IPixelArea { - xMin: number; - xMax: number; - yMin: number; - yMax: number; - } interface IExtent { min: number; max: number; @@ -483,14 +1182,94 @@ declare module Plottable { declare module Plottable { class Domainer { + /** + * @param {(extents: any[][]) => any[]} combineExtents + * If present, this function will be used by the Domainer to merge + * all the extents that are present on a scale. + * + * A plot may draw multiple things relative to a scale, e.g. + * different stocks over time. The plot computes their extents, + * which are a [min, max] pair. combineExtents is responsible for + * merging them all into one [min, max] pair. It defaults to taking + * the min of the first elements and the max of the second arguments. + */ constructor(combineExtents?: (extents: any[][]) => any[]); - computeDomain(extents: any[][], scale: Plottable.Abstract.QuantitativeScale): any[]; - pad(padProportion?: number): Domainer; - addPaddingException(exception: any, key?: string): Domainer; - removePaddingException(keyOrException: any): Domainer; - addIncludedValue(value: any, key?: string): Domainer; - removeIncludedValue(valueOrKey: any): Domainer; - nice(count?: number): Domainer; + /** + * @param {any[][]} extents The list of extents to be reduced to a single + * extent. + * @param {Abstract.QuantitativeScale} scale + * Since nice() must do different things depending on Linear, Log, + * or Time scale, the scale must be passed in for nice() to work. + * @return {any[]} The domain, as a merging of all exents, as a [min, max] + * pair. + */ + public computeDomain(extents: any[][], scale: Abstract.QuantitativeScale): any[]; + /** + * Sets the Domainer to pad by a given ratio. + * + * @param {number} [padProportion] Proportionally how much bigger the + * new domain should be (0.05 = 5% larger). + * + * A domainer will pad equal visual amounts on each side. + * On a linear scale, this means both sides are padded the same + * amount: [10, 20] will be padded to [5, 25]. + * On a log scale, the top will be padded more than the bottom, so + * [10, 100] will be padded to [1, 1000]. + * + * @return {Domainer} The calling Domainer. + */ + public pad(padProportion?: number): Domainer; + /** + * Add a padding exception, a value that will not be padded at either end of the domain. + * + * Eg, if a padding exception is added at x=0, then [0, 100] will pad to [0, 105] instead of [-2.5, 102.5]. + * If a key is provided, it will be registered under that key with standard map semantics. (Overwrite / remove by key) + * If a key is not provided, it will be added with set semantics (Can be removed by value) + * + * @param {any} exception The padding exception to add. + * @param string [key] The key to register the exception under. + * @return Domainer The calling domainer + */ + public addPaddingException(exception: any, key?: string): Domainer; + /** + * Remove a padding exception, allowing the domain to pad out that value again. + * + * If a string is provided, it is assumed to be a key and the exception associated with that key is removed. + * If a non-string is provdied, it is assumed to be an unkeyed exception and that exception is removed. + * + * @param {any} keyOrException The key for the value to remove, or the value to remove + * @return Domainer The calling domainer + */ + public removePaddingException(keyOrException: any): Domainer; + /** + * Add an included value, a value that must be included inside the domain. + * + * Eg, if a value exception is added at x=0, then [50, 100] will expand to [0, 100] rather than [50, 100]. + * If a key is provided, it will be registered under that key with standard map semantics. (Overwrite / remove by key) + * If a key is not provided, it will be added with set semantics (Can be removed by value) + * + * @param {any} value The included value to add. + * @param string [key] The key to register the value under. + * @return Domainer The calling domainer + */ + public addIncludedValue(value: any, key?: string): Domainer; + /** + * Remove an included value, allowing the domain to not include that value gain again. + * + * If a string is provided, it is assumed to be a key and the value associated with that key is removed. + * If a non-string is provdied, it is assumed to be an unkeyed value and that value is removed. + * + * @param {any} keyOrException The key for the value to remove, or the value to remove + * @return Domainer The calling domainer + */ + public removeIncludedValue(valueOrKey: any): Domainer; + /** + * Extends the scale's domain so it starts and ends with "nice" values. + * + * @param {number} [count] The number of ticks that should fit inside the new domain. + * @return {Domainer} The calling Domainer. + */ + public nice(count?: number): Domainer; } } @@ -498,20 +1277,89 @@ declare module Plottable { declare module Plottable { module Abstract { class QuantitativeScale extends Scale { + /** + * Creates a new QuantitativeScale. + * + * @constructor + * @param {D3.Scale.QuantitativeScale} scale The D3 QuantitativeScale backing the QuantitativeScale. + */ constructor(scale: D3.Scale.QuantitativeScale); - invert(value: number): number; - copy(): QuantitativeScale; - domain(): any[]; - domain(values: any[]): QuantitativeScale; - interpolate(): D3.Transition.Interpolate; - interpolate(factory: D3.Transition.Interpolate): QuantitativeScale; - rangeRound(values: number[]): QuantitativeScale; - clamp(): boolean; - clamp(clamp: boolean): QuantitativeScale; - ticks(count?: number): any[]; - tickFormat(count: number, format?: string): (n: number) => string; - domainer(): Domainer; - domainer(domainer: Domainer): QuantitativeScale; + /** + * Retrieves the domain value corresponding to a supplied range value. + * + * @param {number} value: A value from the Scale's range. + * @returns {number} The domain value corresponding to the supplied range value. + */ + public invert(value: number): number; + /** + * Creates a copy of the QuantitativeScale with the same domain and range but without any registered listeners. + * + * @returns {QuantitativeScale} A copy of the calling QuantitativeScale. + */ + public copy(): QuantitativeScale; + public domain(): any[]; + public domain(values: any[]): QuantitativeScale; + /** + * Sets or gets the QuantitativeScale's output interpolator + * + * @param {D3.Transition.Interpolate} [factory] The output interpolator to use. + * @returns {D3.Transition.Interpolate|QuantitativeScale} The current output interpolator, or the calling QuantitativeScale. + */ + public interpolate(): D3.Transition.Interpolate; + public interpolate(factory: D3.Transition.Interpolate): QuantitativeScale; + /** + * Sets the range of the QuantitativeScale and sets the interpolator to d3.interpolateRound. + * + * @param {number[]} values The new range value for the range. + */ + public rangeRound(values: number[]): QuantitativeScale; + /** + * Gets the clamp status of the QuantitativeScale (whether to cut off values outside the ouput range). + * + * @returns {boolean} The current clamp status. + */ + public clamp(): boolean; + /** + * Sets the clamp status of the QuantitativeScale (whether to cut off values outside the ouput range). + * + * @param {boolean} clamp Whether or not to clamp the QuantitativeScale. + * @returns {QuantitativeScale} The calling QuantitativeScale. + */ + public clamp(clamp: boolean): QuantitativeScale; + /** + * Generates tick values. + * + * @param {number} [count] The number of ticks to generate. + * @returns {any[]} The generated ticks. + */ + public ticks(count?: number): any[]; + /** + * Gets a tick formatting function for displaying tick values. + * + * @param {number} count The number of ticks to be displayed + * @param {string} [format] A format specifier string. + * @returns {(n: number) => string} A formatting function. + */ + public tickFormat(count: number, format?: string): (n: number) => string; + /** + * Retrieve a Domainer of a scale. A Domainer is responsible for combining + * multiple extents into a single domain. + * + * @return {QuantitativeScale} The scale's current domainer. + */ + public domainer(): Domainer; + /** + * Sets a Domainer of a scale. A Domainer is responsible for combining + * multiple extents into a single domain. + * + * When you set domainer, we assume that you know what you want the domain + * to look like better that we do. Ensuring that the domain is padded, + * includes 0, etc., will be the responsability of the new domainer. + * + * @param {Domainer} domainer The domainer to be set. + * @return {QuantitativeScale} The calling scale. + */ + public domainer(domainer: Domainer): QuantitativeScale; } } } @@ -519,10 +1367,21 @@ declare module Plottable { declare module Plottable { module Scale { - class Linear extends Plottable.Abstract.QuantitativeScale { + class Linear extends Abstract.QuantitativeScale { + /** + * Creates a new LinearScale. + * + * @constructor + * @param {D3.Scale.LinearScale} [scale] The D3 LinearScale backing the LinearScale. If not supplied, uses a default scale. + */ constructor(); constructor(scale: D3.Scale.LinearScale); - copy(): Linear; + /** + * Creates a copy of the LinearScale with the same domain and range but without any registered listeners. + * + * @returns {LinearScale} A copy of the calling LinearScale. + */ + public copy(): Linear; } } } @@ -530,10 +1389,27 @@ declare module Plottable { declare module Plottable { module Scale { - class Log extends Plottable.Abstract.QuantitativeScale { + class Log extends Abstract.QuantitativeScale { + /** + * Creates a new Scale.Log. + * + * Warning: Log is deprecated; if possible, use ModifiedLog. Log scales are + * very unstable due to the fact that they can't handle 0 or negative + * numbers. The only time when you would want to use a Log scale over a + * ModifiedLog scale is if you're plotting very small data, such as all + * data < 1. + * + * @constructor + * @param {D3.Scale.LogScale} [scale] The D3 Scale.Log backing the Scale.Log. If not supplied, uses a default scale. + */ constructor(); constructor(scale: D3.Scale.LogScale); - copy(): Log; + /** + * Creates a copy of the Scale.Log with the same domain and range but without any registered listeners. + * + * @returns {Scale.Log} A copy of the calling Scale.Log. + */ + public copy(): Log; } } } @@ -541,14 +1417,51 @@ declare module Plottable { declare module Plottable { module Scale { - class ModifiedLog extends Plottable.Abstract.QuantitativeScale { + class ModifiedLog extends Abstract.QuantitativeScale { + /** + * Creates a new Scale.ModifiedLog. + * + * A ModifiedLog scale acts as a regular log scale for large numbers. + * As it approaches 0, it gradually becomes linear. This means that the + * scale won't freak out if you give it 0 or a negative number, where an + * ordinary Log scale would. + * + * However, it does mean that scale will be effectively linear as values + * approach 0. If you want very small values on a log scale, you should use + * an ordinary Scale.Log instead. + * + * @constructor + * @param {number} [base] + * The base of the log. Defaults to 10, and must be > 1. + * + * For base <= x, scale(x) = log(x). + * + * For 0 < x < base, scale(x) will become more and more + * linear as it approaches 0. + * + * At x == 0, scale(x) == 0. + * + * For negative values, scale(-x) = -scale(x). + */ constructor(base?: number); - scale(x: number): number; - invert(x: number): number; - ticks(count?: number): number[]; - copy(): ModifiedLog; - showIntermediateTicks(): boolean; - showIntermediateTicks(show: boolean): ModifiedLog; + public scale(x: number): number; + public invert(x: number): number; + public ticks(count?: number): number[]; + public copy(): ModifiedLog; + /** + * @returns {boolean} + * Whether or not to return tick values other than powers of base. + * + * This defaults to false, so you'll normally only see ticks like + * [10, 100, 1000]. If you turn it on, you might see ticks values + * like [10, 50, 100, 500, 1000]. + */ + public showIntermediateTicks(): boolean; + /** + * @param {boolean} show + * Whether or not to return ticks values other than powers of the base. + */ + public showIntermediateTicks(show: boolean): ModifiedLog; } } } @@ -556,18 +1469,72 @@ declare module Plottable { declare module Plottable { module Scale { - class Ordinal extends Plottable.Abstract.Scale { + class Ordinal extends Abstract.Scale { + /** + * Creates a new OrdinalScale. Domain and Range are set later. + * + * @constructor + */ constructor(scale?: D3.Scale.OrdinalScale); - domain(): any[]; - domain(values: any[]): Ordinal; - range(): number[]; - range(values: number[]): Ordinal; - rangeBand(): number; - innerPadding(): number; - fullBandStartAndWidth(v: any): number[]; - rangeType(): string; - rangeType(rangeType: string, outerPadding?: number, innerPadding?: number): Ordinal; - copy(): Ordinal; + /** + * Gets the domain. + * + * @returns {any[]} The current domain. + */ + public domain(): any[]; + /** + * Sets the domain. + * + * @param {any[]} values The new values for the domain. This array may contain more than 2 values. + * @returns {Ordinal} The calling Ordinal Scale. + */ + public domain(values: any[]): Ordinal; + /** + * Gets the range of pixels spanned by the Ordinal Scale. + * + * @returns {number[]} The pixel range. + */ + public range(): number[]; + /** + * Sets the range of pixels spanned by the Ordinal Scale. + * + * @param {number[]} values The pixel range to to be spanend by the scale. + * @returns {Ordinal} The calling Ordinal Scale. + */ + public range(values: number[]): Ordinal; + /** + * 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; + public innerPadding(): number; + public fullBandStartAndWidth(v: any): number[]; + /** + * Gets the range type. + * + * @returns {string} The current range type. + */ + public rangeType(): string; + /** + * 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 {Ordinal} The calling Ordinal Scale. + */ + public rangeType(rangeType: string, outerPadding?: number, innerPadding?: number): Ordinal; + /** + * Creates a copy of the Scale with the same domain and range but without any registered listeners. + * + * @returns {Ordinal} A copy of the calling Scale. + */ + public copy(): Ordinal; } } } @@ -575,7 +1542,15 @@ declare module Plottable { declare module Plottable { module Scale { - class Color extends Plottable.Abstract.Scale { + class Color extends Abstract.Scale { + /** + * Creates a ColorScale. + * + * @constructor + * @param {string} [scaleType] the type of color scale to create + * (Category10/Category20/Category20b/Category20c). + * See https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors + */ constructor(scaleType?: string); } } @@ -584,13 +1559,24 @@ declare module Plottable { declare module Plottable { module Scale { - class Time extends Plottable.Abstract.QuantitativeScale { + class Time extends Abstract.QuantitativeScale { + /** + * Creates a new Time Scale. + * + * @constructor + * @param {D3.Scale.Time} [scale] The D3 LinearScale backing the TimeScale. If not supplied, uses a default scale. + */ constructor(); constructor(scale: D3.Scale.LinearScale); - tickInterval(interval: D3.Time.Interval, step?: number): any[]; - domain(): any[]; - domain(values: any[]): Time; - copy(): Time; + public tickInterval(interval: D3.Time.Interval, step?: number): any[]; + public domain(): any[]; + public domain(values: any[]): Time; + /** + * Creates a copy of the TimeScale with the same domain and range but without any registered listeners. + * + * @returns {TimeScale} A copy of the calling TimeScale. + */ + public copy(): Time; } } } @@ -598,13 +1584,58 @@ declare module Plottable { declare module Plottable { module Scale { - class InterpolatedColor extends Plottable.Abstract.QuantitativeScale { + /** + * This class implements a color scale that takes quantitive input and + * interpolates between a list of color values. It returns a hex string + * representing the interpolated color. + * + * By default it generates a linear scale internally. + */ + class InterpolatedColor extends Abstract.QuantitativeScale { + /** + * Creates a InterpolatedColorScale. + * + * @constructor + * @param {string|string[]} [colorRange] the type of color scale to + * create. Default is "reds". @see {@link colorRange} for further + * options. + * @param {string} [scaleType] the type of underlying scale to use + * (linear/pow/log/sqrt). Default is "linear". @see {@link scaleType} + * for further options. + */ constructor(colorRange?: any, scaleType?: string); - colorRange(): string[]; - colorRange(colorRange: any): InterpolatedColor; - scaleType(): string; - scaleType(scaleType: string): InterpolatedColor; - autoDomain(): InterpolatedColor; + /** + * Gets the color range. + * + * @returns {string[]} the current color values for the range as strings. + */ + public colorRange(): string[]; + /** + * Sets the color range. + * + * @param {string|string[]} colorRange. If colorRange is one of + * (reds/blues/posneg), uses the built-in color groups. If colorRange + * is an array of strings with at least 2 values + * (e.g. ["#FF00FF", "red", "dodgerblue"], the resulting scale + * will interpolate between the color values across the domain. + * @returns {InterpolatedColor} The calling InterpolatedColor Scale. + */ + public colorRange(colorRange: any): InterpolatedColor; + /** + * Gets the internal scale type. + * + * @returns {string} The current scale type. + */ + public scaleType(): string; + /** + * Sets the internal scale type. + * + * @param {string} scaleType. The type of d3 scale to use internally. + * (linear/log/sqrt/pow). + * @returns {InterpolatedColor} The calling InterpolatedColor Scale. + */ + public scaleType(scaleType: string): InterpolatedColor; + public autoDomain(): InterpolatedColor; } } } @@ -613,8 +1644,14 @@ declare module Plottable { declare module Plottable { module Util { class ScaleDomainCoordinator { - constructor(scales: Plottable.Abstract.Scale[]); - rescale(scale: Plottable.Abstract.Scale): void; + /** + * Creates a ScaleDomainCoordinator. + * + * @constructor + * @param {Scale[]} scales A list of scales whose domains should be linked. + */ + constructor(scales: Abstract.Scale[]); + public rescale(scale: Abstract.Scale): void; } } } @@ -623,11 +1660,26 @@ declare module Plottable { declare module Plottable { module Abstract { class _Drawer { - renderArea: D3.Selection; - key: string; + public renderArea: D3.Selection; + public key: string; + /** + * Creates a Drawer + * + * @constructor + * @param{string} key The key associated with this Drawer + */ constructor(key: string); - remove(): void; - draw(data: any[][], attrToProjector: IAttributeToProjector): void; + /** + * Removes the Drawer and its renderArea + */ + public remove(): void; + /** + * Draws the data into the renderArea using the attrHash for attributes + * + * @param{any[][]} data The data to be drawn + * @param{attrHash} IAttributeToProjector The list of attributes to set on the data + */ + public draw(data: any[][], attrToProjector: IAttributeToProjector): void; } } } @@ -635,8 +1687,8 @@ declare module Plottable { declare module Plottable { module _Drawer { - class Rect extends Plottable.Abstract._Drawer { - draw(data: any[][], attrToProjector: IAttributeToProjector): void; + class Rect extends Abstract._Drawer { + public draw(data: any[][], attrToProjector: IAttributeToProjector): void; } } } @@ -645,29 +1697,138 @@ declare module Plottable { declare module Plottable { module Abstract { class Axis extends Component { + /** + * The css class applied to each end tick mark (the line on the end tick). + */ static END_TICK_MARK_CLASS: string; + /** + * The css class applied to each tick mark (the line on the tick). + */ static TICK_MARK_CLASS: string; + /** + * The css class applied to each tick label (the text associated with the tick). + */ static TICK_LABEL_CLASS: string; constructor(scale: Scale, orientation: string, formatter?: (d: any) => string); - remove(): void; - width(): number; - width(w: any): Axis; - height(): number; - height(h: any): Axis; - formatter(): Formatter; - formatter(formatter: Formatter): Axis; - tickLength(): number; - tickLength(length: number): Axis; - endTickLength(): number; - endTickLength(length: number): Axis; - tickLabelPadding(): number; - tickLabelPadding(padding: number): Axis; - gutter(): number; - gutter(size: number): Axis; - orient(): string; - orient(newOrientation: string): Axis; - showEndTickLabels(): boolean; - showEndTickLabels(show: boolean): Axis; + public remove(): void; + /** + * Gets the current width. + * + * @returns {number} The current width. + */ + public width(): number; + /** + * Sets a user-specified width. + * + * @param {number|String} w A fixed width for the Axis, or "auto" for automatic mode. + * @returns {Axis} The calling Axis. + */ + public width(w: any): Axis; + /** + * Gets the current height. + * + * @returns {number} The current height. + */ + public height(): number; + /** + * Sets a user-specified height. + * + * @param {number|String} h A fixed height for the Axis, or "auto" for automatic mode. + * @returns {Axis} The calling Axis. + */ + public height(h: any): Axis; + /** + * Get the current formatter on the axis. + * + * @returns {Formatter} the axis formatter + */ + public formatter(): Formatter; + /** + * Sets a new tick formatter. + * + * @param {Formatter} formatter + * @returns {Abstract.Axis} The calling Axis. + */ + public formatter(formatter: Formatter): Axis; + /** + * Gets the current tick mark length. + * + * @returns {number} The current tick mark length. + */ + public tickLength(): number; + /** + * Sets the tick mark length. + * + * @param {number} length The length of each tick. + * @returns {BaseAxis} The calling Axis. + */ + public tickLength(length: number): Axis; + /** + * Gets the current end tick mark length. + * + * @returns {number} The current end tick mark length. + */ + public endTickLength(): number; + /** + * Sets the end tick mark length. + * + * @param {number} length The length of the end ticks. + * @returns {BaseAxis} The calling Axis. + */ + public endTickLength(length: number): Axis; + /** + * Gets the padding between each tick mark and its associated label. + * + * @returns {number} The current padding, in pixels. + */ + public tickLabelPadding(): number; + /** + * Sets the padding between each tick mark and its associated label. + * + * @param {number} padding The desired padding, in pixels. + * @returns {Axis} The calling Axis. + */ + public tickLabelPadding(padding: number): Axis; + /** + * Gets the size of the gutter (the extra space between the tick labels and the outer edge of the axis). + * + * @returns {number} The current size of the gutter, in pixels. + */ + public gutter(): number; + /** + * Sets the size of the gutter (the extra space between the tick labels and the outer edge of the axis). + * + * @param {number} size The desired size of the gutter, in pixels. + * @returns {Axis} The calling Axis. + */ + public gutter(size: number): Axis; + /** + * Gets the orientation of the Axis. + * + * @returns {string} The current orientation. + */ + public orient(): string; + /** + * Sets the orientation of the Axis. + * + * @param {string} newOrientation The desired orientation (top/bottom/left/right). + * @returns {Axis} The calling Axis. + */ + public orient(newOrientation: string): Axis; + /** + * Checks whether the Axis is currently set to show the first and last + * tick labels. + * + * @returns {boolean} + */ + public showEndTickLabels(): boolean; + /** + * Set whether or not to show the first and last tick labels. + * + * @param {boolean} show Whether or not to show the first and last labels. + * @returns {Axis} The calling Axis. + */ + public showEndTickLabels(show: boolean): Axis; } } } @@ -680,10 +1841,17 @@ declare module Plottable { step: number; formatString: string; } - class Time extends Plottable.Abstract.Axis { + class Time extends Abstract.Axis { static minorIntervals: ITimeInterval[]; static majorIntervals: ITimeInterval[]; - constructor(scale: Plottable.Scale.Time, orientation: string); + /** + * Creates a TimeAxis + * + * @constructor + * @param {TimeScale} scale The scale to base the Axis on. + * @param {string} orientation The orientation of the Axis (top/bottom) + */ + constructor(scale: Scale.Time, orientation: string); } } } @@ -691,12 +1859,54 @@ declare module Plottable { declare module Plottable { module Axis { - class Numeric extends Plottable.Abstract.Axis { - constructor(scale: Plottable.Abstract.QuantitativeScale, orientation: string, formatter?: (d: any) => string); - tickLabelPosition(): string; - tickLabelPosition(position: string): Numeric; - showEndTickLabel(orientation: string): boolean; - showEndTickLabel(orientation: string, show: boolean): Numeric; + class Numeric extends Abstract.Axis { + /** + * Creates a NumericAxis. + * + * @constructor + * @param {QuantitativeScale} scale The QuantitativeScale to base the NumericAxis on. + * @param {string} orientation The orientation of the QuantitativeScale (top/bottom/left/right) + * @param {Formatter} [formatter] A function to format tick labels (default Formatters.general(3, false)). + */ + constructor(scale: Abstract.QuantitativeScale, orientation: string, formatter?: (d: any) => string); + /** + * Gets the tick label position relative to the tick marks. + * + * @returns {string} The current tick label position. + */ + public tickLabelPosition(): string; + /** + * Sets the tick label position relative to the tick marks. + * + * @param {string} position The relative position of the tick label. + * [top/center/bottom] for a vertical NumericAxis, + * [left/center/right] for a horizontal NumericAxis. + * @returns {NumericAxis} The calling NumericAxis. + */ + public tickLabelPosition(position: string): Numeric; + /** + * Return whether or not the tick labels at the end of the graph are + * displayed when partially cut off. + * + * @param {string} orientation Where on the scale to change tick labels. + * On a "top" or "bottom" axis, this can be "left" or + * "right". On a "left" or "right" axis, this can be "top" + * or "bottom". + * @returns {boolean} The current setting. + */ + public showEndTickLabel(orientation: string): boolean; + /** + * Control whether or not the tick labels at the end of the graph are + * displayed when partially cut off. + * + * @param {string} orientation Where on the scale to change tick labels. + * On a "top" or "bottom" axis, this can be "left" or + * "right". On a "left" or "right" axis, this can be "top" + * or "bottom". + * @param {boolean} show Whether or not the given tick should be displayed. + * @returns {Numeric} The calling Numeric. + */ + public showEndTickLabel(orientation: string, show: boolean): Numeric; } } } @@ -704,8 +1914,19 @@ declare module Plottable { declare module Plottable { module Axis { - class Category extends Plottable.Abstract.Axis { - constructor(scale: Plottable.Scale.Ordinal, orientation?: string, formatter?: (d: any) => string); + class Category extends Abstract.Axis { + /** + * Creates a CategoryAxis. + * + * A CategoryAxis takes an OrdinalScale and includes word-wrapping algorithms and advanced layout logic to try to + * display the scale as efficiently as possible. + * + * @constructor + * @param {OrdinalScale} scale The scale to base the Axis on. + * @param {string} orientation The orientation of the Axis (top/bottom/left/right) + * @param {Formatter} [formatter] The Formatter for the Axis (default Formatters.identity()) + */ + constructor(scale: Scale.Ordinal, orientation?: string, formatter?: (d: any) => string); } } } @@ -713,12 +1934,30 @@ declare module Plottable { declare module Plottable { module Component { - class Label extends Plottable.Abstract.Component { + class Label extends Abstract.Component { + /** + * Creates a Label. + * + * @constructor + * @param {string} [displayText] The text of the Label. + * @param {string} [orientation] The orientation of the Label (horizontal/vertical-left/vertical-right). + */ constructor(displayText?: string, orientation?: string); - xAlign(alignment: string): Label; - yAlign(alignment: string): Label; - text(): string; - text(displayText: string): Label; + public xAlign(alignment: string): Label; + public yAlign(alignment: string): Label; + /** + * Retrieve the current text on the Label. + * + * @returns {string} The text on the label. + */ + public text(): string; + /** + * Sets the text on the Label. + * + * @param {string} displayText The new text for the Label. + * @returns {Label} The calling Label. + */ + public text(displayText: string): Label; } class TitleLabel extends Label { constructor(text?: string, orientation?: string); @@ -738,16 +1977,55 @@ declare module Plottable { interface HoverCallback { (datum?: string): any; } - class Legend extends Plottable.Abstract.Component { + class Legend extends Abstract.Component { + /** + * The css class applied to each legend row + */ static SUBELEMENT_CLASS: string; - constructor(colorScale?: Plottable.Scale.Color); - remove(): void; - toggleCallback(callback: ToggleCallback): Legend; - toggleCallback(): ToggleCallback; - hoverCallback(callback: HoverCallback): Legend; - hoverCallback(): HoverCallback; - scale(scale: Plottable.Scale.Color): Legend; - scale(): Plottable.Scale.Color; + /** + * Creates a Legend. + * + * A legend consists of a series of legend rows, each with a color and label taken from the `colorScale`. + * The rows will be displayed in the order of the `colorScale` domain. + * This legend also allows interactions, through the functions `toggleCallback` and `hoverCallback` + * Setting a callback will also put classes on the individual rows. + * + * @constructor + * @param {Scale.Color} colorScale + */ + constructor(colorScale?: Scale.Color); + public remove(): void; + /** + * Assigns or gets the callback to the Legend + * + * This callback is associated with toggle events, which trigger when a legend row is clicked. + * Internally, this will change the state of of the row from "toggled-on" to "toggled-off" and vice versa. + * Setting a callback will also set a class to each individual legend row as "toggled-on" or "toggled-off". + * Call with argument of null to remove the callback. This will also remove the above classes to legend rows. + * + * @param {ToggleCallback} callback The new callback function + */ + public toggleCallback(callback: ToggleCallback): Legend; + public toggleCallback(): ToggleCallback; + /** + * Assigns or gets the callback to the Legend + * This callback is associated with hover events, which trigger when the mouse enters or leaves a legend row + * Setting a callback will also set the class "hover" to all legend row, + * as well as the class "focus" to the legend row being hovered over. + * Call with argument of null to remove the callback. This will also remove the above classes to legend rows. + * + * @param{HoverCallback} callback The new callback function + */ + public hoverCallback(callback: HoverCallback): Legend; + public hoverCallback(): HoverCallback; + /** + * Assigns a new ColorScale to the Legend. + * + * @param {ColorScale} scale + * @returns {Legend} The calling Legend. + */ + public scale(scale: Scale.Color): Legend; + public scale(): Scale.Color; } } } @@ -755,9 +2033,16 @@ declare module Plottable { declare module Plottable { module Component { - class Gridlines extends Plottable.Abstract.Component { - constructor(xScale: Plottable.Abstract.QuantitativeScale, yScale: Plottable.Abstract.QuantitativeScale); - remove(): Gridlines; + class Gridlines extends Abstract.Component { + /** + * Creates a set of Gridlines. + * @constructor + * + * @param {QuantitativeScale} xScale The scale to base the x gridlines on. Pass null if no gridlines are desired. + * @param {QuantitativeScale} yScale The scale to base the y gridlines on. Pass null if no gridlines are desired. + */ + constructor(xScale: Abstract.QuantitativeScale, yScale: Abstract.QuantitativeScale); + public remove(): Gridlines; } } } @@ -765,9 +2050,17 @@ declare module Plottable { declare module Plottable { module Plot { - class Scatter extends Plottable.Abstract.XYPlot { - constructor(dataset: any, xScale: Plottable.Abstract.Scale, yScale: Plottable.Abstract.Scale); - project(attrToSet: string, accessor: any, scale?: Plottable.Abstract.Scale): Scatter; + class Scatter extends Abstract.XYPlot { + /** + * Creates a ScatterPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ + constructor(dataset: any, xScale: Abstract.Scale, yScale: Abstract.Scale); + public project(attrToSet: string, accessor: any, scale?: Abstract.Scale): Scatter; } } } @@ -775,12 +2068,22 @@ declare module Plottable { declare module Plottable { module Plot { - class Grid extends Plottable.Abstract.XYPlot { - colorScale: Plottable.Abstract.Scale; - xScale: Plottable.Scale.Ordinal; - yScale: Plottable.Scale.Ordinal; - constructor(dataset: any, xScale: Plottable.Scale.Ordinal, yScale: Plottable.Scale.Ordinal, colorScale: Plottable.Abstract.Scale); - project(attrToSet: string, accessor: any, scale?: Plottable.Abstract.Scale): Grid; + class Grid extends Abstract.XYPlot { + public colorScale: Abstract.Scale; + public xScale: Scale.Ordinal; + public yScale: Scale.Ordinal; + /** + * Creates a GridPlot. + * + * @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. + */ + constructor(dataset: any, xScale: Scale.Ordinal, yScale: Scale.Ordinal, colorScale: Abstract.Scale); + public project(attrToSet: string, accessor: any, scale?: Abstract.Scale): Grid; } } } @@ -789,14 +2092,51 @@ declare module Plottable { declare module Plottable { module Abstract { class BarPlot extends XYPlot { + /** + * Creates an AbstractBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ constructor(dataset: any, xScale: Scale, yScale: Scale); - baseline(value: number): BarPlot; - barAlignment(alignment: string): BarPlot; - selectBar(xValOrExtent: IExtent, yValOrExtent: IExtent, select?: boolean): D3.Selection; - selectBar(xValOrExtent: number, yValOrExtent: IExtent, select?: boolean): D3.Selection; - selectBar(xValOrExtent: IExtent, yValOrExtent: number, select?: boolean): D3.Selection; - selectBar(xValOrExtent: number, yValOrExtent: number, select?: boolean): D3.Selection; - deselectAll(): BarPlot; + /** + * Sets the baseline for the bars to the specified value. + * + * @param {number} value The value to position the baseline at. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ + public baseline(value: number): BarPlot; + /** + * Sets the bar alignment relative to the independent axis. + * VerticalBarPlot supports "left", "center", "right" + * HorizontalBarPlot supports "top", "center", "bottom" + * + * @param {string} alignment The desired alignment. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ + public barAlignment(alignment: string): BarPlot; + /** + * Selects the bar under the given pixel position (if [xValOrExtent] + * and [yValOrExtent] are {number}s), under a given line (if only one + * of [xValOrExtent] or [yValOrExtent] are {IExtent}s) or are under a + * 2D area (if [xValOrExtent] and [yValOrExtent] are both {IExtent}s). + * + * @param {any} xValOrExtent The pixel x position, or range of x values. + * @param {any} yValOrExtent The pixel y position, or range of y values. + * @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(xValOrExtent: IExtent, yValOrExtent: IExtent, select?: boolean): D3.Selection; + public selectBar(xValOrExtent: number, yValOrExtent: IExtent, select?: boolean): D3.Selection; + public selectBar(xValOrExtent: IExtent, yValOrExtent: number, select?: boolean): D3.Selection; + public selectBar(xValOrExtent: number, yValOrExtent: number, select?: boolean): D3.Selection; + /** + * Deselects all bars. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ + public deselectAll(): BarPlot; } } } @@ -804,8 +2144,25 @@ declare module Plottable { declare module Plottable { module Plot { - class VerticalBar extends Plottable.Abstract.BarPlot { - constructor(dataset: any, xScale: Plottable.Abstract.Scale, yScale: Plottable.Abstract.QuantitativeScale); + /** + * A VerticalBarPlot draws bars vertically. + * Key projected attributes: + * - "width" - the horizontal width of a bar. + * - if an ordinal scale is attached, this defaults to ordinalScale.rangeBand() + * - if a quantitative scale is attached, this defaults to 10 + * - "x" - the horizontal position of a bar + * - "y" - the vertical height of a bar + */ + class VerticalBar extends Abstract.BarPlot { + /** + * Creates a VerticalBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {QuantitativeScale} yScale The y scale to use. + */ + constructor(dataset: any, xScale: Abstract.Scale, yScale: Abstract.QuantitativeScale); } } } @@ -813,9 +2170,26 @@ declare module Plottable { declare module Plottable { module Plot { - class HorizontalBar extends Plottable.Abstract.BarPlot { - isVertical: boolean; - constructor(dataset: any, xScale: Plottable.Abstract.QuantitativeScale, yScale: Plottable.Abstract.Scale); + /** + * A HorizontalBarPlot draws bars horizontally. + * Key projected attributes: + * - "width" - the vertical height of a bar (since the bar is rotated horizontally) + * - if an ordinal scale is attached, this defaults to ordinalScale.rangeBand() + * - if a quantitative scale is attached, this defaults to 10 + * - "x" - the horizontal length of a bar + * - "y" - the vertical position of a bar + */ + class HorizontalBar extends Abstract.BarPlot { + public isVertical: boolean; + /** + * Creates a HorizontalBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {QuantitativeScale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ + constructor(dataset: any, xScale: Abstract.QuantitativeScale, yScale: Abstract.Scale); } } } @@ -823,8 +2197,16 @@ declare module Plottable { declare module Plottable { module Plot { - class Line extends Plottable.Abstract.XYPlot { - constructor(dataset: any, xScale: Plottable.Abstract.Scale, yScale: Plottable.Abstract.Scale); + class Line extends Abstract.XYPlot { + /** + * Creates a LinePlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ + constructor(dataset: any, xScale: Abstract.Scale, yScale: Abstract.Scale); } } } @@ -832,9 +2214,20 @@ declare module Plottable { declare module Plottable { module Plot { + /** + * An AreaPlot draws a filled region (area) between the plot's projected "y" and projected "y0" values. + */ class Area extends Line { - constructor(dataset: any, xScale: Plottable.Abstract.Scale, yScale: Plottable.Abstract.Scale); - project(attrToSet: string, accessor: any, scale?: Plottable.Abstract.Scale): Area; + /** + * Creates an AreaPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ + constructor(dataset: any, xScale: Abstract.Scale, yScale: Abstract.Scale); + public project(attrToSet: string, accessor: any, scale?: Abstract.Scale): Area; } } } @@ -844,8 +2237,21 @@ declare module Plottable { module Abstract { class NewStyleBarPlot extends NewStylePlot { static DEFAULT_WIDTH: number; + /** + * Creates an NewStyleBarPlot. + * + * @constructor + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ constructor(xScale: Scale, yScale: Scale); - baseline(value: number): any; + /** + * Sets the baseline for the bars to the specified value. + * + * @param {number} value The value to position the baseline at. + * @return {NewStyleBarPlot} The calling NewStyleBarPlot. + */ + public baseline(value: number): any; } } } @@ -853,9 +2259,9 @@ declare module Plottable { declare module Plottable { module Plot { - class ClusteredBar extends Plottable.Abstract.NewStyleBarPlot { + class ClusteredBar extends Abstract.NewStyleBarPlot { static DEFAULT_WIDTH: number; - constructor(xScale: Plottable.Abstract.Scale, yScale: Plottable.Abstract.QuantitativeScale); + constructor(xScale: Abstract.Scale, yScale: Abstract.QuantitativeScale); } } } @@ -863,9 +2269,9 @@ declare module Plottable { declare module Plottable { module Plot { - class StackedBar extends Plottable.Abstract.NewStyleBarPlot { - stackedData: any[][]; - constructor(xScale?: Plottable.Abstract.Scale, yScale?: Plottable.Abstract.Scale); + class StackedBar extends Abstract.NewStyleBarPlot { + public stackedData: any[][]; + constructor(xScale?: Abstract.Scale, yScale?: Abstract.Scale); } } } @@ -873,8 +2279,12 @@ declare module Plottable { declare module Plottable { module Animator { + /** + * An animator implementation with no animation. The attributes are + * immediately set on the selection. + */ class Null implements IPlotAnimator { - animate(selection: any, attrToProjector: IAttributeToProjector, plot: Plottable.Abstract.Plot): any; + public animate(selection: any, attrToProjector: IAttributeToProjector, plot: Abstract.Plot): any; } } } @@ -882,14 +2292,50 @@ declare module Plottable { declare module Plottable { module Animator { + /** + * The default animator implementation with easing, duration, and delay. + */ class Default implements IPlotAnimator { - animate(selection: any, attrToProjector: IAttributeToProjector, plot: Plottable.Abstract.Plot): any; - duration(): Number; - duration(duration: Number): Default; - delay(): Number; - delay(delay: Number): Default; - easing(): string; - easing(easing: string): Default; + public animate(selection: any, attrToProjector: IAttributeToProjector, plot: Abstract.Plot): any; + /** + * Gets the duration of the animation in milliseconds. + * + * @returns {Number} The current duration. + */ + public duration(): Number; + /** + * Sets the duration of the animation in milliseconds. + * + * @param {Number} duration The duration in milliseconds. + * @returns {Default} The calling Default Animator. + */ + public duration(duration: Number): Default; + /** + * Gets the delay of the animation in milliseconds. + * + * @returns {Number} The current delay. + */ + public delay(): Number; + /** + * Sets the delay of the animation in milliseconds. + * + * @param {Number} delay The delay in milliseconds. + * @returns {Default} The calling Default Animator. + */ + public delay(delay: Number): Default; + /** + * Gets the current easing of the animation. + * + * @returns {string} the current easing mode. + */ + public easing(): string; + /** + * Sets the easing mode of the animation. + * + * @param {string} easing The desired easing mode. + * @returns {Default} The calling Default Animator. + */ + public easing(easing: string): Default; } } } @@ -897,8 +2343,14 @@ declare module Plottable { declare module Plottable { module Animator { + /** + * An animator that delays the animation of the attributes using the index + * of the selection data. + * + * The delay between animations can be configured with the .delay getter/setter. + */ class IterativeDelay extends Default { - animate(selection: any, attrToProjector: IAttributeToProjector, plot: Plottable.Abstract.Plot): any; + public animate(selection: any, attrToProjector: IAttributeToProjector, plot: Abstract.Plot): any; } } } @@ -920,10 +2372,20 @@ declare module Plottable { declare module Plottable { module Abstract { class Interaction { - hitBox: D3.Selection; - componentToListenTo: Component; + public hitBox: D3.Selection; + public componentToListenTo: Component; + /** + * Creates an Interaction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for interactions on. + */ constructor(componentToListenTo: Component); - registerWithComponent(): Interaction; + /** + * Registers the Interaction on the Component it's listening to. + * This needs to be called to activate the interaction. + */ + public registerWithComponent(): Interaction; } } } @@ -931,12 +2393,29 @@ declare module Plottable { declare module Plottable { module Interaction { - class Click extends Plottable.Abstract.Interaction { - constructor(componentToListenTo: Plottable.Abstract.Component); - callback(cb: (x: number, y: number) => any): Click; + class Click extends Abstract.Interaction { + /** + * Creates a ClickInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for clicks on. + */ + constructor(componentToListenTo: Abstract.Component); + /** + * Sets an callback to be called when a click is received. + * + * @param {(x: number, y: number) => any} cb: Callback to be called. Takes click x and y in pixels. + */ + public callback(cb: (x: number, y: number) => any): Click; } class DoubleClick extends Click { - constructor(componentToListenTo: Plottable.Abstract.Component); + /** + * Creates a DoubleClickInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for clicks on. + */ + constructor(componentToListenTo: Abstract.Component); } } } @@ -944,9 +2423,9 @@ declare module Plottable { declare module Plottable { module Interaction { - class Mousemove extends Plottable.Abstract.Interaction { - constructor(componentToListenTo: Plottable.Abstract.Component); - mousemove(x: number, y: number): void; + class Mousemove extends Abstract.Interaction { + constructor(componentToListenTo: Abstract.Component); + public mousemove(x: number, y: number): void; } } } @@ -954,9 +2433,21 @@ declare module Plottable { declare module Plottable { module Interaction { - class Key extends Plottable.Abstract.Interaction { - constructor(componentToListenTo: Plottable.Abstract.Component, keyCode: number); - callback(cb: () => any): Key; + class Key extends Abstract.Interaction { + /** + * Creates a KeyInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for keypresses on. + * @param {number} keyCode The key code to listen for. + */ + constructor(componentToListenTo: Abstract.Component, keyCode: number); + /** + * Sets an callback to be called when the designated key is pressed. + * + * @param {() => any} cb: Callback to be called. + */ + public callback(cb: () => any): Key; } } } @@ -964,11 +2455,19 @@ declare module Plottable { declare module Plottable { module Interaction { - class PanZoom extends Plottable.Abstract.Interaction { - xScale: Plottable.Abstract.QuantitativeScale; - yScale: Plottable.Abstract.QuantitativeScale; - constructor(componentToListenTo: Plottable.Abstract.Component, xScale?: Plottable.Abstract.QuantitativeScale, yScale?: Plottable.Abstract.QuantitativeScale); - resetZoom(): void; + class PanZoom extends Abstract.Interaction { + public xScale: Abstract.QuantitativeScale; + public yScale: Abstract.QuantitativeScale; + /** + * Creates a PanZoomInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for interactions on. + * @param {QuantitativeScale} [xScale] The X scale to update on panning/zooming. + * @param {QuantitativeScale} [yScale] The Y scale to update on panning/zooming. + */ + constructor(componentToListenTo: Abstract.Component, xScale?: Abstract.QuantitativeScale, yScale?: Abstract.QuantitativeScale); + public resetZoom(): void; } } } @@ -976,13 +2475,47 @@ declare module Plottable { declare module Plottable { module Interaction { - class BarHover extends Plottable.Abstract.Interaction { - componentToListenTo: Plottable.Abstract.BarPlot; - constructor(barPlot: Plottable.Abstract.BarPlot); - hoverMode(): string; - hoverMode(mode: string): BarHover; - onHover(callback: (datum: any, bar: D3.Selection) => any): BarHover; - onUnhover(callback: (datum: any, bar: D3.Selection) => any): BarHover; + class BarHover extends Abstract.Interaction { + public componentToListenTo: Abstract.BarPlot; + /** + * Creates a new BarHover Interaction. + * + * @param {Abstract.BarPlot} barPlot The Bar Plot to listen for hover events on. + */ + constructor(barPlot: Abstract.BarPlot); + /** + * Gets the current hover mode. + * + * @return {string} The current hover mode. + */ + public hoverMode(): string; + /** + * Sets the hover mode for the interaction. There are two modes: + * - "point": Selects the bar under the mouse cursor (default). + * - "line" : Selects any bar that would be hit by a line extending + * in the same direction as the bar and passing through + * the cursor. + * + * @param {string} mode The desired hover mode. + * @return {BarHover} The calling Interaction.BarHover. + */ + public hoverMode(mode: string): BarHover; + /** + * Attaches an callback to be called when the user mouses over a bar. + * + * @param {(datum: any, bar: D3.Selection) => any} The callback to be called. + * The callback will be passed the data from the hovered-over bar. + * @return {BarHover} The calling Interaction.BarHover. + */ + public onHover(callback: (datum: any, bar: D3.Selection) => any): BarHover; + /** + * Attaches a callback to be called when the user mouses off of a bar. + * + * @param {(datum: any, bar: D3.Selection) => any} The callback to be called. + * The callback will be passed the data from the last-hovered bar. + * @return {BarHover} The calling Interaction.BarHover. + */ + public onUnhover(callback: (datum: any, bar: D3.Selection) => any): BarHover; } } } @@ -990,13 +2523,55 @@ declare module Plottable { declare module Plottable { module Interaction { - class Drag extends Plottable.Abstract.Interaction { - origin: number[]; - location: number[]; - callbackToCall: (dragInfo: any) => any; - constructor(componentToListenTo: Plottable.Abstract.Component); - callback(cb?: (a: any) => any): Drag; - setupZoomCallback(xScale?: Plottable.Abstract.QuantitativeScale, yScale?: Plottable.Abstract.QuantitativeScale): Drag; + class Drag extends Abstract.Interaction { + public origin: number[]; + public location: number[]; + /** + * Creates a Drag. + * + * @param {Component} componentToListenTo The component to listen for interactions on. + */ + constructor(componentToListenTo: Abstract.Component); + /** + * Gets the callback that is called when dragging starts. + * + * @returns {(startLocation: Point) => void} + */ + public dragstart(): (startLocation: Point) => void; + /** + * Sets the callback to be called when dragging starts. + * + * @param {(startLocation: Point) => any} cb The function to be called. + * @returns {Drag} + */ + public dragstart(cb: (startLocation: Point) => any): Drag; + /** + * Gets the callback that is called during dragging. + * + * @returns {(startLocation: Point, endLocation: Point) => void} + */ + public drag(): (startLocation: Point, endLocation: Point) => void; + /** + * Adds a callback to be called during dragging. + * + * @param {(startLocation: Point, endLocation: Point) => any} cb The function to be called. + * @returns {Drag} + */ + public drag(cb: (startLocation: Point, endLocation: Point) => any): Drag; + /** + * Gets the callback that is called when dragging ends. + * + * @returns {(startLocation: Point, endLocation: Point) => void} + */ + public dragend(): (startLocation: Point, endLocation: Point) => void; + /** + * Adds a callback to be called when the dragging ends. + * + * @param {(startLocation: Point, endLocation: Point) => any} cb The function to be called. Takes in a SelectionArea in pixels. + * @returns {Drag} The calling Drag. + */ + public dragend(cb: (startLocation: Point, endLocation: Point) => any): Drag; + public setupZoomCallback(xScale?: Abstract.QuantitativeScale, yScale?: Abstract.QuantitativeScale): Drag; } } } @@ -1005,10 +2580,15 @@ declare module Plottable { declare module Plottable { module Interaction { class DragBox extends Drag { - dragBox: D3.Selection; - boxIsDrawn: boolean; - clearBox(): DragBox; - setBox(x0: number, x1: number, y0: number, y1: number): DragBox; + public dragBox: D3.Selection; + public boxIsDrawn: boolean; + /** + * Clears the highlighted drag-selection box drawn by the AreaInteraction. + * + * @returns {AreaInteraction} The calling AreaInteraction. + */ + public clearBox(): DragBox; + public setBox(x0: number, x1: number, y0: number, y1: number): DragBox; } } } @@ -1017,7 +2597,7 @@ declare module Plottable { declare module Plottable { module Interaction { class XDragBox extends DragBox { - setBox(x0: number, x1: number): XDragBox; + public setBox(x0: number, x1: number): XDragBox; } } } @@ -1034,7 +2614,7 @@ declare module Plottable { declare module Plottable { module Interaction { class YDragBox extends DragBox { - setBox(y0: number, y1: number): YDragBox; + public setBox(y0: number, y1: number): YDragBox; } } } @@ -1043,11 +2623,37 @@ declare module Plottable { declare module Plottable { module Abstract { class Dispatcher extends PlottableObject { + /** + * Creates a Dispatcher with the specified target. + * + * @param {D3.Selection} target The selection to listen for events on. + */ constructor(target: D3.Selection); - target(): D3.Selection; - target(targetElement: D3.Selection): Dispatcher; - connect(): Dispatcher; - disconnect(): Dispatcher; + /** + * Gets the target of the Dispatcher. + * + * @returns {D3.Selection} The Dispatcher's current target. + */ + public target(): D3.Selection; + /** + * Sets the target of the Dispatcher. + * + * @param {D3.Selection} target The element to listen for updates on. + * @returns {Dispatcher} The calling Dispatcher. + */ + public target(targetElement: D3.Selection): Dispatcher; + /** + * Attaches the Dispatcher's listeners to the Dispatcher's target element. + * + * @returns {Dispatcher} The calling Dispatcher. + */ + public connect(): Dispatcher; + /** + * Detaches the Dispatcher's listeners from the Dispatchers' target element. + * + * @returns {Dispatcher} The calling Dispatcher. + */ + public disconnect(): Dispatcher; } } } @@ -1055,14 +2661,55 @@ declare module Plottable { declare module Plottable { module Dispatcher { - class Mouse extends Plottable.Abstract.Dispatcher { + class Mouse extends Abstract.Dispatcher { + /** + * Creates a Mouse Dispatcher with the specified target. + * + * @param {D3.Selection} target The selection to listen for events on. + */ constructor(target: D3.Selection); - mouseover(): (location: Point) => any; - mouseover(callback: (location: Point) => any): Mouse; - mousemove(): (location: Point) => any; - mousemove(callback: (location: Point) => any): Mouse; - mouseout(): (location: Point) => any; - mouseout(callback: (location: Point) => any): Mouse; + /** + * Gets the current callback to be called on mouseover. + * + * @return {(location: Point) => any} The current mouseover callback. + */ + public mouseover(): (location: Point) => any; + /** + * Attaches a callback to be called on mouseover. + * + * @param {(location: Point) => any} callback A function that takes the pixel position of the mouse event. + * Pass in null to remove the callback. + * @return {Mouse} The calling Mouse Handler. + */ + public mouseover(callback: (location: Point) => any): Mouse; + /** + * Gets the current callback to be called on mousemove. + * + * @return {(location: Point) => any} The current mousemove callback. + */ + public mousemove(): (location: Point) => any; + /** + * Attaches a callback to be called on mousemove. + * + * @param {(location: Point) => any} callback A function that takes the pixel position of the mouse event. + * Pass in null to remove the callback. + * @return {Mouse} The calling Mouse Handler. + */ + public mousemove(callback: (location: Point) => any): Mouse; + /** + * Gets the current callback to be called on mouseout. + * + * @return {(location: Point) => any} The current mouseout callback. + */ + public mouseout(): (location: Point) => any; + /** + * Attaches a callback to be called on mouseout. + * + * @param {(location: Point) => any} callback A function that takes the pixel position of the mouse event. + * Pass in null to remove the callback. + * @return {Mouse} The calling Mouse Handler. + */ + public mouseout(callback: (location: Point) => any): Mouse; } } } @@ -1070,22 +2717,22 @@ declare module Plottable { declare module Plottable { module Template { - class StandardChart extends Plottable.Component.Table { + class StandardChart extends Component.Table { constructor(); - yAxis(y: Plottable.Abstract.Axis): StandardChart; - yAxis(): Plottable.Abstract.Axis; - xAxis(x: Plottable.Abstract.Axis): StandardChart; - xAxis(): Plottable.Abstract.Axis; - yLabel(y: Plottable.Component.AxisLabel): StandardChart; - yLabel(y: string): StandardChart; - yLabel(): Plottable.Component.AxisLabel; - xLabel(x: Plottable.Component.AxisLabel): StandardChart; - xLabel(x: string): StandardChart; - xLabel(): Plottable.Component.AxisLabel; - titleLabel(x: Plottable.Component.TitleLabel): StandardChart; - titleLabel(x: string): StandardChart; - titleLabel(): Plottable.Component.TitleLabel; - center(c: Plottable.Abstract.Component): StandardChart; + public yAxis(y: Abstract.Axis): StandardChart; + public yAxis(): Abstract.Axis; + public xAxis(x: Abstract.Axis): StandardChart; + public xAxis(): Abstract.Axis; + public yLabel(y: Component.AxisLabel): StandardChart; + public yLabel(y: string): StandardChart; + public yLabel(): Component.AxisLabel; + public xLabel(x: Component.AxisLabel): StandardChart; + public xLabel(x: string): StandardChart; + public xLabel(): Component.AxisLabel; + public titleLabel(x: Component.TitleLabel): StandardChart; + public titleLabel(x: string): StandardChart; + public titleLabel(): Component.TitleLabel; + public center(c: Abstract.Component): StandardChart; } } } diff --git a/plottable.js b/plottable.js index 506e61cd7c..720ac54552 100644 --- a/plottable.js +++ b/plottable.js @@ -4,32 +4,65 @@ Copyright 2014 Palantir Technologies Licensed under MIT (https://github.com/palantir/plottable/blob/master/LICENSE) */ +/// var Plottable; (function (Plottable) { (function (Util) { (function (Methods) { + /** + * Checks if x is between a and b. + * + * @param {number} x The value to test if in range + * @param {number} a The beginning of the (inclusive) range + * @param {number} b The ending of the (inclusive) range + * @return {boolean} Whether x is in [a, b] + */ function inRange(x, a, b) { return (Math.min(a, b) <= x && x <= Math.max(a, b)); } Methods.inRange = inRange; + + /** Print a warning message to the console, if it is available. + * + * @param {string} The warnings to print + */ function warn(warning) { + /* tslint:disable:no-console */ if (window.console != null) { if (window.console.warn != null) { console.warn(warning); - } - else if (window.console.log != null) { + } else if (window.console.log != null) { console.log(warning); } } + /* tslint:enable:no-console */ } Methods.warn = warn; + + /** + * Takes two arrays of numbers and adds them together + * + * @param {number[]} alist The first array of numbers + * @param {number[]} blist The second array of numbers + * @return {number[]} An array of numbers where x[i] = alist[i] + blist[i] + */ function addArrays(alist, blist) { if (alist.length !== blist.length) { throw new Error("attempted to add arrays of unequal length"); } - return alist.map(function (_, i) { return alist[i] + blist[i]; }); + return alist.map(function (_, i) { + return alist[i] + blist[i]; + }); } Methods.addArrays = addArrays; + + /** + * Takes two sets and returns the intersection + * + * @param {D3.Set} set1 The first set + * @param {D3.Set} set2 The second set + * @return {D3.Set} A set that contains elements that appear in both set1 and set2 + */ function intersection(set1, set2) { var set = d3.set(); set1.forEach(function (v) { @@ -40,37 +73,66 @@ var Plottable; return set; } Methods.intersection = intersection; + + /** + * Take an accessor object (may be a string to be made into a key, or a value, or a color code) + * and "activate" it by turning it into a function in (datum, index, metadata) + */ 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; }; + } else if (typeof (accessor) === "string" && accessor[0] !== "#") { + return function (d, i, s) { + return d[accessor]; + }; + } else { + return function (d, i, s) { + return accessor; + }; } ; } Methods._accessorize = _accessorize; + + /** + * Takes two sets and returns the union + * + * @param{D3.Set} set1 The first set + * @param{D3.Set} set2 The second set + * @return{D3.Set} A set that contains elements that appear in either set1 or set2 + */ function union(set1, set2) { var set = d3.set(); - set1.forEach(function (v) { return set.add(v); }); - set2.forEach(function (v) { return set.add(v); }); + set1.forEach(function (v) { + return set.add(v); + }); + set2.forEach(function (v) { + return set.add(v); + }); return set; } Methods.union = union; + + /** + * Take an accessor object, activate it, and partially apply it to a Plot's datasource's metadata + */ function _applyAccessor(accessor, plot) { var activatedAccessor = _accessorize(accessor); - return function (d, i) { return activatedAccessor(d, i, plot.dataSource().metadata()); }; + return function (d, i) { + return activatedAccessor(d, i, plot.dataSource().metadata()); + }; } Methods._applyAccessor = _applyAccessor; + function uniq(strings) { var seen = {}; - strings.forEach(function (s) { return seen[s] = true; }); + strings.forEach(function (s) { + return seen[s] = true; + }); return d3.keys(seen); } Methods.uniq = uniq; + function uniqNumbers(a) { var seen = d3.set(); var result = []; @@ -83,6 +145,14 @@ var Plottable; return result; } Methods.uniqNumbers = uniqNumbers; + + /** + * Creates an array of length `count`, filled with value or (if value is a function), value() + * + * @param {any} value The value to fill the array with, or, if a function, a generator for values + * @param {number} count The length of the array to generate + * @return {any[]} + */ function createFilledArray(value, count) { var out = []; for (var i = 0; i < count; i++) { @@ -91,11 +161,21 @@ var Plottable; return out; } Methods.createFilledArray = createFilledArray; + + /** + * @param {T[][]} a The 2D array that will have its elements joined together. + * @return {T[]} Every array in a, concatenated together in the order they appear. + */ function flatten(a) { return Array.prototype.concat.apply([], a); } Methods.flatten = flatten; + + /** + * Check if two arrays are equal by strict equality. + */ function arrayEq(a, b) { + // Technically, null and undefined are arrays too if (a == null || b == null) { return a === b; } @@ -110,14 +190,27 @@ var Plottable; return true; } Methods.arrayEq = arrayEq; + + /** + * @param {any} a Object to check against b for equality. + * @param {any} b Object to check against a for equality. + * + * @returns {boolean} whether or not two objects share the same keys, and + * values associated with those keys. Values will be compared + * with ===. + */ function objEq(a, b) { if (a == null || b == null) { return a === b; } var keysA = Object.keys(a).sort(); var keysB = Object.keys(b).sort(); - var valuesA = keysA.map(function (k) { return a[k]; }); - var valuesB = keysB.map(function (k) { return b[k]; }); + var valuesA = keysA.map(function (k) { + return a[k]; + }); + var valuesB = keysB.map(function (k) { + return b[k]; + }); return arrayEq(keysA, keysB) && arrayEq(valuesA, valuesB); } Methods.objEq = objEq; @@ -127,20 +220,26 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// +// This file contains open source utilities, along with their copyright notices var Plottable; (function (Plottable) { (function (Util) { (function (OpenSource) { + + function sortedIndex(val, arr, accessor) { var low = 0; var high = arr.length; while (low < high) { + /* tslint:disable:no-bitwise */ var mid = (low + high) >>> 1; + + /* tslint:enable:no-bitwise */ var x = accessor == null ? arr[mid] : accessor(arr[mid]); if (x < val) { low = mid + 1; - } - else { + } else { high = mid; } } @@ -154,6 +253,7 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { @@ -166,14 +266,17 @@ var Plottable; 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]; @@ -185,13 +288,26 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { + /** + * 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 Key to set in the store + * @param {any} value Value to set in the store + * @return {boolean} True if key already in store, false otherwise + */ StrictEqualityAssociativeArray.prototype.set = function (key, value) { if (key !== key) { throw new Error("NaN may not be used as a key to the StrictEqualityAssociativeArray"); @@ -205,6 +321,13 @@ var Plottable; this.keyValuePairs.push([key, value]); return false; }; + + /** + * Get a value from the store, given a key. + * + * @param {any} key Key associated with value to retrieve + * @return {any} Value if found, undefined otherwise + */ StrictEqualityAssociativeArray.prototype.get = function (key) { for (var i = 0; i < this.keyValuePairs.length; i++) { if (this.keyValuePairs[i][0] === key) { @@ -213,6 +336,16 @@ var Plottable; } return undefined; }; + + /** + * Test whether store has a value associated with given key. + * + * Will return true if there is a key/value entry, + * even if the value is explicitly `undefined`. + * + * @param {any} key Key to test for presence of an entry + * @return {boolean} Whether there was a matching entry for that key + */ StrictEqualityAssociativeArray.prototype.has = function (key) { for (var i = 0; i < this.keyValuePairs.length; i++) { if (this.keyValuePairs[i][0] === key) { @@ -221,17 +354,47 @@ var Plottable; } return false; }; + + /** + * Return an array of the values in the key-value store + * + * @return {any[]} The values in the store + */ StrictEqualityAssociativeArray.prototype.values = function () { - return this.keyValuePairs.map(function (x) { return x[1]; }); + return this.keyValuePairs.map(function (x) { + return x[1]; + }); }; + + /** + * Return an array of keys in the key-value store + * + * @return {any[]} The keys in the store + */ StrictEqualityAssociativeArray.prototype.keys = function () { - return this.keyValuePairs.map(function (x) { return x[0]; }); + return this.keyValuePairs.map(function (x) { + return x[0]; + }); }; + + /** + * Execute a callback for each entry in the array. + * + * @param {(key: any, val?: any, index?: number) => any} callback The callback to eecute + * @return {any[]} The results of mapping the callback over the entries + */ StrictEqualityAssociativeArray.prototype.map = function (cb) { return this.keyValuePairs.map(function (kv, index) { return cb(kv[0], kv[1], index); }); }; + + /** + * Delete a key from the key-value store. Return whether the key was present. + * + * @param {any} The key to remove + * @return {boolean} Whether a matching entry was found and removed + */ StrictEqualityAssociativeArray.prototype.delete = function (key) { for (var i = 0; i < this.keyValuePairs.length; i++) { if (this.keyValuePairs[i][0] === key) { @@ -248,12 +411,26 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { var Cache = (function () { + /** + * @constructor + * + * @param {string} compute The function whose results will be cached. + * @param {string} [canonicalKey] If present, when clear() is called, + * this key will be re-computed. If its result hasn't been changed, + * the cache will not be cleared. + * @param {(v: T, w: T) => boolean} [valueEq] + * Used to determine if the value of canonicalKey has changed. + * If omitted, defaults to === comparision. + */ function Cache(compute, canonicalKey, valueEq) { - if (valueEq === void 0) { valueEq = function (v, w) { return v === w; }; } + if (typeof valueEq === "undefined") { valueEq = function (v, w) { + return v === w; + }; } this.cache = d3.map(); this.canonicalKey = null; this.compute = compute; @@ -263,12 +440,29 @@ var Plottable; this.cache.set(this.canonicalKey, this.compute(this.canonicalKey)); } } + /** + * Attempt to look up k in the cache, computing the result if it isn't + * found. + * + * @param {string} k The key to look up in the cache. + * @return {T} The value associated with k; the result of compute(k). + */ Cache.prototype.get = function (k) { if (!this.cache.has(k)) { this.cache.set(k, this.compute(k)); } return this.cache.get(k); }; + + /** + * Reset the cache empty. + * + * If canonicalKey was provided at construction, compute(canonicalKey) + * will be re-run. If the result matches what is already in the cache, + * it will not clear the cache. + * + * @return {Cache} The calling Cache. + */ Cache.prototype.clear = function () { if (this.canonicalKey === undefined || !this.valueEq(this.cache.get(this.canonicalKey), this.compute(this.canonicalKey))) { this.cache = d3.map(); @@ -282,13 +476,25 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { (function (Text) { Text.HEIGHT_TEXT = "bqpdl"; + ; + ; + + /** + * Returns a quasi-pure function of typesignature (t: string) => Dimensions which measures height and width of text + * in the given text selection + * + * @param {D3.Selection} selection: A temporary text selection that the string will be placed into for measurement. + * Will be removed on function creation and appended only for measurement. + * @returns {Dimensions} width and height of the text + */ function getTextMeasurer(selection) { var parentNode = selection.node().parentNode; selection.remove(); @@ -304,19 +510,45 @@ var Plottable; }; } Text.getTextMeasurer = getTextMeasurer; + + /** + * @return {TextMeasurer} A test measurer that will treat all sequences + * of consecutive whitespace as a single " ". + */ function combineWhitespace(tm) { - return function (s) { return tm(s.replace(/\s+/g, " ")); }; + return function (s) { + return tm(s.replace(/\s+/g, " ")); + }; } + + /** + * Returns a text measure that measures each individual character of the + * string with tm, then combines all the individual measurements. + */ function measureByCharacter(tm) { return function (s) { var whs = s.trim().split("").map(tm); return { - width: d3.sum(whs, function (wh) { return wh.width; }), - height: d3.max(whs, function (wh) { return wh.height; }) + width: d3.sum(whs, function (wh) { + return wh.width; + }), + height: d3.max(whs, function (wh) { + return wh.height; + }) }; }; } + var CANONICAL_CHR = "a"; + + /** + * Some TextMeasurers get confused when measuring something that's only + * whitespace: only whitespace in a dom node takes up 0 x 0 space. + * + * @return {TextMeasurer} A function that if its argument is all + * whitespace, it will wrap its argument in CANONICAL_CHR before + * measuring in order to get a non-zero size of the whitespace. + */ function wrapWhitespace(tm) { return function (s) { if (/^\s*$/.test(s)) { @@ -329,21 +561,40 @@ var Plottable; }; }); return { - width: d3.sum(whs, function (x) { return x.width; }), - height: d3.max(whs, function (x) { return x.height; }) + width: d3.sum(whs, function (x) { + return x.width; + }), + height: d3.max(whs, function (x) { + return x.height; + }) }; - } - else { + } else { return tm(s); } }; } + + /** + * This class will measure text by measuring each character individually, + * then adding up the dimensions. It will also cache the dimensions of each + * letter. + */ var CachingCharacterMeasurer = (function () { + /** + * @param {D3.Selection} textSelection The element that will have text inserted into + * it in order to measure text. The styles present for text in + * this element will to the text being measured. + */ function CachingCharacterMeasurer(textSelection) { var _this = this; this.cache = new Util.Cache(getTextMeasurer(textSelection), CANONICAL_CHR, Util.Methods.objEq); - this.measure = combineWhitespace(measureByCharacter(wrapWhitespace(function (s) { return _this.cache.get(s); }))); + this.measure = combineWhitespace(measureByCharacter(wrapWhitespace(function (s) { + return _this.cache.get(s); + }))); } + /** + * Clear the cache, if it seems that the text has changed size. + */ CachingCharacterMeasurer.prototype.clear = function () { this.cache.clear(); return this; @@ -351,18 +602,33 @@ var Plottable; return CachingCharacterMeasurer; })(); Text.CachingCharacterMeasurer = CachingCharacterMeasurer; + + /** + * Gets a truncated version of a sting that fits in the available space, given the element in which to draw the text + * + * @param {string} text: The string to be truncated + * @param {number} availableWidth: The available width, in pixels + * @param {D3.Selection} element: The text element used to measure the text + * @returns {string} text - the shortened text + */ function getTruncatedText(text, availableWidth, measurer) { if (measurer(text).width <= availableWidth) { return text; - } - else { + } else { return _addEllipsesToLine(text, availableWidth, measurer); } } Text.getTruncatedText = getTruncatedText; + + /** + * Takes a line, a width to fit it in, and a text measurer. Will attempt to add ellipses to the end of the line, + * shortening the line as required to ensure that it fits within width. + */ function _addEllipsesToLine(line, width, measureText) { var mutatedLine = line.trim(); - var widthMeasure = function (s) { return measureText(s).width; }; + var widthMeasure = function (s) { + return measureText(s).width; + }; var lineWidth = widthMeasure(line); var ellipsesWidth = widthMeasure("..."); if (width < ellipsesWidth) { @@ -380,9 +646,10 @@ var Plottable; return mutatedLine + "..."; } Text._addEllipsesToLine = _addEllipsesToLine; + function writeLineHorizontally(line, g, width, height, xAlign, yAlign) { - if (xAlign === void 0) { xAlign = "left"; } - if (yAlign === void 0) { yAlign = "top"; } + if (typeof xAlign === "undefined") { xAlign = "left"; } + if (typeof yAlign === "undefined") { yAlign = "top"; } var xOffsetFactor = { left: 0, center: 0.5, right: 1 }; var yOffsetFactor = { top: 0, center: 0.5, bottom: 1 }; if (xOffsetFactor[xAlign] === undefined || yOffsetFactor[yAlign] === undefined) { @@ -409,10 +676,11 @@ var Plottable; return { width: w, height: h }; } Text.writeLineHorizontally = writeLineHorizontally; + function writeLineVertically(line, g, width, height, xAlign, yAlign, rotation) { - if (xAlign === void 0) { xAlign = "left"; } - if (yAlign === void 0) { yAlign = "top"; } - if (rotation === void 0) { rotation = "right"; } + if (typeof xAlign === "undefined") { xAlign = "left"; } + if (typeof yAlign === "undefined") { yAlign = "top"; } + if (typeof rotation === "undefined") { rotation = "right"; } if (rotation !== "right" && rotation !== "left") { throw new Error("unrecognized rotation: " + rotation); } @@ -426,12 +694,14 @@ var Plottable; xForm.rotate = rotation === "right" ? 90 : -90; xForm.translate = [isRight ? width : 0, isRight ? 0 : height]; innerG.attr("transform", xForm.toString()); + return wh; } Text.writeLineVertically = writeLineVertically; + function writeTextHorizontally(brokenText, g, width, height, xAlign, yAlign) { - if (xAlign === void 0) { xAlign = "left"; } - if (yAlign === void 0) { yAlign = "top"; } + if (typeof xAlign === "undefined") { xAlign = "left"; } + if (typeof yAlign === "undefined") { yAlign = "top"; } var h = getTextMeasurer(g.append("text"))(Text.HEIGHT_TEXT).height; var maxWidth = 0; var blockG = g.append("g"); @@ -449,10 +719,11 @@ var Plottable; Util.DOM.translate(blockG, 0, freeSpace * translator[yAlign]); return { width: maxWidth, height: usedSpace }; } + function writeTextVertically(brokenText, g, width, height, xAlign, yAlign, rotation) { - if (xAlign === void 0) { xAlign = "left"; } - if (yAlign === void 0) { yAlign = "top"; } - if (rotation === void 0) { rotation = "left"; } + if (typeof xAlign === "undefined") { xAlign = "left"; } + if (typeof yAlign === "undefined") { yAlign = "top"; } + if (typeof rotation === "undefined") { rotation = "left"; } var h = getTextMeasurer(g.append("text"))(Text.HEIGHT_TEXT).height; var maxHeight = 0; var blockG = g.append("g"); @@ -468,31 +739,49 @@ var Plottable; var freeSpace = width - usedSpace; var translator = { center: 0.5, left: 0, right: 1 }; Util.DOM.translate(blockG, freeSpace * translator[xAlign], 0); + return { width: usedSpace, height: maxHeight }; } + ; + + /** + * @param {write} [IWriteOptions] If supplied, the text will be written + * To the given g. Will align the text vertically if it seems like + * that is appropriate. + * Returns an IWriteTextResult with info on whether the text fit, and how much width/height was used. + */ function writeText(text, width, height, tm, horizontally, write) { var orientHorizontally = (horizontally != null) ? horizontally : width * 1.1 > height; var primaryDimension = orientHorizontally ? width : height; var secondaryDimension = orientHorizontally ? height : width; var wrappedText = Util.WordWrap.breakTextToFitRect(text, primaryDimension, secondaryDimension, tm); + if (wrappedText.lines.length === 0) { return { textFits: wrappedText.textFits, usedWidth: 0, usedHeight: 0 }; } + var usedWidth, usedHeight; if (write == null) { var widthFn = orientHorizontally ? d3.max : d3.sum; var heightFn = orientHorizontally ? d3.sum : d3.max; - usedWidth = widthFn(wrappedText.lines, function (line) { return tm(line).width; }); - usedHeight = heightFn(wrappedText.lines, function (line) { return tm(line).height; }); - } - else { + usedWidth = widthFn(wrappedText.lines, function (line) { + return tm(line).width; + }); + usedHeight = heightFn(wrappedText.lines, function (line) { + return tm(line).height; + }); + } else { var innerG = write.g.append("g").classed("writeText-inner-g", true); + + // the outerG contains general transforms for positining the whole block, the inner g + // will contain transforms specific to orienting the text properly within the block. var writeTextFn = orientHorizontally ? writeTextHorizontally : writeTextVertically; var wh = writeTextFn(wrappedText.lines, innerG, width, height, write.xAlign, write.yAlign); usedWidth = wh.width; usedHeight = wh.height; } + return { textFits: wrappedText.textFits, usedWidth: usedWidth, usedHeight: usedHeight }; } Text.writeText = writeText; @@ -502,6 +791,7 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { @@ -509,9 +799,17 @@ var Plottable; var LINE_BREAKS_BEFORE = /[{\[]/; var LINE_BREAKS_AFTER = /[!"%),-.:;?\]}]/; var SPACES = /^\s+$/; + ; + + /** + * Takes a block of text, a width and height to fit it in, and a 2-d text measurement function. + * Wraps words and fits as much of the text as possible into the given width and height. + */ function breakTextToFitRect(text, width, height, measureText) { - var widthMeasure = function (s) { return measureText(s).width; }; + var widthMeasure = function (s) { + return measureText(s).width; + }; var lines = breakTextToFitWidth(text, width, widthMeasure); var textHeight = measureText("hello world").height; var nLinesThatFit = Math.floor(height / textHeight); @@ -519,12 +817,19 @@ var Plottable; if (!textFit) { lines = lines.splice(0, nLinesThatFit); if (nLinesThatFit > 0) { + // Overwrite the last line to one that has had a ... appended to the end lines[nLinesThatFit - 1] = Util.Text._addEllipsesToLine(lines[nLinesThatFit - 1], width, measureText); } } return { originalText: text, lines: lines, textFits: textFit }; } WordWrap.breakTextToFitRect = breakTextToFitRect; + + /** + * Splits up the text so that it will fit in width (or splits into a list of single characters if it is impossible + * to fit in width). Tries to avoid breaking words on non-linebreak-or-space characters, and will only break a word if + * the word is too big to fit within width on its own. + */ function breakTextToFitWidth(text, width, widthMeasure) { var ret = []; var paragraphs = text.split("\n"); @@ -532,13 +837,18 @@ var Plottable; var paragraph = paragraphs[i]; if (paragraph !== null) { ret = ret.concat(breakParagraphToFitWidth(paragraph, width, widthMeasure)); - } - else { + } else { ret.push(""); } } return ret; } + + /** + * Determines if it is possible to fit a given text within width without breaking any of the words. + * Simple algorithm, split the text up into tokens, and make sure that the widest token doesn't exceed + * allowed width. + */ function canWrapWithoutBreakingWords(text, width, widthMeasure) { var tokens = tokenize(text); var widths = tokens.map(widthMeasure); @@ -546,6 +856,13 @@ var Plottable; return maxWidth <= width; } WordWrap.canWrapWithoutBreakingWords = canWrapWithoutBreakingWords; + + /** + * A paragraph is a string of text containing no newlines. + * Given a paragraph, break it up into lines that are no + * wider than width. widthMeasure is a function that takes + * text as input, and returns the width of the text in pixels. + */ function breakParagraphToFitWidth(text, width, widthMeasure) { var lines = []; var tokens = tokenize(text); @@ -557,8 +874,10 @@ var Plottable; nextToken = tokens[i++]; } var brokenToken = breakNextTokenToFitInWidth(curLine, nextToken, width, widthMeasure); + var canAdd = brokenToken[0]; var leftOver = brokenToken[1]; + if (canAdd !== null) { curLine += canAdd; } @@ -573,6 +892,15 @@ var Plottable; } return lines; } + + /** + * Breaks up the next token and so that some part of it can be + * added to curLine and fits in the width. the return value + * is an array with 2 elements, the part that can be added + * and the left over part of the token + * widthMeasure is a function that takes text as input, + * and returns the width of the text in pixels. + */ function breakNextTokenToFitInWidth(curLine, nextToken, width, widthMeasure) { if (isBlank(nextToken)) { return [nextToken, null]; @@ -587,8 +915,7 @@ var Plottable; while (i < nextToken.length) { if (widthMeasure(curLine + nextToken[i] + "-") <= width) { curLine += nextToken[i++]; - } - else { + } else { break; } } @@ -599,6 +926,15 @@ var Plottable; } return [nextToken.substring(0, i) + append, nextToken.substring(i)]; } + + /** + * Breaks up into tokens for word wrapping + * Each token is comprised of either: + * 1) Only word and non line break characters + * 2) Only spaces characters + * 3) Line break characters such as ":" or ";" or "," + * (will be single character token, unless there is a repeated linebreak character) + */ function tokenize(text) { var ret = []; var token = ""; @@ -607,8 +943,7 @@ var Plottable; var curChar = text[i]; if (token === "" || isTokenizedTogether(token[0], curChar, lastChar)) { token += curChar; - } - else { + } else { ret.push(token); token = curChar; } @@ -619,17 +954,31 @@ var Plottable; } return ret; } + + /** + * Returns whether a string is blank. + * + * @param {string} str: The string to test for blank-ness + * @returns {boolean} Whether the string is blank + */ function isBlank(text) { return text == null ? true : text.trim() === ""; } + + /** + * Given a token (ie a string of characters that are similar and shouldn't be broken up) and a character, determine + * whether that character should be added to the token. Groups of characters that don't match the space or line break + * regex are always tokenzied together. Spaces are always tokenized together. Line break characters are almost always + * split into their own token, except that two subsequent identical line break characters are put into the same token. + * For isTokenizedTogether(":", ",") == False but isTokenizedTogether("::") == True. + */ function isTokenizedTogether(text, nextChar, lastChar) { if (!(text && nextChar)) { false; } if (SPACES.test(text) && SPACES.test(nextChar)) { return true; - } - else if (SPACES.test(text) || SPACES.test(nextChar)) { + } else if (SPACES.test(text) || SPACES.test(nextChar)) { return false; } if (LINE_BREAKS_AFTER.test(lastChar) || LINE_BREAKS_BEFORE.test(nextChar)) { @@ -647,20 +996,26 @@ var Plottable; (function (Plottable) { (function (Util) { (function (DOM) { + /** + * Gets the bounding box of an element. + * @param {D3.Selection} element + * @returns {SVGRed} The bounding box. + */ function getBBox(element) { return element.node().getBBox(); } DOM.getBBox = getBBox; + DOM.POLYFILL_TIMEOUT_MSEC = 1000 / 60; function requestAnimationFramePolyfill(fn) { if (window.requestAnimationFrame != null) { window.requestAnimationFrame(fn); - } - else { + } else { setTimeout(fn, DOM.POLYFILL_TIMEOUT_MSEC); } } DOM.requestAnimationFramePolyfill = requestAnimationFramePolyfill; + function _getParsedStyleValue(style, prop) { var value = style.getPropertyValue(prop); var parsedValue = parseFloat(value); @@ -669,6 +1024,8 @@ var Plottable; } return parsedValue; } + + // function isSelectionRemovedFromSVG(selection) { var n = selection.node(); while (n !== null && n.nodeName !== "svg") { @@ -677,20 +1034,25 @@ var Plottable; return (n == null); } DOM.isSelectionRemovedFromSVG = isSelectionRemovedFromSVG; + 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"); } DOM.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"); } DOM.getElementHeight = getElementHeight; + function getSVGPixelWidth(svg) { var width = svg.node().clientWidth; + if (width === 0) { var widthAttr = svg.attr("width"); + if (widthAttr.indexOf("%") !== -1) { var ancestorNode = svg.node().parentNode; while (ancestorNode != null && ancestorNode.clientWidth === 0) { @@ -700,20 +1062,20 @@ var Plottable; throw new Error("Could not compute width of element"); } width = ancestorNode.clientWidth * parseFloat(widthAttr) / 100; - } - else { + } else { width = parseFloat(widthAttr); } } + return width; } DOM.getSVGPixelWidth = getSVGPixelWidth; + function translate(s, x, y) { var xform = d3.transform(s.attr("transform")); if (x == null) { return xform.translate; - } - else { + } else { y = (y == null) ? 0 : y; xform.translate[0] = x; xform.translate[1] = y; @@ -722,6 +1084,7 @@ var Plottable; } } DOM.translate = translate; + function boxesOverlap(boxA, boxB) { if (boxA.right < boxB.left) { return false; @@ -744,17 +1107,29 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { Plottable.MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000; + var Formatters = (function () { function Formatters() { } + /** + * Creates a formatter for currency values. + * + * @param {number} [precision] The number of decimal places to show (default 2). + * @param {string} [symbol] The currency symbol to use (default "$"). + * @param {boolean} [prefix] Whether to prepend or append the currency symbol (default true). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for currency values. + */ Formatters.currency = function (precision, symbol, prefix, onlyShowUnchanged) { - if (precision === void 0) { precision = 2; } - if (symbol === void 0) { symbol = "$"; } - if (prefix === void 0) { prefix = true; } - if (onlyShowUnchanged === void 0) { onlyShowUnchanged = true; } + if (typeof precision === "undefined") { precision = 2; } + if (typeof symbol === "undefined") { symbol = "$"; } + if (typeof prefix === "undefined") { prefix = true; } + if (typeof onlyShowUnchanged === "undefined") { onlyShowUnchanged = true; } var fixedFormatter = Formatters.fixed(precision); return function (d) { var formattedValue = fixedFormatter(Math.abs(d)); @@ -764,10 +1139,10 @@ var Plottable; if (formattedValue !== "") { if (prefix) { formattedValue = symbol + formattedValue; - } - else { + } else { formattedValue += symbol; } + if (d < 0) { formattedValue = "-" + formattedValue; } @@ -775,9 +1150,18 @@ var Plottable; return formattedValue; }; }; + + /** + * Creates a formatter that displays exactly [precision] decimal places. + * + * @param {number} [precision] The number of decimal places to show (default 3). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter that displays exactly [precision] decimal places. + */ Formatters.fixed = function (precision, onlyShowUnchanged) { - if (precision === void 0) { precision = 3; } - if (onlyShowUnchanged === void 0) { onlyShowUnchanged = true; } + if (typeof precision === "undefined") { precision = 3; } + if (typeof onlyShowUnchanged === "undefined") { onlyShowUnchanged = true; } Formatters.verifyPrecision(precision); return function (d) { var formattedValue = d.toFixed(precision); @@ -787,9 +1171,19 @@ var Plottable; return formattedValue; }; }; + + /** + * Creates a formatter that formats numbers to show no more than + * [precision] decimal places. All other values are stringified. + * + * @param {number} [precision] The number of decimal places to show (default 3). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for general values. + */ Formatters.general = function (precision, onlyShowUnchanged) { - if (precision === void 0) { precision = 3; } - if (onlyShowUnchanged === void 0) { onlyShowUnchanged = true; } + if (typeof precision === "undefined") { precision = 3; } + if (typeof onlyShowUnchanged === "undefined") { onlyShowUnchanged = true; } Formatters.verifyPrecision(precision); return function (d) { if (typeof d === "number") { @@ -799,26 +1193,44 @@ var Plottable; return ""; } return formattedValue; - } - else { + } else { return String(d); } }; }; + + /** + * Creates a formatter that stringifies its input. + * + * @returns {Formatter} A formatter that stringifies its input. + */ Formatters.identity = function () { return function (d) { return String(d); }; }; + + /** + * Creates a formatter for percentage values. + * Multiplies the input by 100 and appends "%". + * + * @param {number} [precision] The number of decimal places to show (default 0). + * @param {boolean} [onlyShowUnchanged] Whether to return a value if value changes after formatting (default true). + * + * @returns {Formatter} A formatter for percentage values. + */ Formatters.percentage = function (precision, onlyShowUnchanged) { - if (precision === void 0) { precision = 0; } - if (onlyShowUnchanged === void 0) { onlyShowUnchanged = true; } + if (typeof precision === "undefined") { precision = 0; } + if (typeof onlyShowUnchanged === "undefined") { onlyShowUnchanged = true; } var fixedFormatter = Formatters.fixed(precision); return function (d) { var valToFormat = d * 100; + + // Account for float imprecision var valString = d.toString(); var integerPowerTen = Math.pow(10, valString.length - (valString.indexOf(".") + 1)); valToFormat = parseInt((valToFormat * integerPowerTen).toString(), 10) / integerPowerTen; + var formattedValue = fixedFormatter(valToFormat); if (onlyShowUnchanged && Formatters._valueChanged(valToFormat, formattedValue)) { return ""; @@ -829,48 +1241,84 @@ var Plottable; return formattedValue; }; }; + + /** + * Creates a formatter for values that displays [precision] significant figures + * and puts SI notation. + * + * @param {number} [precision] The number of significant figures to show (default 3). + * + * @returns {Formatter} A formatter for SI values. + */ Formatters.siSuffix = function (precision) { - if (precision === void 0) { precision = 3; } + if (typeof precision === "undefined") { precision = 3; } Formatters.verifyPrecision(precision); return function (d) { return d3.format("." + precision + "s")(d); }; }; + + /** + * Creates a formatter that displays dates. + * + * @returns {Formatter} A formatter for time/date values. + */ Formatters.time = function () { var numFormats = 8; + + // these defaults were taken from d3 + // https://github.com/mbostock/d3/wiki/Time-Formatting#format_multi var timeFormat = {}; + timeFormat[0] = { format: ".%L", - filter: function (d) { return d.getMilliseconds() !== 0; } + filter: function (d) { + return d.getMilliseconds() !== 0; + } }; timeFormat[1] = { format: ":%S", - filter: function (d) { return d.getSeconds() !== 0; } + filter: function (d) { + return d.getSeconds() !== 0; + } }; timeFormat[2] = { format: "%I:%M", - filter: function (d) { return d.getMinutes() !== 0; } + filter: function (d) { + return d.getMinutes() !== 0; + } }; timeFormat[3] = { format: "%I %p", - filter: function (d) { return d.getHours() !== 0; } + filter: function (d) { + return d.getHours() !== 0; + } }; timeFormat[4] = { format: "%a %d", - filter: function (d) { return d.getDay() !== 0 && d.getDate() !== 1; } + filter: function (d) { + return d.getDay() !== 0 && d.getDate() !== 1; + } }; timeFormat[5] = { format: "%b %d", - filter: function (d) { return d.getDate() !== 1; } + filter: function (d) { + return d.getDate() !== 1; + } }; timeFormat[6] = { format: "%b", - filter: function (d) { return d.getMonth() !== 0; } + filter: function (d) { + return d.getMonth() !== 0; + } }; timeFormat[7] = { format: "%Y", - filter: function () { return true; } + filter: function () { + return true; + } }; + return function (d) { for (var i = 0; i < numFormats; i++) { if (timeFormat[i].filter(d)) { @@ -879,20 +1327,32 @@ var Plottable; } }; }; + + /** + * Creates a formatter for relative dates. + * + * @param {number} baseValue The start date (as epoch time) used in computing relative dates (default 0) + * @param {number} increment The unit used in calculating relative date values (default MILLISECONDS_IN_ONE_DAY) + * @param {string} label The label to append to the formatted string (default "") + * + * @returns {Formatter} A formatter for time/date values. + */ Formatters.relativeDate = function (baseValue, increment, label) { - if (baseValue === void 0) { baseValue = 0; } - if (increment === void 0) { increment = Plottable.MILLISECONDS_IN_ONE_DAY; } - if (label === void 0) { label = ""; } + if (typeof baseValue === "undefined") { baseValue = 0; } + if (typeof increment === "undefined") { increment = Plottable.MILLISECONDS_IN_ONE_DAY; } + if (typeof label === "undefined") { label = ""; } return function (d) { var relativeDate = Math.round((d.valueOf() - baseValue) / increment); return relativeDate.toString() + label; }; }; + Formatters.verifyPrecision = function (precision) { if (precision < 0 || precision > 20) { throw new RangeError("Formatter precision must be between 0 and 20"); } }; + Formatters._valueChanged = function (d, formattedValue) { return d !== parseFloat(formattedValue); }; @@ -901,11 +1361,13 @@ var Plottable; Plottable.Formatters = Formatters; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { Plottable.version = "0.24.0"; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Core) { @@ -922,6 +1384,7 @@ var Plottable; Colors.CERISE_RED = "#db2e65"; Colors.BRIGHT_SUN = "#ffe43d"; Colors.JACARTA = "#2c2b6f"; + Colors.PLOTTABLE_COLORS = [ Colors.CORAL_RED, Colors.INDIGO, @@ -941,6 +1404,7 @@ var Plottable; var Core = Plottable.Core; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Abstract) { @@ -956,6 +1420,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -965,30 +1430,79 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Core) { + + + + + /** + * The Broadcaster class is owned by an IListenable. Third parties can register and deregister listeners + * from the broadcaster. When the broadcaster.broadcast method is activated, all registered callbacks are + * called. The registered callbacks are called with the registered Listenable that the broadcaster is attached + * to, along with optional arguments passed to the `broadcast` method. + * + * The listeners are called synchronously. + */ var Broadcaster = (function (_super) { __extends(Broadcaster, _super); + /** + * Construct a broadcaster, taking the Listenable that the broadcaster will be attached to. + * + * @constructor + * @param {IListenable} listenable The Listenable-object that this broadcaster is attached to. + */ function Broadcaster(listenable) { _super.call(this); this.key2callback = new Plottable.Util.StrictEqualityAssociativeArray(); this.listenable = listenable; } + /** + * Registers a callback to be called when the broadcast method is called. Also takes a key which + * is used to support deregistering the same callback later, by passing in the same key. + * If there is already a callback associated with that key, then the callback will be replaced. + * + * @param key The key associated with the callback. Key uniqueness is determined by deep equality. + * @param {IBroadcasterCallback} callback A callback to be called when the Scale's domain changes. + * @returns {Broadcaster} this object + */ Broadcaster.prototype.registerListener = function (key, callback) { this.key2callback.set(key, 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; _i++) { - args[_i - 0] = arguments[_i]; + for (var _i = 0; _i < (arguments.length - 0); _i++) { + args[_i] = arguments[_i + 0]; } - this.key2callback.values().forEach(function (callback) { return callback(_this.listenable, args); }); + this.key2callback.values().forEach(function (callback) { + return callback(_this.listenable, args); + }); return this; }; + + /** + * Deregisters the callback associated with a key. + * + * @param key The key to deregister. + * @returns {Broadcaster} this object + */ Broadcaster.prototype.deregisterListener = function (key) { this.key2callback.delete(key); return this; }; + + /** + * Deregisters all listeners and callbacks associated with the broadcaster. + * + * @returns {Broadcaster} this object + */ Broadcaster.prototype.deregisterAllListeners = function () { this.key2callback = new Plottable.Util.StrictEqualityAssociativeArray(); }; @@ -999,6 +1513,7 @@ var Plottable; var Core = Plottable.Core; })(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; } @@ -1009,9 +1524,16 @@ 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 (data === void 0) { data = []; } - if (metadata === void 0) { metadata = {}; } + if (typeof data === "undefined") { data = []; } + if (typeof metadata === "undefined") { metadata = {}; } _super.call(this); this.broadcaster = new Plottable.Core.Broadcaster(this); this._data = data; @@ -1021,25 +1543,25 @@ var Plottable; DataSource.prototype.data = function (data) { if (data == null) { return this._data; - } - else { + } else { this._data = data; this.accessor2cachedExtent = new Plottable.Util.StrictEqualityAssociativeArray(); this.broadcaster.broadcast(); return this; } }; + DataSource.prototype.metadata = function (metadata) { if (metadata == null) { return this._metadata; - } - else { + } else { this._metadata = metadata; this.accessor2cachedExtent = new Plottable.Util.StrictEqualityAssociativeArray(); this.broadcaster.broadcast(); return this; } }; + DataSource.prototype._getExtent = function (accessor) { var cachedExtent = this.accessor2cachedExtent.get(accessor); if (cachedExtent === undefined) { @@ -1048,20 +1570,18 @@ var Plottable; } return cachedExtent; }; + DataSource.prototype.computeExtent = function (accessor) { var mappedData = this._data.map(accessor); if (mappedData.length === 0) { return []; - } - else if (typeof (mappedData[0]) === "string") { + } else if (typeof (mappedData[0]) === "string") { return Plottable.Util.Methods.uniq(mappedData); - } - else { + } else { var extent = d3.extent(mappedData); if (extent[0] == null || extent[1] == null) { return []; - } - else { + } else { return extent; } } @@ -1071,6 +1591,7 @@ var Plottable; Plottable.DataSource = DataSource; })(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; } @@ -1099,25 +1620,41 @@ var Plottable; this._isAnchored = false; this.removed = false; } + /** + * Attaches the Component as a child of a given a DOM element. Usually only directly invoked on root-level Components. + * + * @param {D3.Selection} element A D3 selection consisting of the element to anchor under. + */ Component.prototype._anchor = function (element) { if (this.removed) { throw new Error("Can't reuse remove()-ed components!"); } + if (element.node().nodeName === "svg") { + // 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.isTopLevelComponent = true; } + if (this.element != null) { + // reattach existing element element.node().appendChild(this.element.node()); - } - else { + } else { this.element = element.append("g"); this._setup(); } this._isAnchored = true; }; + + /** + * Creates additional elements as necessary for the Component to function. + * Called during _anchor() if the Component's element has not been created yet. + * Override in subclasses to provide additional functionality. + */ Component.prototype._setup = function () { var _this = this; if (this._isSetup) { @@ -1127,45 +1664,67 @@ var Plottable; _this.element.classed(cssClass, true); }); this.cssClasses = null; + this.backgroundContainer = this.element.append("g").classed("background-container", true); this.content = this.element.append("g").classed("content", true); this.foregroundContainer = this.element.append("g").classed("foreground-container", true); this.boxContainer = this.element.append("g").classed("box-container", true); + if (this.clipPathEnabled) { this.generateClipPath(); } ; + this.addBox("bounding-box"); - this.interactionsToRegister.forEach(function (r) { return _this.registerInteraction(r); }); + + this.interactionsToRegister.forEach(function (r) { + return _this.registerInteraction(r); + }); this.interactionsToRegister = null; if (this.isTopLevelComponent) { this.autoResize(Component.AUTORESIZE_BY_DEFAULT); } this._isSetup = true; }; + Component.prototype._requestedSpace = function (availableWidth, availableHeight) { return { width: 0, height: 0, wantsWidth: false, wantsHeight: false }; }; + + /** + * Computes the size, position, and alignment from the specified values. + * If no parameters are supplied and the component is a root node, + * they are inferred from the size of the component's element. + * + * @param {number} xOrigin + * @param {number} yOrigin + * @param {number} availableWidth + * @param {number} availableHeight + */ Component.prototype._computeLayout = function (xOrigin, yOrigin, availableWidth, availableHeight) { var _this = this; if (xOrigin == null || yOrigin == null || availableWidth == null || availableHeight == null) { if (this.element == null) { throw new Error("anchor must be called before computeLayout"); - } - else if (this.isTopLevelComponent) { + } else if (this.isTopLevelComponent) { + // we are the root node, retrieve height/width from root SVG xOrigin = 0; yOrigin = 0; + + // Set width/height to 100% if not specified, to allow accurate size calculation + // see http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width + // and http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height if (this.rootSVG.attr("width") == null) { this.rootSVG.attr("width", "100%"); } if (this.rootSVG.attr("height") == null) { this.rootSVG.attr("height", "100%"); } + var elem = this.rootSVG.node(); availableWidth = Plottable.Util.DOM.getElementWidth(elem); availableHeight = Plottable.Util.DOM.getElementHeight(elem); - } - else { + } else { throw new Error("null arguments cannot be passed to _computeLayout() on a non-root node"); } } @@ -1173,51 +1732,71 @@ var Plottable; this.yOrigin = yOrigin; var xPosition = this.xOrigin; var yPosition = this.yOrigin; + var requestedSpace = this._requestedSpace(availableWidth, availableHeight); + xPosition += (availableWidth - requestedSpace.width) * this._xAlignProportion; xPosition += this._xOffset; if (this._isFixedWidth()) { + // Decrease size so hitbox / bounding box and children are sized correctly availableWidth = Math.min(availableWidth, requestedSpace.width); } + yPosition += (availableHeight - requestedSpace.height) * this._yAlignProportion; yPosition += this._yOffset; if (this._isFixedHeight()) { availableHeight = Math.min(availableHeight, requestedSpace.height); } + this.availableWidth = availableWidth; this.availableHeight = availableHeight; this.element.attr("transform", "translate(" + xPosition + "," + yPosition + ")"); - this.boxes.forEach(function (b) { return b.attr("width", _this.availableWidth).attr("height", _this.availableHeight); }); + this.boxes.forEach(function (b) { + return b.attr("width", _this.availableWidth).attr("height", _this.availableHeight); + }); }; + + /** + * Renders the component. + */ Component.prototype._render = function () { if (this._isAnchored && this._isSetup) { Plottable.Core.RenderController.registerToRender(this); } }; + Component.prototype._scheduleComputeLayout = function () { if (this._isAnchored && this._isSetup) { Plottable.Core.RenderController.registerToComputeLayout(this); } }; + Component.prototype._doRender = function () { + //no-op }; + Component.prototype._invalidateLayout = function () { if (this._isAnchored && this._isSetup) { if (this.isTopLevelComponent) { this._scheduleComputeLayout(); - } - else { + } else { this._parent._invalidateLayout(); } } }; + + /** + * Renders the Component into a given DOM element. + * + * @param {String|D3.Selection} element A D3 selection or a selector for getting the element to render into. + * @return {Component} The calling component. + */ Component.prototype.renderTo = function (element) { if (element != null) { var selection; if (typeof (element.node) === "function") { selection = element; - } - else { + } else { selection = d3.select(element); } this._anchor(selection); @@ -1226,6 +1805,13 @@ var Plottable; this._render(); return this; }; + + /** + * Cause the Component to recompute layout and redraw. If passed arguments, will resize the root SVG it lives in. + * + * @param {number} [availableWidth] - the width of the container element + * @param {number} [availableHeight] - the height of the container element + */ Component.prototype.resize = function (width, height) { if (!this.isTopLevelComponent) { throw new Error("Cannot resize on non top-level component"); @@ -1236,59 +1822,91 @@ var Plottable; this._invalidateLayout(); return this; }; + + /** + * Enables and disables auto-resize. + * + * If enabled, window resizes will enqueue this component for a re-layout + * and re-render. Animations are disabled during window resizes when auto- + * resize is enabled. + * + * @param {boolean} flag - Enables (true) or disables (false) auto-resize. + */ Component.prototype.autoResize = function (flag) { if (flag) { Plottable.Core.ResizeBroadcaster.register(this); - } - else { + } else { Plottable.Core.ResizeBroadcaster.deregister(this); } return this; }; + + /** + * Sets the x alignment of the Component. + * + * @param {string} alignment The x alignment of the Component (one of LEFT/CENTER/RIGHT). + * @returns {Component} The calling Component. + */ Component.prototype.xAlign = function (alignment) { alignment = alignment.toLowerCase(); if (alignment === "left") { this._xAlignProportion = 0; - } - else if (alignment === "center") { + } else if (alignment === "center") { this._xAlignProportion = 0.5; - } - else if (alignment === "right") { + } else if (alignment === "right") { this._xAlignProportion = 1; - } - else { + } else { throw new Error("Unsupported alignment"); } this._invalidateLayout(); return this; }; + + /** + * Sets the y alignment of the Component. + * + * @param {string} alignment The y alignment of the Component (one of TOP/CENTER/BOTTOM). + * @returns {Component} The calling Component. + */ Component.prototype.yAlign = function (alignment) { alignment = alignment.toLowerCase(); if (alignment === "top") { this._yAlignProportion = 0; - } - else if (alignment === "center") { + } else if (alignment === "center") { this._yAlignProportion = 0.5; - } - else if (alignment === "bottom") { + } else if (alignment === "bottom") { this._yAlignProportion = 1; - } - else { + } else { throw new Error("Unsupported alignment"); } this._invalidateLayout(); return this; }; + + /** + * Sets the x offset of the Component. + * + * @param {number} offset The desired x offset, in pixels. + * @returns {Component} The calling Component. + */ Component.prototype.xOffset = function (offset) { this._xOffset = offset; this._invalidateLayout(); return this; }; + + /** + * Sets the y offset of the Component. + * + * @param {number} offset The desired y offset, in pixels. + * @returns {Component} The calling Component. + */ Component.prototype.yOffset = function (offset) { this._yOffset = offset; this._invalidateLayout(); return this; }; + Component.prototype.addBox = function (className, parentElement) { if (this.element == null) { throw new Error("Adding boxes before anchoring is currently disallowed"); @@ -1305,37 +1923,46 @@ var Plottable; } return box; }; + Component.prototype.generateClipPath = function () { + // The clip path will prevent content from overflowing its component space. 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); }; + + /** + * Attaches an Interaction to the Component, so that the Interaction will listen for events on the Component. + * + * @param {Interaction} interaction The Interaction to attach to the Component. + * @return {Component} The calling Component. + */ Component.prototype.registerInteraction = function (interaction) { + // Interactions can be registered before or after anchoring. If registered before, they are + // pushed to this.interactionsToRegister and registered during anchoring. If after, they are + // registered immediately if (this.element != null) { if (this.hitBox == null) { this.hitBox = this.addBox("hit-box"); - this.hitBox.style("fill", "#ffffff").style("opacity", 0); + this.hitBox.style("fill", "#ffffff").style("opacity", 0); // We need to set these so Chrome will register events } interaction._anchor(this.hitBox); - } - else { + } else { this.interactionsToRegister.push(interaction); } return this; }; + Component.prototype.classed = function (cssClass, addClass) { if (addClass == null) { if (cssClass == null) { return false; - } - else if (this.element == null) { + } else if (this.element == null) { return (this.cssClasses.indexOf(cssClass) !== -1); - } - else { + } else { return this.element.classed(cssClass); } - } - else { + } else { if (cssClass == null) { return this; } @@ -1343,23 +1970,47 @@ var Plottable; var classIndex = this.cssClasses.indexOf(cssClass); if (addClass && classIndex === -1) { this.cssClasses.push(cssClass); - } - else if (!addClass && classIndex !== -1) { + } else if (!addClass && classIndex !== -1) { this.cssClasses.splice(classIndex, 1); } - } - else { + } else { this.element.classed(cssClass, addClass); } return this; } }; + + /** + * Checks if the Component has a fixed width or false if it grows to fill available space. + * Returns false by default on the base Component class. + * + * @return {boolean} Whether the component has a fixed width. + */ Component.prototype._isFixedWidth = function () { return this._fixedWidthFlag; }; + + /** + * Checks if the Component has a fixed height or false if it grows to fill available space. + * Returns false by default on the base Component class. + * + * @return {boolean} Whether the component has a fixed height. + */ Component.prototype._isFixedHeight = function () { return this._fixedHeightFlag; }; + + /** + * Merges this Component with another Component, returning a ComponentGroup. + * There are four cases: + * Component + Component: Returns a ComponentGroup with both components inside it. + * ComponentGroup + Component: Returns the ComponentGroup with the Component appended. + * Component + ComponentGroup: Returns the ComponentGroup with the Component prepended. + * ComponentGroup + ComponentGroup: Returns a new ComponentGroup with two ComponentGroups inside it. + * + * @param {Component} c The component to merge in. + * @return {ComponentGroup} + */ Component.prototype.merge = function (c) { var cg; if (this._isSetup || this._isAnchored) { @@ -1369,12 +2020,17 @@ var Plottable; cg = c; cg._addComponent(this, true); return cg; - } - else { + } else { cg = new Plottable.Component.Group([this, c]); return cg; } }; + + /** + * Detaches a Component from the DOM. The component can be reused. + * + * @returns The calling Component. + */ Component.prototype.detach = function () { if (this._isAnchored) { this.element.remove(); @@ -1386,6 +2042,11 @@ var Plottable; this._parent = null; return this; }; + + /** + * Removes a Component from the DOM and disconnects it from everything it's + * listening to (effectively destroying it). + */ Component.prototype.remove = function () { this.removed = true; this.detach(); @@ -1399,6 +2060,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -1412,16 +2074,26 @@ var Plottable; __extends(ComponentContainer, _super); function ComponentContainer() { _super.apply(this, arguments); + /* + * An abstract ComponentContainer class to encapsulate Table and ComponentGroup's shared functionality. + * It will not do anything if instantiated directly. + */ this._components = []; } ComponentContainer.prototype._anchor = function (element) { var _this = this; _super.prototype._anchor.call(this, element); - this._components.forEach(function (c) { return c._anchor(_this.content); }); + this._components.forEach(function (c) { + return c._anchor(_this.content); + }); }; + ComponentContainer.prototype._render = function () { - this._components.forEach(function (c) { return c._render(); }); + this._components.forEach(function (c) { + return c._render(); + }); }; + ComponentContainer.prototype._removeComponent = function (c) { var removeIndex = this._components.indexOf(c); if (removeIndex >= 0) { @@ -1429,15 +2101,16 @@ var Plottable; this._invalidateLayout(); } }; + ComponentContainer.prototype._addComponent = function (c, prepend) { - if (prepend === void 0) { prepend = false; } + if (typeof prepend === "undefined") { prepend = false; } if (c == null || this._components.indexOf(c) >= 0) { return false; } + if (prepend) { this._components.unshift(c); - } - else { + } else { this._components.push(c); } c._parent = this; @@ -1447,19 +2120,45 @@ var Plottable; this._invalidateLayout(); return true; }; + + /** + * Returns a list of components in the ComponentContainer + * + * @returns{Component[]} the contained Components + */ ComponentContainer.prototype.components = function () { return this._components.slice(); }; + + /** + * Returns true iff the ComponentContainer is empty. + * + * @returns {boolean} Whether the calling ComponentContainer is empty. + */ ComponentContainer.prototype.empty = function () { return this._components.length === 0; }; + + /** + * Detaches all components contained in the ComponentContainer, and + * empties the ComponentContainer. + * + * @returns {ComponentContainer} The calling ComponentContainer + */ ComponentContainer.prototype.detachAll = function () { - this._components.slice().forEach(function (c) { return c.detach(); }); + // Calling c.remove() will mutate this._components because the component will call this._parent._removeComponent(this) + // Since mutating an array while iterating over it is dangerous, we instead iterate over a copy generated by Arr.slice() + this._components.slice().forEach(function (c) { + return c.detach(); + }); return this; }; + ComponentContainer.prototype.remove = function () { _super.prototype.remove.call(this); - this._components.slice().forEach(function (c) { return c.remove(); }); + this._components.slice().forEach(function (c) { + return c.remove(); + }); }; return ComponentContainer; })(Abstract.Component); @@ -1468,6 +2167,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -1479,27 +2179,51 @@ var Plottable; (function (Component) { var Group = (function (_super) { __extends(Group, _super); + /** + * Creates a ComponentGroup. + * + * @constructor + * @param {Component[]} [components] The Components in the Group. + */ function Group(components) { - if (components === void 0) { components = []; } - _super.call(this); + if (typeof components === "undefined") { components = []; } var _this = this; + _super.call(this); this.classed("component-group", true); - components.forEach(function (c) { return _this._addComponent(c); }); + components.forEach(function (c) { + return _this._addComponent(c); + }); } Group.prototype._requestedSpace = function (offeredWidth, offeredHeight) { - var requests = this._components.map(function (c) { return c._requestedSpace(offeredWidth, offeredHeight); }); + var requests = this._components.map(function (c) { + return c._requestedSpace(offeredWidth, offeredHeight); + }); var isEmpty = this.empty(); return { - width: isEmpty ? 0 : d3.max(requests, function (request) { return request.width; }), - height: isEmpty ? 0 : d3.max(requests, function (request) { return request.height; }), - wantsWidth: isEmpty ? false : requests.map(function (r) { return r.wantsWidth; }).some(function (x) { return x; }), - wantsHeight: isEmpty ? false : requests.map(function (r) { return r.wantsHeight; }).some(function (x) { return x; }) + width: isEmpty ? 0 : d3.max(requests, function (request) { + return request.width; + }), + height: isEmpty ? 0 : d3.max(requests, function (request) { + return request.height; + }), + wantsWidth: isEmpty ? false : requests.map(function (r) { + return r.wantsWidth; + }).some(function (x) { + return x; + }), + wantsHeight: isEmpty ? false : requests.map(function (r) { + return r.wantsHeight; + }).some(function (x) { + return x; + }) }; }; + Group.prototype.merge = function (c) { this._addComponent(c); return this; }; + Group.prototype._computeLayout = function (xOrigin, yOrigin, availableWidth, availableHeight) { var _this = this; _super.prototype._computeLayout.call(this, xOrigin, yOrigin, availableWidth, availableHeight); @@ -1508,11 +2232,17 @@ var Plottable; }); return this; }; + Group.prototype._isFixedWidth = function () { - return this._components.every(function (c) { return c._isFixedWidth(); }); + return this._components.every(function (c) { + return c._isFixedWidth(); + }); }; + Group.prototype._isFixedHeight = function () { - return this._components.every(function (c) { return c._isFixedHeight(); }); + return this._components.every(function (c) { + return c._isFixedHeight(); + }); }; return Group; })(Plottable.Abstract.ComponentContainer); @@ -1521,6 +2251,7 @@ var Plottable; var Component = Plottable.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; } @@ -1531,12 +2262,20 @@ var Plottable; (function (Plottable) { (function (Component) { ; + var Table = (function (_super) { __extends(Table, _super); + /** + * Creates a Table. + * + * @constructor + * @param {Component[][]} [rows] A 2-D array of the Components to place in the table. + * null can be used if a cell is empty. + */ function Table(rows) { - if (rows === void 0) { rows = []; } - _super.call(this); + if (typeof rows === "undefined") { rows = []; } var _this = this; + _super.call(this); this.rowPadding = 0; this.colPadding = 0; this.rows = []; @@ -1551,24 +2290,35 @@ var Plottable; }); }); } + /** + * Adds a Component in the specified cell. + * + * @param {number} row The row in which to add the Component. + * @param {number} col The column in which to add the Component. + * @param {Component} component The Component to be added. + */ Table.prototype.addComponent = function (row, col, component) { if (this._addComponent(component)) { this.nRows = Math.max(row + 1, this.nRows); this.nCols = Math.max(col + 1, this.nCols); this.padTableToSize(this.nRows, this.nCols); + var currentComponent = this.rows[row][col]; if (currentComponent != null) { throw new Error("Table.addComponent cannot be called on a cell where a component already exists (for the moment)"); } + this.rows[row][col] = component; } return this; }; + Table.prototype._removeComponent = function (component) { _super.prototype._removeComponent.call(this, component); var rowpos; var colpos; - outer: for (var i = 0; i < this.nRows; i++) { + outer: + for (var i = 0; i < this.nRows; i++) { for (var j = 0; j < this.nCols; j++) { if (this.rows[i][j] === component) { rowpos = i; @@ -1577,24 +2327,63 @@ var Plottable; } } } + if (rowpos !== undefined) { this.rows[rowpos][colpos] = null; } }; + Table.prototype.iterateLayout = function (availableWidth, availableHeight) { + /* + * Given availableWidth and availableHeight, figure out how to allocate it between rows and columns using an iterative algorithm. + * + * For both dimensions, keeps track of "guaranteedSpace", which the fixed-size components have requested, and + * "proportionalSpace", which is being given to proportionally-growing components according to the weights on the table. + * Here is how it works (example uses width but it is the same for height). First, columns are guaranteed no width, and + * the free width is allocated to columns based on their colWeights. Then, in determineGuarantees, every component is + * offered its column's width and may request some amount of it, which increases that column's guaranteed + * width. If there are some components that were not satisfied with the width they were offered, and there is free + * width that has not already been guaranteed, then the remaining width is allocated to the unsatisfied columns and the + * algorithm runs again. If all components are satisfied, then the remaining width is allocated as proportional space + * according to the colWeights. + * + * The guaranteed width for each column is monotonically increasing as the algorithm iterates. Since it is deterministic + * and monotonically increasing, if the freeWidth does not change during an iteration it implies that no further progress + * is possible, so the algorithm will not continue iterating on that dimension's account. + * + * If the algorithm runs more than 5 times, we stop and just use whatever we arrived at. It's not clear under what + * circumstances this will happen or if it will happen at all. A message will be printed to the console if this occurs. + * + */ var cols = d3.transpose(this.rows); var availableWidthAfterPadding = availableWidth - this.colPadding * (this.nCols - 1); var availableHeightAfterPadding = availableHeight - this.rowPadding * (this.nRows - 1); - var rowWeights = Table.calcComponentWeights(this.rowWeights, this.rows, function (c) { return (c == null) || c._isFixedHeight(); }); - var colWeights = Table.calcComponentWeights(this.colWeights, cols, function (c) { return (c == null) || c._isFixedWidth(); }); - var heuristicColWeights = colWeights.map(function (c) { return c === 0 ? 0.5 : c; }); - var heuristicRowWeights = rowWeights.map(function (c) { return c === 0 ? 0.5 : c; }); + + var rowWeights = Table.calcComponentWeights(this.rowWeights, this.rows, function (c) { + return (c == null) || c._isFixedHeight(); + }); + var colWeights = Table.calcComponentWeights(this.colWeights, cols, function (c) { + return (c == null) || c._isFixedWidth(); + }); + + // To give the table a good starting position to iterate from, we give the fixed-width components half-weight + // so that they will get some initial space allocated to work with + var heuristicColWeights = colWeights.map(function (c) { + return c === 0 ? 0.5 : c; + }); + var heuristicRowWeights = rowWeights.map(function (c) { + return c === 0 ? 0.5 : c; + }); + var colProportionalSpace = Table.calcProportionalSpace(heuristicColWeights, availableWidthAfterPadding); var rowProportionalSpace = Table.calcProportionalSpace(heuristicRowWeights, availableHeightAfterPadding); + var guaranteedWidths = Plottable.Util.Methods.createFilledArray(0, this.nCols); var guaranteedHeights = Plottable.Util.Methods.createFilledArray(0, this.nRows); + var freeWidth; var freeHeight; + var nIterations = 0; while (true) { var offeredHeights = Plottable.Util.Methods.addArrays(guaranteedHeights, rowProportionalSpace); @@ -1602,46 +2391,68 @@ var Plottable; var guarantees = this.determineGuarantees(offeredWidths, offeredHeights); guaranteedWidths = guarantees.guaranteedWidths; guaranteedHeights = guarantees.guaranteedHeights; - var wantsWidth = guarantees.wantsWidthArr.some(function (x) { return x; }); - var wantsHeight = guarantees.wantsHeightArr.some(function (x) { return x; }); + var wantsWidth = guarantees.wantsWidthArr.some(function (x) { + return x; + }); + var wantsHeight = guarantees.wantsHeightArr.some(function (x) { + return x; + }); + var lastFreeWidth = freeWidth; var lastFreeHeight = freeHeight; freeWidth = availableWidthAfterPadding - d3.sum(guarantees.guaranteedWidths); freeHeight = availableHeightAfterPadding - d3.sum(guarantees.guaranteedHeights); var xWeights; if (wantsWidth) { - xWeights = guarantees.wantsWidthArr.map(function (x) { return x ? 0.1 : 0; }); + xWeights = guarantees.wantsWidthArr.map(function (x) { + return x ? 0.1 : 0; + }); xWeights = Plottable.Util.Methods.addArrays(xWeights, colWeights); - } - else { + } else { xWeights = colWeights; } + var yWeights; if (wantsHeight) { - yWeights = guarantees.wantsHeightArr.map(function (x) { return x ? 0.1 : 0; }); + yWeights = guarantees.wantsHeightArr.map(function (x) { + return x ? 0.1 : 0; + }); yWeights = Plottable.Util.Methods.addArrays(yWeights, rowWeights); - } - else { + } else { yWeights = rowWeights; } + colProportionalSpace = Table.calcProportionalSpace(xWeights, freeWidth); rowProportionalSpace = Table.calcProportionalSpace(yWeights, freeHeight); nIterations++; + var canImproveWidthAllocation = freeWidth > 0 && wantsWidth && freeWidth !== lastFreeWidth; var canImproveHeightAllocation = freeHeight > 0 && wantsHeight && freeHeight !== lastFreeHeight; + if (!(canImproveWidthAllocation || canImproveHeightAllocation)) { break; } + if (nIterations > 5) { break; } } + + // Redo the proportional space one last time, to ensure we use the real weights not the wantsWidth/Height weights freeWidth = availableWidthAfterPadding - d3.sum(guarantees.guaranteedWidths); freeHeight = availableHeightAfterPadding - d3.sum(guarantees.guaranteedHeights); colProportionalSpace = Table.calcProportionalSpace(colWeights, freeWidth); rowProportionalSpace = Table.calcProportionalSpace(rowWeights, freeHeight); - return { colProportionalSpace: colProportionalSpace, rowProportionalSpace: rowProportionalSpace, guaranteedWidths: guarantees.guaranteedWidths, guaranteedHeights: guarantees.guaranteedHeights, wantsWidth: wantsWidth, wantsHeight: wantsHeight }; + + return { + colProportionalSpace: colProportionalSpace, + rowProportionalSpace: rowProportionalSpace, + guaranteedWidths: guarantees.guaranteedWidths, + guaranteedHeights: guarantees.guaranteedHeights, + wantsWidth: wantsWidth, + wantsHeight: wantsHeight }; }; + Table.prototype.determineGuarantees = function (offeredWidths, offeredHeights) { var requestedWidths = Plottable.Util.Methods.createFilledArray(0, this.nCols); var requestedHeights = Plottable.Util.Methods.createFilledArray(0, this.nRows); @@ -1652,35 +2463,51 @@ var Plottable; var spaceRequest; if (component != null) { spaceRequest = component._requestedSpace(offeredWidths[colIndex], offeredHeights[rowIndex]); - } - else { + } else { spaceRequest = { width: 0, height: 0, wantsWidth: false, wantsHeight: false }; } + var allocatedWidth = Math.min(spaceRequest.width, offeredWidths[colIndex]); var allocatedHeight = Math.min(spaceRequest.height, offeredHeights[rowIndex]); + requestedWidths[colIndex] = Math.max(requestedWidths[colIndex], allocatedWidth); requestedHeights[rowIndex] = Math.max(requestedHeights[rowIndex], allocatedHeight); layoutWantsWidth[colIndex] = layoutWantsWidth[colIndex] || spaceRequest.wantsWidth; layoutWantsHeight[rowIndex] = layoutWantsHeight[rowIndex] || spaceRequest.wantsHeight; }); }); - return { guaranteedWidths: requestedWidths, guaranteedHeights: requestedHeights, wantsWidthArr: layoutWantsWidth, wantsHeightArr: layoutWantsHeight }; + return { + guaranteedWidths: requestedWidths, + guaranteedHeights: requestedHeights, + wantsWidthArr: layoutWantsWidth, + wantsHeightArr: layoutWantsHeight }; }; + Table.prototype._requestedSpace = function (offeredWidth, offeredHeight) { var layout = this.iterateLayout(offeredWidth, offeredHeight); - return { width: d3.sum(layout.guaranteedWidths), height: d3.sum(layout.guaranteedHeights), wantsWidth: layout.wantsWidth, wantsHeight: layout.wantsHeight }; + return { + width: d3.sum(layout.guaranteedWidths), + height: d3.sum(layout.guaranteedHeights), + wantsWidth: layout.wantsWidth, + wantsHeight: layout.wantsHeight }; }; + + // xOffset is relative to parent element, not absolute Table.prototype._computeLayout = function (xOffset, yOffset, availableWidth, availableHeight) { var _this = this; _super.prototype._computeLayout.call(this, xOffset, yOffset, availableWidth, availableHeight); var layout = this.iterateLayout(this.availableWidth, this.availableHeight); - var sumPair = function (p) { return p[0] + p[1]; }; + + var sumPair = function (p) { + return p[0] + p[1]; + }; var rowHeights = Plottable.Util.Methods.addArrays(layout.rowProportionalSpace, layout.guaranteedHeights); var colWidths = Plottable.Util.Methods.addArrays(layout.colProportionalSpace, layout.guaranteedWidths); var childYOffset = 0; this.rows.forEach(function (row, rowIndex) { var childXOffset = 0; row.forEach(function (component, colIndex) { + // recursively compute layout if (component != null) { component._computeLayout(childXOffset, childYOffset, colWidths[colIndex], rowHeights[rowIndex]); } @@ -1689,29 +2516,62 @@ var Plottable; childYOffset += rowHeights[rowIndex] + _this.rowPadding; }); }; + + /** + * Sets the row and column padding on the Table. + * + * @param {number} rowPadding The padding above and below each row, in pixels. + * @param {number} colPadding the padding to the left and right of each column, in pixels. + * @returns {Table} The calling Table. + */ Table.prototype.padding = function (rowPadding, colPadding) { this.rowPadding = rowPadding; this.colPadding = colPadding; this._invalidateLayout(); return this; }; + + /** + * Sets the layout weight of a particular row. + * Space is allocated to rows based on their weight. Rows with higher weights receive proportionally more space. + * + * @param {number} index The index of the row. + * @param {number} weight The weight to be set on the row. + * @returns {Table} The calling Table. + */ Table.prototype.rowWeight = function (index, weight) { this.rowWeights[index] = weight; this._invalidateLayout(); return this; }; + + /** + * Sets the layout weight of a particular column. + * Space is allocated to columns based on their weight. Columns with higher weights receive proportionally more space. + * + * @param {number} index The index of the column. + * @param {number} weight The weight to be set on the column. + * @returns {Table} The calling Table. + */ Table.prototype.colWeight = function (index, weight) { this.colWeights[index] = weight; this._invalidateLayout(); return this; }; + Table.prototype._isFixedWidth = function () { var cols = d3.transpose(this.rows); - return Table.fixedSpace(cols, function (c) { return (c == null) || c._isFixedWidth(); }); + return Table.fixedSpace(cols, function (c) { + return (c == null) || c._isFixedWidth(); + }); }; + Table.prototype._isFixedHeight = function () { - return Table.fixedSpace(this.rows, function (c) { return (c == null) || c._isFixedHeight(); }); + return Table.fixedSpace(this.rows, function (c) { + return (c == null) || c._isFixedHeight(); + }); }; + Table.prototype.padTableToSize = function (nRows, nCols) { for (var i = 0; i < nRows; i++) { if (this.rows[i] === undefined) { @@ -1730,28 +2590,43 @@ var Plottable; } } }; + Table.calcComponentWeights = function (setWeights, componentGroups, fixityAccessor) { + // If the row/col weight was explicitly set, then return it outright + // If the weight was not explicitly set, then guess it using the heuristic that if all components are fixed-space + // then weight is 0, otherwise weight is 1 return setWeights.map(function (w, i) { if (w != null) { return w; } var fixities = componentGroups[i].map(fixityAccessor); - var allFixed = fixities.reduce(function (a, b) { return a && b; }, true); + var allFixed = fixities.reduce(function (a, b) { + return a && b; + }, true); return allFixed ? 0 : 1; }); }; + Table.calcProportionalSpace = function (weights, freeSpace) { var weightSum = d3.sum(weights); if (weightSum === 0) { return Plottable.Util.Methods.createFilledArray(0, weights.length); - } - else { - return weights.map(function (w) { return freeSpace * w / weightSum; }); + } else { + return weights.map(function (w) { + return freeSpace * w / weightSum; + }); } }; + Table.fixedSpace = function (componentGroup, fixityAccessor) { - var all = function (bools) { return bools.reduce(function (a, b) { return a && b; }, true); }; - var group_isFixed = function (components) { return all(components.map(fixityAccessor)); }; + var all = function (bools) { + return bools.reduce(function (a, b) { + return a && b; + }, true); + }; + var group_isFixed = function (components) { + return all(components.map(fixityAccessor)); + }; return all(componentGroup.map(group_isFixed)); }; return Table; @@ -1761,6 +2636,7 @@ var Plottable; var Component = Plottable.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; } @@ -1772,6 +2648,12 @@ var Plottable; (function (Abstract) { var Scale = (function (_super) { __extends(Scale, _super); + /** + * Creates a new Scale. + * + * @constructor + * @param {D3.Scale.Scale} scale The D3 scale backing the Scale. + */ function Scale(scale) { _super.call(this); this.autoDomainAutomatically = true; @@ -1782,56 +2664,93 @@ var Plottable; Scale.prototype._getAllExtents = function () { return d3.values(this._rendererAttrID2Extent); }; + Scale.prototype._getExtent = function () { return []; }; + + /** + * 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.autoDomain = function () { this.autoDomainAutomatically = true; this._setDomain(this._getExtent()); return this; }; + Scale.prototype._autoDomainIfAutomaticMode = function () { if (this.autoDomainAutomatically) { this.autoDomain(); } }; + + /** + * Returns the range value corresponding to a given domain value. + * + * @param value {any} A domain value to be scaled. + * @returns {any} The range value corresponding to the supplied domain value. + */ Scale.prototype.scale = function (value) { return this._d3Scale(value); }; + Scale.prototype.domain = function (values) { if (values == null) { return this._getDomain(); - } - else { + } else { this.autoDomainAutomatically = false; this._setDomain(values); return this; } }; + Scale.prototype._getDomain = function () { return this._d3Scale.domain(); }; + Scale.prototype._setDomain = function (values) { this._d3Scale.domain(values); this.broadcaster.broadcast(); }; + Scale.prototype.range = function (values) { if (values == null) { return this._d3Scale.range(); - } - else { + } else { this._d3Scale.range(values); return this; } }; + + /** + * Creates a copy of the Scale with the same domain and range but without any registered listeners. + * + * @returns {Scale} A copy of the calling Scale. + */ Scale.prototype.copy = function () { return new Scale(this._d3Scale.copy()); }; + + /** + * When a renderer determines that the extent of a projector has changed, + * it will call this function. This function should ensure that + * the scale has a domain at least large enough to include extent. + * + * @param {number} rendererID A unique indentifier of the renderer sending + * the new extent. + * @param {string} attr The attribute being projected, e.g. "x", "y0", "r" + * @param {any[]} extent The new extent to be included in the scale. + */ Scale.prototype.updateExtent = function (plotProvidedKey, attr, extent) { this._rendererAttrID2Extent[plotProvidedKey + attr] = extent; this._autoDomainIfAutomaticMode(); return this; }; + Scale.prototype.removeExtent = function (plotProvidedKey, attr) { delete this._rendererAttrID2Extent[plotProvidedKey + attr]; this._autoDomainIfAutomaticMode(); @@ -1844,6 +2763,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -1865,16 +2785,15 @@ var Plottable; this.animateOnNextRender = true; this.clipPathEnabled = true; this.classed("plot", true); + var dataSource; if (dataset != null) { if (typeof dataset.data === "function") { dataSource = dataset; - } - else { + } else { dataSource = dataSource = new Plottable.DataSource(dataset); } - } - else { + } else { dataSource = new Plottable.DataSource(); } this.dataSource(dataSource); @@ -1885,10 +2804,13 @@ var Plottable; this._dataChanged = true; this._updateAllProjectors(); }; + Plot.prototype.remove = function () { var _this = this; _super.prototype.remove.call(this); this._dataSource.broadcaster.deregisterListener(this); + + // deregister from all scales var properties = Object.keys(this._projectors); properties.forEach(function (property) { var projector = _this._projectors[property]; @@ -1897,6 +2819,7 @@ var Plottable; } }); }; + Plot.prototype.dataSource = function (source) { var _this = this; if (source == null) { @@ -1907,34 +2830,43 @@ var Plottable; this._dataSource.broadcaster.deregisterListener(this); } this._dataSource = source; - this._dataSource.broadcaster.registerListener(this, function () { return _this._onDataSourceUpdate(); }); + this._dataSource.broadcaster.registerListener(this, function () { + return _this._onDataSourceUpdate(); + }); this._onDataSourceUpdate(); return this; }; + Plot.prototype._onDataSourceUpdate = function () { this._updateAllProjectors(); this.animateOnNextRender = true; this._dataChanged = true; this._render(); }; + Plot.prototype.project = function (attrToSet, accessor, scale) { var _this = this; attrToSet = attrToSet.toLowerCase(); var currentProjection = this._projectors[attrToSet]; var existingScale = (currentProjection != null) ? currentProjection.scale : null; + if (existingScale != null) { existingScale.removeExtent(this._plottableID.toString(), attrToSet); existingScale.broadcaster.deregisterListener(this); } + if (scale != null) { - scale.broadcaster.registerListener(this, function () { return _this._render(); }); + scale.broadcaster.registerListener(this, function () { + return _this._render(); + }); } var activatedAccessor = Plottable.Util.Methods._applyAccessor(accessor, this); this._projectors[attrToSet] = { accessor: activatedAccessor, scale: scale, attribute: attrToSet }; this._updateProjector(attrToSet); - this._render(); + this._render(); // queue a re-render upon changing projector return this; }; + Plot.prototype._generateAttrToProjector = function () { var _this = this; var h = {}; @@ -1942,11 +2874,14 @@ var Plottable; var projector = _this._projectors[a]; var accessor = projector.accessor; var scale = projector.scale; - var fn = scale == null ? accessor : function (d, i) { return scale.scale(accessor(d, i)); }; + var fn = scale == null ? accessor : function (d, i) { + return scale.scale(accessor(d, i)); + }; h[a] = fn; }); return h; }; + Plot.prototype._doRender = function () { if (this._isAnchored) { this._paint(); @@ -1954,50 +2889,83 @@ var Plottable; this.animateOnNextRender = false; } }; + Plot.prototype._paint = function () { + // no-op }; + Plot.prototype._setup = function () { _super.prototype._setup.call(this); this.renderArea = this.content.append("g").classed("render-area", true); }; + + /** + * Enables or disables animation. + * + * @param {boolean} enabled Whether or not to animate. + */ Plot.prototype.animate = function (enabled) { this._animate = enabled; return this; }; + Plot.prototype.detach = function () { _super.prototype.detach.call(this); + + // make the domain resize this._updateAllProjectors(); return this; }; + + /** + * This function makes sure that all of the scales in this._projectors + * have an extent that includes all the data that is projected onto them. + */ Plot.prototype._updateAllProjectors = function () { var _this = this; - d3.keys(this._projectors).forEach(function (attr) { return _this._updateProjector(attr); }); + d3.keys(this._projectors).forEach(function (attr) { + return _this._updateProjector(attr); + }); }; + Plot.prototype._updateProjector = function (attr) { var projector = this._projectors[attr]; if (projector.scale != null) { var extent = this.dataSource()._getExtent(projector.accessor); if (extent.length === 0 || !this._isAnchored) { projector.scale.removeExtent(this._plottableID.toString(), attr); - } - else { + } else { projector.scale.updateExtent(this._plottableID.toString(), attr, extent); } } }; + + /** + * Apply attributes to the selection. + * + * If animation is enabled and a valid animator's key is specified, the + * attributes are applied with the animator. Otherwise, they are applied + * immediately to the selection. + * + * The animation will not animate during auto-resize renders. + * + * @param {D3.Selection} selection The selection of elements to update. + * @param {string} animatorKey The key for the animator. + * @param {IAttributeToProjector} attrToProjector The set of attributes to set on the selection. + * @return {D3.Selection} The resulting selection (potentially after the transition) + */ Plot.prototype._applyAnimatedAttributes = function (selection, animatorKey, attrToProjector) { if (this._animate && this.animateOnNextRender && this._animators[animatorKey] != null) { return this._animators[animatorKey].animate(selection, attrToProjector, this); - } - else { + } else { return selection.attr(attrToProjector); } }; + Plot.prototype.animator = function (animatorKey, animator) { if (animator === undefined) { return this._animators[animatorKey]; - } - else { + } else { this._animators[animatorKey] = animator; return this; } @@ -2009,6 +2977,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -2020,32 +2989,48 @@ var Plottable; (function (Abstract) { var XYPlot = (function (_super) { __extends(XYPlot, _super); + /** + * Creates an XYPlot. + * + * @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. + */ function XYPlot(dataset, xScale, yScale) { _super.call(this, dataset); if (xScale == null || yScale == null) { throw new Error("XYPlots require an xScale and yScale"); } this.classed("xy-plot", true); - this.project("x", "x", xScale); - this.project("y", "y", yScale); + + this.project("x", "x", xScale); // default accessor + this.project("y", "y", yScale); // default accessor } XYPlot.prototype.project = function (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" && scale != null) { this.xScale = scale; this._updateXDomainer(); } + if (attrToSet === "y" && scale != null) { this.yScale = scale; this._updateYDomainer(); } + _super.prototype.project.call(this, attrToSet, accessor, scale); + return this; }; + XYPlot.prototype._computeLayout = function (xOffset, yOffset, availableWidth, availableHeight) { _super.prototype._computeLayout.call(this, xOffset, yOffset, availableWidth, availableHeight); this.xScale.range([0, this.availableWidth]); this.yScale.range([this.availableHeight, 0]); }; + XYPlot.prototype._updateXDomainer = function () { if (this.xScale instanceof Abstract.QuantitativeScale) { var scale = this.xScale; @@ -2054,6 +3039,7 @@ var Plottable; } } }; + XYPlot.prototype._updateYDomainer = function () { if (this.yScale instanceof Abstract.QuantitativeScale) { var scale = this.yScale; @@ -2069,6 +3055,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -2080,7 +3067,15 @@ var Plottable; (function (Abstract) { var NewStylePlot = (function (_super) { __extends(NewStylePlot, _super); + /** + * Creates a NewStylePlot. + * + * @constructor + * @param [Scale] xScale The x scale to use + * @param [Scale] yScale The y scale to use + */ function NewStylePlot(xScale, yScale) { + // make a dummy dataSource to satisfy the base Plot (HACKHACK) _super.call(this, new Plottable.DataSource(), xScale, yScale); this.nextSeriesIndex = 0; this._key2DatasetDrawerKey = {}; @@ -2089,13 +3084,19 @@ var Plottable; NewStylePlot.prototype._setup = function () { var _this = this; _super.prototype._setup.call(this); - this._getDrawersInOrder().forEach(function (d) { return d.renderArea = _this.renderArea.append("g"); }); + this._getDrawersInOrder().forEach(function (d) { + return d.renderArea = _this.renderArea.append("g"); + }); }; + NewStylePlot.prototype.remove = function () { var _this = this; _super.prototype.remove.call(this); - this._datasetKeysInOrder.forEach(function (k) { return _this.removeDataset(k); }); + this._datasetKeysInOrder.forEach(function (k) { + return _this.removeDataset(k); + }); }; + NewStylePlot.prototype.addDataset = function (keyOrDataset, dataset) { if (typeof (keyOrDataset) !== "string" && dataset !== undefined) { throw new Error("invalid input to addDataset"); @@ -2106,9 +3107,11 @@ var Plottable; var key = typeof (keyOrDataset) === "string" ? keyOrDataset : "_" + this.nextSeriesIndex++; var data = typeof (keyOrDataset) !== "string" ? keyOrDataset : dataset; var dataset = (data instanceof Plottable.DataSource) ? data : new Plottable.DataSource(data); + this._addDataset(key, dataset); return this; }; + NewStylePlot.prototype._addDataset = function (key, dataset) { var _this = this; if (this._key2DatasetDrawerKey[key] != null) { @@ -2119,15 +3122,20 @@ var Plottable; var ddk = { drawer: drawer, dataset: dataset, key: key }; this._datasetKeysInOrder.push(key); this._key2DatasetDrawerKey[key] = ddk; + if (this._isSetup) { drawer.renderArea = this.renderArea.append("g"); } - dataset.broadcaster.registerListener(this, function () { return _this._onDataSourceUpdate(); }); + dataset.broadcaster.registerListener(this, function () { + return _this._onDataSourceUpdate(); + }); this._onDataSourceUpdate(); }; + NewStylePlot.prototype._getDrawer = function (key) { throw new Error("Abstract Method Not Implemented"); }; + NewStylePlot.prototype._updateProjector = function (attr) { var _this = this; var projector = this._projectors[attr]; @@ -2137,13 +3145,13 @@ var Plottable; var scaleKey = _this._plottableID.toString() + "_" + ddk.key; if (extent.length === 0 || !_this._isAnchored) { projector.scale.removeExtent(scaleKey, attr); - } - else { + } else { projector.scale.updateExtent(scaleKey, attr, extent); } }); } }; + NewStylePlot.prototype.datasetOrder = function (order) { if (order === undefined) { return this._datasetKeysInOrder; @@ -2156,16 +3164,23 @@ var Plottable; if (isPermutation(order, this._datasetKeysInOrder)) { this._datasetKeysInOrder = order; this._onDataSourceUpdate(); - } - else { + } else { Plottable.Util.Methods.warn("Attempted to change datasetOrder, but new order is not permutation of old. Ignoring."); } return this; }; + + /** + * Removes a dataset + * + * @param {string} key The key of the dataset + * @return {NewStylePlot} The calling NewStylePlot. + */ NewStylePlot.prototype.removeDataset = function (key) { if (this._key2DatasetDrawerKey[key] != null) { var ddk = this._key2DatasetDrawerKey[key]; ddk.drawer.remove(); + var projectors = d3.values(this._projectors); var scaleKey = this._plottableID.toString() + "_" + key; projectors.forEach(function (p) { @@ -2173,6 +3188,7 @@ var Plottable; p.scale.removeExtent(scaleKey, p.attribute); } }); + ddk.dataset.broadcaster.deregisterListener(this); this._datasetKeysInOrder.splice(this._datasetKeysInOrder.indexOf(key), 1); delete this._key2DatasetDrawerKey[key]; @@ -2180,13 +3196,19 @@ var Plottable; } return this; }; + NewStylePlot.prototype._getDatasetsInOrder = function () { var _this = this; - return this._datasetKeysInOrder.map(function (k) { return _this._key2DatasetDrawerKey[k].dataset; }); + return this._datasetKeysInOrder.map(function (k) { + return _this._key2DatasetDrawerKey[k].dataset; + }); }; + NewStylePlot.prototype._getDrawersInOrder = function () { var _this = this; - return this._datasetKeysInOrder.map(function (k) { return _this._key2DatasetDrawerKey[k].drawer; }); + return this._datasetKeysInOrder.map(function (k) { + return _this._key2DatasetDrawerKey[k].drawer; + }); }; return NewStylePlot; })(Abstract.XYPlot); @@ -2195,6 +3217,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Core) { @@ -2209,6 +3232,7 @@ var Plottable; return Immediate; })(); RenderPolicy.Immediate = Immediate; + var AnimationFrame = (function () { function AnimationFrame() { } @@ -2218,6 +3242,7 @@ var Plottable; return AnimationFrame; })(); RenderPolicy.AnimationFrame = AnimationFrame; + var Timeout = (function () { function Timeout() { this._timeoutMsec = Plottable.Util.DOM.POLYFILL_TIMEOUT_MSEC; @@ -2236,19 +3261,38 @@ var Plottable; var Core = Plottable.Core; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Core) { + /** + * The RenderController is responsible for enqueueing and synchronizing + * layout and render calls for Plottable components. + * + * Layouts and renders occur inside an animation callback + * (window.requestAnimationFrame if available). + * + * If you require immediate rendering, call RenderController.flush() to + * perform enqueued layout and rendering serially. + */ (function (RenderController) { var _componentsNeedingRender = {}; var _componentsNeedingComputeLayout = {}; var _animationRequested = false; var _isCurrentlyFlushing = false; RenderController._renderPolicy = new RenderController.RenderPolicy.AnimationFrame(); + function setRenderPolicy(policy) { RenderController._renderPolicy = policy; } RenderController.setRenderPolicy = setRenderPolicy; + + /** + * If the RenderController is enabled, we enqueue the component for + * render. Otherwise, it is rendered immediately. + * + * @param {Abstract.Component} component Any Plottable component. + */ function registerToRender(c) { if (_isCurrentlyFlushing) { Plottable.Util.Methods.warn("Registered to render while other components are flushing: request may be ignored"); @@ -2257,42 +3301,69 @@ var Plottable; requestRender(); } RenderController.registerToRender = registerToRender; + + /** + * If the RenderController is enabled, we enqueue the component for + * layout and render. Otherwise, it is rendered immediately. + * + * @param {Abstract.Component} component Any Plottable component. + */ function registerToComputeLayout(c) { _componentsNeedingComputeLayout[c._plottableID] = c; _componentsNeedingRender[c._plottableID] = c; requestRender(); } RenderController.registerToComputeLayout = registerToComputeLayout; + function requestRender() { + // Only run or enqueue flush on first request. if (!_animationRequested) { _animationRequested = true; RenderController._renderPolicy.render(); } } + function flush() { if (_animationRequested) { + // Layout var toCompute = d3.values(_componentsNeedingComputeLayout); - toCompute.forEach(function (c) { return c._computeLayout(); }); + toCompute.forEach(function (c) { + return c._computeLayout(); + }); + + // Top level render. + // Containers will put their children in the toRender queue var toRender = d3.values(_componentsNeedingRender); - toRender.forEach(function (c) { return c._render(); }); + toRender.forEach(function (c) { + return c._render(); + }); + + // now we are flushing _isCurrentlyFlushing = true; + + // Finally, perform render of all components var failed = {}; Object.keys(_componentsNeedingRender).forEach(function (k) { - try { + try { _componentsNeedingRender[k]._doRender(); - } - catch (err) { + } catch (err) { + // using setTimeout instead of console.log, we get the familiar red + // stack trace setTimeout(function () { throw err; }, 0); failed[k] = _componentsNeedingRender[k]; } }); + + // Reset queues _componentsNeedingComputeLayout = {}; _componentsNeedingRender = failed; _animationRequested = false; _isCurrentlyFlushing = false; } + + // Reset resize flag regardless of queue'd components Core.ResizeBroadcaster.clearResizing(); } RenderController.flush = flush; @@ -2302,35 +3373,74 @@ var Plottable; var Core = Plottable.Core; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Core) { + /** + * The ResizeBroadcaster will broadcast a notification to any registered + * components when the window is resized. + * + * The broadcaster and single event listener are lazily constructed. + * + * Upon resize, the _resized flag will be set to true until after the next + * flush of the RenderController. This is used, for example, to disable + * animations during resize. + */ (function (ResizeBroadcaster) { var broadcaster; var _resizing = false; + function _lazyInitialize() { if (broadcaster === undefined) { broadcaster = new Core.Broadcaster(ResizeBroadcaster); window.addEventListener("resize", _onResize); } } + function _onResize() { _resizing = true; broadcaster.broadcast(); } + + /** + * Returns true if the window has been resized and the RenderController + * has not yet been flushed. + */ function resizing() { return _resizing; } ResizeBroadcaster.resizing = resizing; + function clearResizing() { _resizing = false; } ResizeBroadcaster.clearResizing = clearResizing; + + /** + * Registers a component. + * + * When the window is resized, we invoke ._invalidateLayout() on the + * component, which will enqueue the component for layout and rendering + * with the RenderController. + * + * @param {Abstract.Component} component Any Plottable component. + */ function register(c) { _lazyInitialize(); - broadcaster.registerListener(c._plottableID, function () { return c._invalidateLayout(); }); + broadcaster.registerListener(c._plottableID, function () { + return c._invalidateLayout(); + }); } ResizeBroadcaster.register = register; + + /** + * Deregisters the components. + * + * The component will no longer receive updates on window resize. + * + * @param {Abstract.Component} component Any Plottable component. + */ function deregister(c) { if (broadcaster) { broadcaster.deregisterListener(c._plottableID); @@ -2343,94 +3453,186 @@ var Plottable; var Core = Plottable.Core; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { ; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { var Domainer = (function () { + /** + * @param {(extents: any[][]) => any[]} combineExtents + * If present, this function will be used by the Domainer to merge + * all the extents that are present on a scale. + * + * A plot may draw multiple things relative to a scale, e.g. + * different stocks over time. The plot computes their extents, + * which are a [min, max] pair. combineExtents is responsible for + * merging them all into one [min, max] pair. It defaults to taking + * the min of the first elements and the max of the second arguments. + */ function Domainer(combineExtents) { this.doNice = false; this.padProportion = 0.0; this.paddingExceptions = d3.map(); this.unregisteredPaddingExceptions = d3.set(); this.includedValues = d3.map(); + // includedValues needs to be a map, even unregistered, to support getting un-stringified values back out this.unregisteredIncludedValues = d3.map(); this.combineExtents = combineExtents; } + /** + * @param {any[][]} extents The list of extents to be reduced to a single + * extent. + * @param {Abstract.QuantitativeScale} scale + * Since nice() must do different things depending on Linear, Log, + * or Time scale, the scale must be passed in for nice() to work. + * @return {any[]} The domain, as a merging of all exents, as a [min, max] + * pair. + */ Domainer.prototype.computeDomain = function (extents, scale) { var domain; if (this.combineExtents != null) { domain = this.combineExtents(extents); - } - else if (extents.length === 0) { + } else if (extents.length === 0) { domain = scale._defaultExtent(); - } - else { - domain = [d3.min(extents, function (e) { return e[0]; }), d3.max(extents, function (e) { return e[1]; })]; + } else { + domain = [d3.min(extents, function (e) { + return e[0]; + }), d3.max(extents, function (e) { + return e[1]; + })]; } domain = this.includeDomain(domain); domain = this.padDomain(scale, domain); domain = this.niceDomain(scale, domain); return domain; }; + + /** + * Sets the Domainer to pad by a given ratio. + * + * @param {number} [padProportion] Proportionally how much bigger the + * new domain should be (0.05 = 5% larger). + * + * A domainer will pad equal visual amounts on each side. + * On a linear scale, this means both sides are padded the same + * amount: [10, 20] will be padded to [5, 25]. + * On a log scale, the top will be padded more than the bottom, so + * [10, 100] will be padded to [1, 1000]. + * + * @return {Domainer} The calling Domainer. + */ Domainer.prototype.pad = function (padProportion) { - if (padProportion === void 0) { padProportion = 0.05; } + if (typeof padProportion === "undefined") { padProportion = 0.05; } this.padProportion = padProportion; return this; }; + + /** + * Add a padding exception, a value that will not be padded at either end of the domain. + * + * Eg, if a padding exception is added at x=0, then [0, 100] will pad to [0, 105] instead of [-2.5, 102.5]. + * If a key is provided, it will be registered under that key with standard map semantics. (Overwrite / remove by key) + * If a key is not provided, it will be added with set semantics (Can be removed by value) + * + * @param {any} exception The padding exception to add. + * @param string [key] The key to register the exception under. + * @return Domainer The calling domainer + */ Domainer.prototype.addPaddingException = function (exception, key) { if (key != null) { this.paddingExceptions.set(key, exception); - } - else { + } else { this.unregisteredPaddingExceptions.add(exception); } return this; }; + + /** + * Remove a padding exception, allowing the domain to pad out that value again. + * + * If a string is provided, it is assumed to be a key and the exception associated with that key is removed. + * If a non-string is provdied, it is assumed to be an unkeyed exception and that exception is removed. + * + * @param {any} keyOrException The key for the value to remove, or the value to remove + * @return Domainer The calling domainer + */ Domainer.prototype.removePaddingException = function (keyOrException) { if (typeof (keyOrException) === "string") { this.paddingExceptions.remove(keyOrException); - } - else { + } else { this.unregisteredPaddingExceptions.remove(keyOrException); } return this; }; + + /** + * Add an included value, a value that must be included inside the domain. + * + * Eg, if a value exception is added at x=0, then [50, 100] will expand to [0, 100] rather than [50, 100]. + * If a key is provided, it will be registered under that key with standard map semantics. (Overwrite / remove by key) + * If a key is not provided, it will be added with set semantics (Can be removed by value) + * + * @param {any} value The included value to add. + * @param string [key] The key to register the value under. + * @return Domainer The calling domainer + */ Domainer.prototype.addIncludedValue = function (value, key) { if (key != null) { this.includedValues.set(key, value); - } - else { + } else { this.unregisteredIncludedValues.set(value, value); } return this; }; + + /** + * Remove an included value, allowing the domain to not include that value gain again. + * + * If a string is provided, it is assumed to be a key and the value associated with that key is removed. + * If a non-string is provdied, it is assumed to be an unkeyed value and that value is removed. + * + * @param {any} keyOrException The key for the value to remove, or the value to remove + * @return Domainer The calling domainer + */ Domainer.prototype.removeIncludedValue = function (valueOrKey) { if (typeof (valueOrKey) === "string") { this.includedValues.remove(valueOrKey); - } - else { + } else { this.unregisteredIncludedValues.remove(valueOrKey); } return this; }; + + /** + * Extends the scale's domain so it starts and ends with "nice" values. + * + * @param {number} [count] The number of ticks that should fit inside the new domain. + * @return {Domainer} The calling Domainer. + */ Domainer.prototype.nice = function (count) { this.doNice = true; this.niceCount = count; return this; }; + Domainer.defaultCombineExtents = function (extents) { if (extents.length === 0) { return [0, 1]; - } - else { - return [d3.min(extents, function (e) { return e[0]; }), d3.max(extents, function (e) { return e[1]; })]; + } else { + return [d3.min(extents, function (e) { + return e[0]; + }), d3.max(extents, function (e) { + return e[1]; + })]; } }; + Domainer.prototype.padDomain = function (scale, domain) { var min = domain[0]; var max = domain[1]; @@ -2438,15 +3640,20 @@ var Plottable; var d = min.valueOf(); if (min instanceof Date) { return [d - Domainer.ONE_DAY, d + Domainer.ONE_DAY]; - } - else { - return [d - Domainer.PADDING_FOR_IDENTICAL_DOMAIN, d + Domainer.PADDING_FOR_IDENTICAL_DOMAIN]; + } else { + return [ + d - Domainer.PADDING_FOR_IDENTICAL_DOMAIN, + d + Domainer.PADDING_FOR_IDENTICAL_DOMAIN]; } } + if (scale.domain()[0] === scale.domain()[1]) { return domain; } var p = this.padProportion / 2; + + // This scaling is done to account for log scales and other non-linear + // scales. A log scale should be padded more on the max than on the min. var newMin = scale.invert(scale.scale(min) - (scale.scale(max) - scale.scale(min)) * p); var newMax = scale.invert(scale.scale(max) + (scale.scale(max) - scale.scale(min)) * p); var exceptionValues = this.paddingExceptions.values().concat(this.unregisteredPaddingExceptions.values()); @@ -2459,17 +3666,20 @@ var Plottable; } return [newMin, newMax]; }; + Domainer.prototype.niceDomain = function (scale, domain) { if (this.doNice) { return scale._niceDomain(domain, this.niceCount); - } - else { + } else { return domain; } }; + Domainer.prototype.includeDomain = function (domain) { var includedValues = this.includedValues.values().concat(this.unregisteredIncludedValues.values()); - return includedValues.reduce(function (domain, value) { return [Math.min(domain[0], value), Math.max(domain[1], value)]; }, domain); + return includedValues.reduce(function (domain, value) { + return [Math.min(domain[0], value), Math.max(domain[1], value)]; + }, domain); }; Domainer.PADDING_FOR_IDENTICAL_DOMAIN = 1; Domainer.ONE_DAY = 1000 * 60 * 60 * 24; @@ -2478,6 +3688,7 @@ var Plottable; Plottable.Domainer = Domainer; })(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; } @@ -2489,6 +3700,12 @@ var Plottable; (function (Abstract) { var QuantitativeScale = (function (_super) { __extends(QuantitativeScale, _super); + /** + * Creates a new QuantitativeScale. + * + * @constructor + * @param {D3.Scale.QuantitativeScale} scale The D3 QuantitativeScale backing the QuantitativeScale. + */ function QuantitativeScale(scale) { _super.call(this, scale); this._lastRequestedTickCount = 10; @@ -2499,23 +3716,41 @@ var Plottable; QuantitativeScale.prototype._getExtent = function () { return this._domainer.computeDomain(this._getAllExtents(), this); }; + + /** + * Retrieves the domain value corresponding to a supplied range value. + * + * @param {number} value: A value from the Scale's range. + * @returns {number} The domain value corresponding to the supplied range value. + */ QuantitativeScale.prototype.invert = function (value) { return this._d3Scale.invert(value); }; + + /** + * Creates a copy of the QuantitativeScale with the same domain and range but without any registered listeners. + * + * @returns {QuantitativeScale} A copy of the calling QuantitativeScale. + */ QuantitativeScale.prototype.copy = function () { return new QuantitativeScale(this._d3Scale.copy()); }; + QuantitativeScale.prototype.domain = function (values) { return _super.prototype.domain.call(this, values); }; + QuantitativeScale.prototype._setDomain = function (values) { - var isNaNOrInfinity = function (x) { return x !== x || x === Infinity || x === -Infinity; }; + var isNaNOrInfinity = function (x) { + return x !== x || x === Infinity || x === -Infinity; + }; if (isNaNOrInfinity(values[0]) || isNaNOrInfinity(values[1])) { Plottable.Util.Methods.warn("Warning: QuantitativeScales cannot take NaN or Infinity as a domain value. Ignoring."); return; } _super.prototype._setDomain.call(this, values); }; + QuantitativeScale.prototype.interpolate = function (factory) { if (factory == null) { return this._d3Scale.interpolate(); @@ -2523,10 +3758,17 @@ var Plottable; this._d3Scale.interpolate(factory); return this; }; + + /** + * Sets the range of the QuantitativeScale and sets the interpolator to d3.interpolateRound. + * + * @param {number[]} values The new range value for the range. + */ QuantitativeScale.prototype.rangeRound = function (values) { this._d3Scale.rangeRound(values); return this; }; + QuantitativeScale.prototype.clamp = function (clamp) { if (clamp == null) { return this._d3Scale.clamp(); @@ -2534,29 +3776,50 @@ var Plottable; this._d3Scale.clamp(clamp); return this; }; + + /** + * Generates tick values. + * + * @param {number} [count] The number of ticks to generate. + * @returns {any[]} The generated ticks. + */ QuantitativeScale.prototype.ticks = function (count) { if (count != null) { this._lastRequestedTickCount = count; } return this._d3Scale.ticks(this._lastRequestedTickCount); }; + + /** + * Gets a tick formatting function for displaying tick values. + * + * @param {number} count The number of ticks to be displayed + * @param {string} [format] A format specifier string. + * @returns {(n: number) => string} A formatting function. + */ QuantitativeScale.prototype.tickFormat = function (count, format) { return this._d3Scale.tickFormat(count, format); }; + + /** + * Given a domain, expands its domain onto "nice" values, e.g. whole + * numbers. + */ QuantitativeScale.prototype._niceDomain = function (domain, count) { return this._d3Scale.copy().domain(domain).nice(count).domain(); }; + QuantitativeScale.prototype.domainer = function (domainer) { if (domainer == null) { return this._domainer; - } - else { + } else { this._domainer = domainer; this._userSetDomainer = true; this._autoDomainIfAutomaticMode(); return this; } }; + QuantitativeScale.prototype._defaultExtent = function () { return [0, 1]; }; @@ -2567,6 +3830,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -2581,6 +3845,11 @@ var Plottable; function Linear(scale) { _super.call(this, scale == null ? d3.scale.linear() : scale); } + /** + * Creates a copy of the LinearScale with the same domain and range but without any registered listeners. + * + * @returns {LinearScale} A copy of the calling LinearScale. + */ Linear.prototype.copy = function () { return new Linear(this._d3Scale.copy()); }; @@ -2591,6 +3860,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2609,9 +3879,15 @@ var Plottable; Plottable.Util.Methods.warn("Plottable.Scale.Log is deprecated. If possible, use Plottable.Scale.ModifiedLog instead."); } } + /** + * Creates a copy of the Scale.Log with the same domain and range but without any registered listeners. + * + * @returns {Scale.Log} A copy of the calling Scale.Log. + */ Log.prototype.copy = function () { return new Log(this._d3Scale.copy()); }; + Log.prototype._defaultExtent = function () { return [1, 10]; }; @@ -2623,6 +3899,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2634,8 +3911,33 @@ var Plottable; (function (Scale) { var ModifiedLog = (function (_super) { __extends(ModifiedLog, _super); + /** + * Creates a new Scale.ModifiedLog. + * + * A ModifiedLog scale acts as a regular log scale for large numbers. + * As it approaches 0, it gradually becomes linear. This means that the + * scale won't freak out if you give it 0 or a negative number, where an + * ordinary Log scale would. + * + * However, it does mean that scale will be effectively linear as values + * approach 0. If you want very small values on a log scale, you should use + * an ordinary Scale.Log instead. + * + * @constructor + * @param {number} [base] + * The base of the log. Defaults to 10, and must be > 1. + * + * For base <= x, scale(x) = log(x). + * + * For 0 < x < base, scale(x) will become more and more + * linear as it approaches 0. + * + * At x == 0, scale(x) == 0. + * + * For negative values, scale(-x) = -scale(x). + */ function ModifiedLog(base) { - if (base === void 0) { base = 10; } + if (typeof base === "undefined") { base = 10; } _super.call(this, d3.scale.linear()); this._showIntermediateTicks = false; this.base = base; @@ -2646,61 +3948,111 @@ var Plottable; throw new Error("ModifiedLogScale: The base must be > 1"); } } + /** + * Returns an adjusted log10 value for graphing purposes. The first + * adjustment is that negative values are changed to positive during + * the calculations, and then the answer is negated at the end. The + * second is that, for values less than 10, an increasingly large + * (0 to 1) scaling factor is added such that at 0 the value is + * adjusted to 1, resulting in a returned result of 0. + */ ModifiedLog.prototype.adjustedLog = function (x) { var negationFactor = x < 0 ? -1 : 1; x *= negationFactor; + if (x < this.pivot) { x += (this.pivot - x) / this.pivot; } + x = Math.log(x) / Math.log(this.base); + x *= negationFactor; return x; }; + ModifiedLog.prototype.invertedAdjustedLog = function (x) { var negationFactor = x < 0 ? -1 : 1; x *= negationFactor; + x = Math.pow(this.base, x); + if (x < this.pivot) { x = (this.pivot * (x - 1)) / (this.pivot - 1); } + x *= negationFactor; return x; }; + ModifiedLog.prototype.scale = function (x) { return this._d3Scale(this.adjustedLog(x)); }; + ModifiedLog.prototype.invert = function (x) { return this.invertedAdjustedLog(this._d3Scale.invert(x)); }; + ModifiedLog.prototype._getDomain = function () { return this.untransformedDomain; }; + ModifiedLog.prototype._setDomain = function (values) { this.untransformedDomain = values; var transformedDomain = [this.adjustedLog(values[0]), this.adjustedLog(values[1])]; this._d3Scale.domain(transformedDomain); this.broadcaster.broadcast(); }; + ModifiedLog.prototype.ticks = function (count) { if (count != null) { _super.prototype.ticks.call(this, count); } - var middle = function (x, y, z) { return [x, y, z].sort(function (a, b) { return a - b; })[1]; }; + + // Say your domain is [-100, 100] and your pivot is 10. + // then we're going to draw negative log ticks from -100 to -10, + // linear ticks from -10 to 10, and positive log ticks from 10 to 100. + var middle = function (x, y, z) { + return [x, y, z].sort(function (a, b) { + return a - b; + })[1]; + }; var min = d3.min(this.untransformedDomain); var max = d3.max(this.untransformedDomain); var negativeLower = min; var negativeUpper = middle(min, max, -this.pivot); var positiveLower = middle(min, max, this.pivot); var positiveUpper = max; - var negativeLogTicks = this.logTicks(-negativeUpper, -negativeLower).map(function (x) { return -x; }).reverse(); + + var negativeLogTicks = this.logTicks(-negativeUpper, -negativeLower).map(function (x) { + return -x; + }).reverse(); var positiveLogTicks = this.logTicks(positiveLower, positiveUpper); - var linearTicks = this._showIntermediateTicks ? d3.scale.linear().domain([negativeUpper, positiveLower]).ticks(this.howManyTicks(negativeUpper, positiveLower)) : [-this.pivot, 0, this.pivot].filter(function (x) { return min <= x && x <= max; }); + var linearTicks = this._showIntermediateTicks ? d3.scale.linear().domain([negativeUpper, positiveLower]).ticks(this.howManyTicks(negativeUpper, positiveLower)) : [-this.pivot, 0, this.pivot].filter(function (x) { + return min <= x && x <= max; + }); + var ticks = negativeLogTicks.concat(linearTicks).concat(positiveLogTicks); + + // If you only have 1 tick, you can't tell how big the scale is. if (ticks.length <= 1) { ticks = d3.scale.linear().domain([min, max]).ticks(this._lastRequestedTickCount); } return ticks; }; + + /** + * Return an appropriate number of ticks from lower to upper. + * + * This will first try to fit as many powers of this.base as it can from + * lower to upper. + * + * If it still has ticks after that, it will generate ticks in "clusters", + * e.g. [20, 30, ... 90, 100] would be a cluster, [200, 300, ... 900, 1000] + * would be another cluster. + * + * This function will generate clusters as large as it can while not + * drastically exceeding its number of ticks. + */ ModifiedLog.prototype.logTicks = function (lower, upper) { var _this = this; var nTicks = this.howManyTicks(lower, upper); @@ -2713,12 +4065,28 @@ var Plottable; var nMultiples = this._showIntermediateTicks ? Math.floor(nTicks / bases.length) : 1; var multiples = d3.range(this.base, 1, -(this.base - 1) / nMultiples).map(Math.floor); var uniqMultiples = Plottable.Util.Methods.uniqNumbers(multiples); - var clusters = bases.map(function (b) { return uniqMultiples.map(function (x) { return Math.pow(_this.base, b - 1) * x; }); }); + var clusters = bases.map(function (b) { + return uniqMultiples.map(function (x) { + return Math.pow(_this.base, b - 1) * x; + }); + }); var flattened = Plottable.Util.Methods.flatten(clusters); - var filtered = flattened.filter(function (x) { return lower <= x && x <= upper; }); - var sorted = filtered.sort(function (x, y) { return x - y; }); + var filtered = flattened.filter(function (x) { + return lower <= x && x <= upper; + }); + var sorted = filtered.sort(function (x, y) { + return x - y; + }); return sorted; }; + + /** + * How many ticks does the range [lower, upper] deserve? + * + * e.g. if your domain was [10, 1000] and I asked howManyTicks(10, 100), + * I would get 1/2 of the ticks. The range 10, 100 takes up 1/2 of the + * distance when plotted. + */ ModifiedLog.prototype.howManyTicks = function (lower, upper) { var adjustedMin = this.adjustedLog(d3.min(this.untransformedDomain)); var adjustedMax = this.adjustedLog(d3.max(this.untransformedDomain)); @@ -2728,17 +4096,19 @@ var Plottable; var ticks = Math.ceil(proportion * this._lastRequestedTickCount); return ticks; }; + ModifiedLog.prototype.copy = function () { return new ModifiedLog(this.base); }; + ModifiedLog.prototype._niceDomain = function (domain, count) { return domain; }; + ModifiedLog.prototype.showIntermediateTicks = function (show) { if (show == null) { return this._showIntermediateTicks; - } - else { + } else { this._showIntermediateTicks = show; } }; @@ -2749,6 +4119,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2760,10 +4131,16 @@ var Plottable; (function (Scale) { var Ordinal = (function (_super) { __extends(Ordinal, _super); + /** + * Creates a new OrdinalScale. Domain and Range are set later. + * + * @constructor + */ function Ordinal(scale) { _super.call(this, scale == null ? d3.scale.ordinal() : scale); this._range = [0, 1]; this._rangeType = "bands"; + // Padding as a proportion of the spacing between domain values this._innerPadding = 0.3; this._outerPadding = 0.5; if (this._innerPadding > this._outerPadding) { @@ -2774,31 +4151,39 @@ var Plottable; var extents = this._getAllExtents(); return Plottable.Util.Methods.uniq(Plottable.Util.Methods.flatten(extents)); }; + Ordinal.prototype.domain = function (values) { return _super.prototype.domain.call(this, values); }; + Ordinal.prototype._setDomain = function (values) { _super.prototype._setDomain.call(this, values); - this.range(this.range()); + this.range(this.range()); // update range }; + Ordinal.prototype.range = function (values) { if (values == null) { return this._range; - } - else { + } else { this._range = values; if (this._rangeType === "points") { - this._d3Scale.rangePoints(values, 2 * this._outerPadding); - } - else if (this._rangeType === "bands") { + 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". + */ Ordinal.prototype.rangeBand = function () { return this._d3Scale.rangeBand(); }; + Ordinal.prototype.innerPadding = function () { var d = this.domain(); if (d.length < 2) { @@ -2807,16 +4192,17 @@ var Plottable; var step = Math.abs(this.scale(d[1]) - this.scale(d[0])); return step - this.rangeBand(); }; + Ordinal.prototype.fullBandStartAndWidth = function (v) { var start = this.scale(v) - this.innerPadding() / 2; var width = this.rangeBand() + this.innerPadding(); return [start, width]; }; + Ordinal.prototype.rangeType = function (rangeType, outerPadding, innerPadding) { if (rangeType == null) { return this._rangeType; - } - else { + } else { if (!(rangeType === "points" || rangeType === "bands")) { throw new Error("Unsupported range type: " + rangeType); } @@ -2831,6 +4217,12 @@ var Plottable; return this; } }; + + /** + * Creates a copy of the Scale with the same domain and range but without any registered listeners. + * + * @returns {Ordinal} A copy of the calling Scale. + */ Ordinal.prototype.copy = function () { return new Ordinal(this._d3Scale.copy()); }; @@ -2841,6 +4233,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2852,6 +4245,14 @@ var Plottable; (function (Scale) { var Color = (function (_super) { __extends(Color, _super); + /** + * Creates a ColorScale. + * + * @constructor + * @param {string} [scaleType] the type of color scale to create + * (Category10/Category20/Category20b/Category20c). + * See https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors + */ function Color(scaleType) { var scale; switch (scaleType) { @@ -2884,6 +4285,7 @@ var Plottable; } _super.call(this, scale); } + // Duplicated from OrdinalScale._getExtent - should be removed in #388 Color.prototype._getExtent = function () { var extents = this._getAllExtents(); var concatenatedExtents = []; @@ -2899,6 +4301,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2911,28 +4314,40 @@ var Plottable; var Time = (function (_super) { __extends(Time, _super); function Time(scale) { + // need to cast since d3 time scales do not descend from Quantitative scales _super.call(this, scale == null ? d3.time.scale() : scale); } Time.prototype.tickInterval = function (interval, step) { + // temporarily creats a time scale from our linear scale into a time scale so we can get access to its api var tempScale = d3.time.scale(); tempScale.domain(this.domain()); tempScale.range(this.range()); return tempScale.ticks(interval.range, step); }; + Time.prototype.domain = function (values) { if (values == null) { return _super.prototype.domain.call(this); - } - else { + } else { + // attempt to parse dates if (typeof (values[0]) === "string") { - values = values.map(function (d) { return new Date(d); }); + values = values.map(function (d) { + return new Date(d); + }); } return _super.prototype.domain.call(this, values); } }; + + /** + * Creates a copy of the TimeScale with the same domain and range but without any registered listeners. + * + * @returns {TimeScale} A copy of the calling TimeScale. + */ Time.prototype.copy = function () { return new Time(this._d3Scale.copy()); }; + Time.prototype._defaultExtent = function () { var endTime = new Date().valueOf(); var startTime = endTime - Plottable.MILLISECONDS_IN_ONE_DAY; @@ -2945,6 +4360,7 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -2955,15 +4371,43 @@ var Plottable; (function (Plottable) { (function (Scale) { ; + + /** + * This class implements a color scale that takes quantitive input and + * interpolates between a list of color values. It returns a hex string + * representing the interpolated color. + * + * By default it generates a linear scale internally. + */ var InterpolatedColor = (function (_super) { __extends(InterpolatedColor, _super); + /** + * Creates a InterpolatedColorScale. + * + * @constructor + * @param {string|string[]} [colorRange] the type of color scale to + * create. Default is "reds". @see {@link colorRange} for further + * options. + * @param {string} [scaleType] the type of underlying scale to use + * (linear/pow/log/sqrt). Default is "linear". @see {@link scaleType} + * for further options. + */ function InterpolatedColor(colorRange, scaleType) { - if (colorRange === void 0) { colorRange = "reds"; } - if (scaleType === void 0) { scaleType = "linear"; } + if (typeof colorRange === "undefined") { colorRange = "reds"; } + if (typeof scaleType === "undefined") { scaleType = "linear"; } this._colorRange = this._resolveColorValues(colorRange); this._scaleType = scaleType; _super.call(this, InterpolatedColor.getD3InterpolatedScale(this._colorRange, this._scaleType)); } + /** + * Converts the string array into a d3 scale. + * + * @param {string[]} colors an array of strings representing color + * values in hex ("#FFFFFF") or keywords ("white"). + * @param {string} scaleType a string representing the underlying scale + * type ("linear"/"log"/"sqrt"/"pow") + * @returns a Quantitative d3 scale. + */ InterpolatedColor.getD3InterpolatedScale = function (colors, scaleType) { var scale; switch (scaleType) { @@ -2985,6 +4429,18 @@ var Plottable; } return scale.range([0, 1]).interpolate(InterpolatedColor.interpolateColors(colors)); }; + + /** + * Creates a d3 interpolator given the color array. + * + * 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"). + */ InterpolatedColor.interpolateColors = function (colors) { if (colors.length < 2) { throw new Error("Color scale arrays must have at least two elements."); @@ -2992,15 +4448,21 @@ var Plottable; ; return 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); }; }; }; + InterpolatedColor.prototype.colorRange = function (colorRange) { if (colorRange == null) { return this._colorRange; @@ -3009,6 +4471,7 @@ var Plottable; this._resetScale(); return this; }; + InterpolatedColor.prototype.scaleType = function (scaleType) { if (scaleType == null) { return this._scaleType; @@ -3017,26 +4480,32 @@ var Plottable; this._resetScale(); return this; }; + InterpolatedColor.prototype._resetScale = function () { this._d3Scale = InterpolatedColor.getD3InterpolatedScale(this._colorRange, this._scaleType); this._autoDomainIfAutomaticMode(); this.broadcaster.broadcast(); }; + InterpolatedColor.prototype._resolveColorValues = function (colorRange) { if (colorRange instanceof Array) { return colorRange; - } - else if (InterpolatedColor.COLOR_SCALES[colorRange] != null) { + } else if (InterpolatedColor.COLOR_SCALES[colorRange] != null) { return InterpolatedColor.COLOR_SCALES[colorRange]; - } - else { + } else { return InterpolatedColor.COLOR_SCALES["reds"]; } }; + InterpolatedColor.prototype.autoDomain = function () { + // unlike other QuantitativeScales, interpolatedColorScale ignores its domainer var extents = this._getAllExtents(); if (extents.length > 0) { - this._setDomain([d3.min(extents, function (x) { return x[0]; }), d3.max(extents, function (x) { return x[1]; })]); + this._setDomain([d3.min(extents, function (x) { + return x[0]; + }), d3.max(extents, function (x) { + return x[1]; + })]); } return this; }; @@ -3090,18 +4559,33 @@ var Plottable; var Scale = Plottable.Scale; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Util) { var ScaleDomainCoordinator = (function () { + /** + * Creates a ScaleDomainCoordinator. + * + * @constructor + * @param {Scale[]} scales A list of scales whose domains should be linked. + */ function ScaleDomainCoordinator(scales) { var _this = this; + /* This class is responsible for maintaining coordination between linked scales. + It registers event listeners for when one of its scales changes its domain. When the scale + does change its domain, it re-propogates the change to every linked scale. + */ this.rescaleInProgress = false; if (scales == null) { throw new Error("ScaleDomainCoordinator requires scales to coordinate"); } this.scales = scales; - this.scales.forEach(function (s) { return s.broadcaster.registerListener(_this, function (sx) { return _this.rescale(sx); }); }); + this.scales.forEach(function (s) { + return s.broadcaster.registerListener(_this, function (sx) { + return _this.rescale(sx); + }); + }); } ScaleDomainCoordinator.prototype.rescale = function (scale) { if (this.rescaleInProgress) { @@ -3109,7 +4593,9 @@ var Plottable; } this.rescaleInProgress = true; var newDomain = scale.domain(); - this.scales.forEach(function (s) { return s.domain(newDomain); }); + this.scales.forEach(function (s) { + return s.domain(newDomain); + }); this.rescaleInProgress = false; }; return ScaleDomainCoordinator; @@ -3119,18 +4605,35 @@ var Plottable; var Util = Plottable.Util; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Abstract) { var _Drawer = (function () { + /** + * Creates a Drawer + * + * @constructor + * @param{string} key The key associated with this Drawer + */ function _Drawer(key) { this.key = key; } + /** + * Removes the Drawer and its renderArea + */ _Drawer.prototype.remove = function () { if (this.renderArea != null) { this.renderArea.remove(); } }; + + /** + * Draws the data into the renderArea using the attrHash for attributes + * + * @param{any[][]} data The data to be drawn + * @param{attrHash} IAttributeToProjector The list of attributes to set on the data + */ _Drawer.prototype.draw = function (data, attrToProjector) { throw new Error("Abstract Method Not Implemented"); }; @@ -3141,6 +4644,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -3158,6 +4662,7 @@ var Plottable; Rect.prototype.draw = function (data, attrToProjector) { var svgElement = "rect"; var dataElements = this.renderArea.selectAll(svgElement).data(data); + dataElements.enter().append(svgElement); dataElements.attr(attrToProjector); dataElements.exit().remove(); @@ -3169,6 +4674,7 @@ var Plottable; var _Drawer = Plottable._Drawer; })(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; } @@ -3181,9 +4687,9 @@ var Plottable; var Axis = (function (_super) { __extends(Axis, _super); function Axis(scale, orientation, formatter) { - if (formatter === void 0) { formatter = Plottable.Formatters.identity(); } - _super.call(this); + if (typeof formatter === "undefined") { formatter = Plottable.Formatters.identity(); } var _this = this; + _super.call(this); this._width = "auto"; this._height = "auto"; this._endTickLength = 5; @@ -3196,34 +4702,45 @@ var Plottable; } this._scale = scale; this.orient(orientation); + this.classed("axis", true); if (this._isHorizontal()) { this.classed("x-axis", true); - } - else { + } else { this.classed("y-axis", true); } + this.formatter(formatter); - this._scale.broadcaster.registerListener(this, function () { return _this._render(); }); + + this._scale.broadcaster.registerListener(this, function () { + return _this._render(); + }); } Axis.prototype.remove = function () { _super.prototype.remove.call(this); this._scale.broadcaster.deregisterListener(this); }; + Axis.prototype._isHorizontal = function () { return this._orientation === "top" || this._orientation === "bottom"; }; + Axis.prototype._computeWidth = function () { + // to be overridden by subclass logic this._computedWidth = this._maxLabelTickLength(); return this._computedWidth; }; + Axis.prototype._computeHeight = function () { + // to be overridden by subclass logic this._computedHeight = this._maxLabelTickLength(); return this._computedHeight; }; + Axis.prototype._requestedSpace = function (offeredWidth, offeredHeight) { var requestedWidth = this._width; var requestedHeight = this._height; + if (this._isHorizontal()) { if (this._height === "auto") { if (this._computedHeight == null) { @@ -3232,8 +4749,7 @@ var Plottable; requestedHeight = this._computedHeight + this._gutter; } requestedWidth = 0; - } - else { + } else { if (this._width === "auto") { if (this._computedWidth == null) { this._computeWidth(); @@ -3242,6 +4758,7 @@ var Plottable; } requestedHeight = 0; } + return { width: requestedWidth, height: requestedHeight, @@ -3249,30 +4766,39 @@ var Plottable; wantsHeight: this._isHorizontal() && offeredHeight < requestedHeight }; }; + Axis.prototype._isFixedHeight = function () { return this._isHorizontal(); }; + Axis.prototype._isFixedWidth = function () { return !this._isHorizontal(); }; + Axis.prototype._computeLayout = function (xOffset, yOffset, availableWidth, availableHeight) { _super.prototype._computeLayout.call(this, xOffset, yOffset, availableWidth, availableHeight); if (this._isHorizontal()) { this._scale.range([0, this.availableWidth]); - } - else { + } else { this._scale.range([this.availableHeight, 0]); } }; + Axis.prototype._setup = function () { _super.prototype._setup.call(this); this._tickMarkContainer = this.content.append("g").classed(Axis.TICK_MARK_CLASS + "-container", true); this._tickLabelContainer = this.content.append("g").classed(Axis.TICK_LABEL_CLASS + "-container", true); this._baseline = this.content.append("line").classed("baseline", true); }; + + /* + * Function for generating tick values in data-space (as opposed to pixel values). + * To be implemented by subclasses. + */ Axis.prototype._getTickValues = function () { return []; }; + Axis.prototype._doRender = function () { var tickMarkValues = this._getTickValues(); var tickMarks = this._tickMarkContainer.selectAll("." + Axis.TICK_MARK_CLASS).data(tickMarkValues); @@ -3283,6 +4809,7 @@ var Plottable; tickMarks.exit().remove(); this._baseline.attr(this._generateBaselineAttrHash()); }; + Axis.prototype._generateBaselineAttrHash = function () { var baselineAttrHash = { x1: 0, @@ -3290,73 +4817,88 @@ var Plottable; x2: 0, y2: 0 }; + switch (this._orientation) { case "bottom": baselineAttrHash.x2 = this.availableWidth; break; + case "top": baselineAttrHash.x2 = this.availableWidth; baselineAttrHash.y1 = this.availableHeight; baselineAttrHash.y2 = this.availableHeight; break; + case "left": baselineAttrHash.x1 = this.availableWidth; baselineAttrHash.x2 = this.availableWidth; baselineAttrHash.y2 = this.availableHeight; break; + case "right": baselineAttrHash.y2 = this.availableHeight; break; } + return baselineAttrHash; }; + Axis.prototype._generateTickMarkAttrHash = function (isEndTickMark) { var _this = this; - if (isEndTickMark === void 0) { isEndTickMark = false; } + if (typeof isEndTickMark === "undefined") { isEndTickMark = false; } var tickMarkAttrHash = { x1: 0, y1: 0, x2: 0, y2: 0 }; - var scalingFunction = function (d) { return _this._scale.scale(d); }; + + var scalingFunction = function (d) { + return _this._scale.scale(d); + }; if (this._isHorizontal()) { tickMarkAttrHash["x1"] = scalingFunction; tickMarkAttrHash["x2"] = scalingFunction; - } - else { + } else { tickMarkAttrHash["y1"] = scalingFunction; tickMarkAttrHash["y2"] = scalingFunction; } + var tickLength = isEndTickMark ? this._endTickLength : this._tickLength; + switch (this._orientation) { case "bottom": tickMarkAttrHash["y2"] = tickLength; break; + case "top": tickMarkAttrHash["y1"] = this.availableHeight; tickMarkAttrHash["y2"] = this.availableHeight - tickLength; break; + case "left": tickMarkAttrHash["x1"] = this.availableWidth; tickMarkAttrHash["x2"] = this.availableWidth - tickLength; break; + case "right": tickMarkAttrHash["x2"] = tickLength; break; } + return tickMarkAttrHash; }; + Axis.prototype._invalidateLayout = function () { this._computedWidth = null; this._computedHeight = null; _super.prototype._invalidateLayout.call(this); }; + Axis.prototype.width = function (w) { if (w == null) { return this.availableWidth; - } - else { + } else { if (this._isHorizontal()) { throw new Error("width cannot be set on a horizontal Axis"); } @@ -3368,11 +4910,11 @@ var Plottable; return this; } }; + Axis.prototype.height = function (h) { if (h == null) { return this.availableHeight; - } - else { + } else { if (!this._isHorizontal()) { throw new Error("height cannot be set on a vertical Axis"); } @@ -3384,6 +4926,7 @@ var Plottable; return this; } }; + Axis.prototype.formatter = function (formatter) { if (formatter === undefined) { return this._formatter; @@ -3392,11 +4935,11 @@ var Plottable; this._invalidateLayout(); return this; }; + Axis.prototype.tickLength = function (length) { if (length == null) { return this._tickLength; - } - else { + } else { if (length < 0) { throw new Error("tick length must be positive"); } @@ -3405,11 +4948,11 @@ var Plottable; return this; } }; + Axis.prototype.endTickLength = function (length) { if (length == null) { return this._endTickLength; - } - else { + } else { if (length < 0) { throw new Error("end tick length must be positive"); } @@ -3418,19 +4961,19 @@ var Plottable; return this; } }; + Axis.prototype._maxLabelTickLength = function () { if (this.showEndTickLabels()) { return Math.max(this.tickLength(), this.endTickLength()); - } - else { + } else { return this.tickLength(); } }; + Axis.prototype.tickLabelPadding = function (padding) { if (padding == null) { return this._tickLabelPadding; - } - else { + } else { if (padding < 0) { throw new Error("tick label padding must be positive"); } @@ -3439,11 +4982,11 @@ var Plottable; return this; } }; + Axis.prototype.gutter = function (size) { if (size == null) { return this._gutter; - } - else { + } else { if (size < 0) { throw new Error("gutter size must be positive"); } @@ -3452,11 +4995,11 @@ var Plottable; return this; } }; + Axis.prototype.orient = function (newOrientation) { if (newOrientation == null) { return this._orientation; - } - else { + } else { var newOrientationLC = newOrientation.toLowerCase(); if (newOrientationLC !== "top" && newOrientationLC !== "bottom" && newOrientationLC !== "left" && newOrientationLC !== "right") { throw new Error("unsupported orientation"); @@ -3466,6 +5009,7 @@ var Plottable; return this; } }; + Axis.prototype.showEndTickLabels = function (show) { if (show == null) { return this._showEndTickLabels; @@ -3474,12 +5018,15 @@ var Plottable; this._render(); return this; }; + Axis.prototype._hideEndTickLabels = function () { var _this = this; var boundingBox = this.element.select(".bounding-box")[0][0].getBoundingClientRect(); + var isInsideBBox = function (tickBox) { return (Math.floor(boundingBox.left) <= Math.ceil(tickBox.left) && Math.floor(boundingBox.top) <= Math.ceil(tickBox.top) && Math.floor(tickBox.right) <= Math.ceil(boundingBox.left + _this.availableWidth) && Math.floor(tickBox.bottom) <= Math.ceil(boundingBox.top + _this.availableHeight)); }; + var tickLabels = this._tickLabelContainer.selectAll("." + Abstract.Axis.TICK_LABEL_CLASS); if (tickLabels[0].length === 0) { return; @@ -3493,25 +5040,28 @@ var Plottable; d3.select(lastTickLabel).style("visibility", "hidden"); } }; + Axis.prototype._hideOverlappingTickLabels = function () { var visibleTickLabels = this._tickLabelContainer.selectAll("." + Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; }); var lastLabelClientRect; + visibleTickLabels.each(function (d) { var clientRect = this.getBoundingClientRect(); var tickLabel = d3.select(this); if (lastLabelClientRect != null && Plottable.Util.DOM.boxesOverlap(clientRect, lastLabelClientRect)) { tickLabel.style("visibility", "hidden"); - } - else { + } else { lastLabelClientRect = clientRect; tickLabel.style("visibility", "visible"); } }); }; Axis.END_TICK_MARK_CLASS = "end-tick-mark"; + Axis.TICK_MARK_CLASS = "tick-mark"; + Axis.TICK_LABEL_CLASS = "tick-label"; return Axis; })(Abstract.Component); @@ -3520,6 +5070,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -3530,8 +5081,16 @@ var Plottable; (function (Plottable) { (function (Axis) { ; + var Time = (function (_super) { __extends(Time, _super); + /** + * Creates a TimeAxis + * + * @constructor + * @param {TimeScale} scale The scale to base the Axis on. + * @param {string} orientation The orientation of the Axis (top/bottom) + */ function Time(scale, orientation) { orientation = orientation.toLowerCase(); if (orientation !== "top" && orientation !== "bottom") { @@ -3551,30 +5110,43 @@ var Plottable; this._computedHeight = this._maxLabelTickLength() + 2 * this.tickLabelPadding(); return this._computedHeight; }; + Time.prototype.calculateWorstWidth = function (container, format) { + // returns the worst case width for a format + // September 29, 9999 at 12:59.9999 PM Wednesday var longDate = new Date(9999, 8, 29, 12, 59, 9999); return this.measurer(d3.time.format(format)(longDate)).width; }; + Time.prototype.getIntervalLength = function (interval) { var startDate = this._scale.domain()[0]; var endDate = interval.timeUnit.offset(startDate, interval.step); if (endDate > this._scale.domain()[1]) { + // this offset is too large, so just return available width return this.availableWidth; } + + // measure how much space one date can get var stepLength = Math.abs(this._scale.scale(endDate) - this._scale.scale(startDate)); return stepLength; }; + Time.prototype.isEnoughSpace = function (container, interval) { + // compute number of ticks + // if less than a certain threshold var worst = this.calculateWorstWidth(container, interval.formatString) + 2 * this.tickLabelPadding(); var stepLength = Math.min(this.getIntervalLength(interval), this.availableWidth); return worst < stepLength; }; + Time.prototype._setup = function () { _super.prototype._setup.call(this); this._majorTickLabels = this.content.append("g").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); this._minorTickLabels = this.content.append("g").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); this.measurer = Plottable.Util.Text.getTextMeasurer(this._majorTickLabels.append("text")); }; + + // returns a number to index into the major/minor intervals Time.prototype.getTickLevel = function () { for (var i = 0; i < Time.minorIntervals.length; i++) { if (this.isEnoughSpace(this._minorTickLabels, Time.minorIntervals[i]) && this.isEnoughSpace(this._majorTickLabels, Time.majorIntervals[i])) { @@ -3587,21 +5159,25 @@ var Plottable; } return i; }; + Time.prototype._getTickIntervalValues = function (interval) { return this._scale.tickInterval(interval.timeUnit, interval.step); }; + Time.prototype._getTickValues = function () { var index = this.getTickLevel(); var minorTicks = this._getTickIntervalValues(Time.minorIntervals[index]); var majorTicks = this._getTickIntervalValues(Time.majorIntervals[index]); return minorTicks.concat(majorTicks); }; + Time.prototype._measureTextHeight = function (container) { var fakeTickLabel = container.append("g").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); var textHeight = this.measurer(Plottable.Util.Text.HEIGHT_TEXT).height; fakeTickLabel.remove(); return textHeight; }; + Time.prototype.renderTickLabels = function (container, interval, height) { var _this = this; container.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).remove(); @@ -3609,6 +5185,8 @@ var Plottable; tickPos.splice(0, 0, this._scale.domain()[0]); tickPos.push(this._scale.domain()[1]); var shouldCenterText = interval.step === 1; + + // only center when the label should span the whole interval var labelPos = []; if (shouldCenterText) { tickPos.map(function (datum, index) { @@ -3617,12 +5195,15 @@ var Plottable; } labelPos.push(new Date((tickPos[index + 1].valueOf() - tickPos[index].valueOf()) / 2 + tickPos[index].valueOf())); }); - } - else { + } else { labelPos = tickPos; } - labelPos = labelPos.filter(function (d) { return _this.canFitLabelFilter(container, d, d3.time.format(interval.formatString)(d), shouldCenterText); }); - var tickLabels = container.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).data(labelPos, function (d) { return d.valueOf(); }); + labelPos = labelPos.filter(function (d) { + return _this.canFitLabelFilter(container, d, d3.time.format(interval.formatString)(d), shouldCenterText); + }); + var tickLabels = container.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).data(labelPos, function (d) { + return d.valueOf(); + }); var tickLabelsEnter = tickLabels.enter().append("g").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); tickLabelsEnter.append("text"); var xTranslate = shouldCenterText ? 0 : this.tickLabelPadding(); @@ -3632,10 +5213,15 @@ var Plottable; Plottable.Util.DOM.translate(textSelection, xTranslate, yTranslate); } tickLabels.exit().remove(); - tickLabels.attr("transform", function (d) { return "translate(" + _this._scale.scale(d) + ",0)"; }); + tickLabels.attr("transform", function (d) { + return "translate(" + _this._scale.scale(d) + ",0)"; + }); var anchor = shouldCenterText ? "middle" : "start"; - tickLabels.selectAll("text").text(function (d) { return d3.time.format(interval.formatString)(d); }).style("text-anchor", anchor); + tickLabels.selectAll("text").text(function (d) { + return d3.time.format(interval.formatString)(d); + }).style("text-anchor", anchor); }; + Time.prototype.canFitLabelFilter = function (container, position, label, isCentered) { var endPosition; var startPosition; @@ -3643,33 +5229,42 @@ var Plottable; if (isCentered) { endPosition = this._scale.scale(position) + width / 2; startPosition = this._scale.scale(position) - width / 2; - } - else { + } else { endPosition = this._scale.scale(position) + width; startPosition = this._scale.scale(position); } + return endPosition < this.availableWidth && startPosition > 0; }; + Time.prototype.adjustTickLength = function (height, interval) { var tickValues = this._getTickIntervalValues(interval); - var selection = this._tickMarkContainer.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS).filter(function (d) { return tickValues.map(function (x) { return x.valueOf(); }).indexOf(d.valueOf()) >= 0; }); + var selection = this._tickMarkContainer.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS).filter(function (d) { + return tickValues.map(function (x) { + return x.valueOf(); + }).indexOf(d.valueOf()) >= 0; + }); if (this._orientation === "top") { height = this.availableHeight - height; } selection.attr("y2", height); }; + Time.prototype.generateLabellessTicks = function (index) { if (index < 0) { return; } + var smallTicks = this._getTickIntervalValues(Time.minorIntervals[index]); var allTicks = this._getTickValues().concat(smallTicks); + var tickMarks = this._tickMarkContainer.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS).data(allTicks); tickMarks.enter().append("line").classed(Plottable.Abstract.Axis.TICK_MARK_CLASS, true); tickMarks.attr(this._generateTickMarkAttrHash()); tickMarks.exit().remove(); this.adjustTickLength(this.tickLabelPadding(), Time.minorIntervals[index]); }; + Time.prototype._doRender = function () { _super.prototype._doRender.call(this); var index = this.getTickLevel(); @@ -3680,7 +5275,11 @@ var Plottable; if (this.getIntervalLength(Time.minorIntervals[index]) * 1.5 >= totalLength) { this.generateLabellessTicks(index - 1); } + + // make minor ticks shorter this.adjustTickLength(this._maxLabelTickLength() / 2, Time.minorIntervals[index]); + + // however, we need to make major ticks longer, since they may have overlapped with some minor ticks this.adjustTickLength(this._maxLabelTickLength(), Time.majorIntervals[index]); }; Time.minorIntervals = [ @@ -3714,6 +5313,7 @@ var Plottable; { timeUnit: d3.time.year, step: 500, formatString: "%Y" }, { timeUnit: d3.time.year, step: 1000, formatString: "%Y" } ]; + Time.majorIntervals = [ { timeUnit: d3.time.day, step: 1, formatString: "%B %e, %Y" }, { timeUnit: d3.time.day, step: 1, formatString: "%B %e, %Y" }, @@ -3752,6 +5352,7 @@ var Plottable; var Axis = Plottable.Axis; })(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; } @@ -3763,10 +5364,20 @@ var Plottable; (function (Axis) { var Numeric = (function (_super) { __extends(Numeric, _super); + /** + * Creates a NumericAxis. + * + * @constructor + * @param {QuantitativeScale} scale The QuantitativeScale to base the NumericAxis on. + * @param {string} orientation The orientation of the QuantitativeScale (top/bottom/left/right) + * @param {Formatter} [formatter] A function to format tick labels (default Formatters.general(3, false)). + */ function Numeric(scale, orientation, formatter) { - if (formatter === void 0) { formatter = Plottable.Formatters.general(3, false); } + if (typeof formatter === "undefined") { formatter = Plottable.Formatters.general(3, false); } _super.call(this, scale, orientation, formatter); this.tickLabelPositioning = "center"; + // Whether or not first/last tick label will still be displayed even if + // the label is cut off. this.showFirstTickLabel = false; this.showLastTickLabel = false; } @@ -3774,6 +5385,7 @@ var Plottable; _super.prototype._setup.call(this); this.measurer = Plottable.Util.Text.getTextMeasurer(this._tickLabelContainer.append("text").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true)); }; + Numeric.prototype._computeWidth = function () { var _this = this; var tickValues = this._getTickValues(); @@ -3781,39 +5393,49 @@ var Plottable; var formattedValue = _this._formatter(v); return _this.measurer(formattedValue).width; }); + var maxTextLength = d3.max(textLengths); + if (this.tickLabelPositioning === "center") { this._computedWidth = this._maxLabelTickLength() + this.tickLabelPadding() + maxTextLength; - } - else { + } else { this._computedWidth = Math.max(this._maxLabelTickLength(), this.tickLabelPadding() + maxTextLength); } + return this._computedWidth; }; + Numeric.prototype._computeHeight = function () { var textHeight = this.measurer(Plottable.Util.Text.HEIGHT_TEXT).height; + if (this.tickLabelPositioning === "center") { this._computedHeight = this._maxLabelTickLength() + this.tickLabelPadding() + textHeight; - } - else { + } else { this._computedHeight = Math.max(this._maxLabelTickLength(), this.tickLabelPadding() + textHeight); } + return this._computedHeight; }; + Numeric.prototype._getTickValues = function () { return this._scale.ticks(); }; + Numeric.prototype._doRender = function () { _super.prototype._doRender.call(this); + var tickLabelAttrHash = { x: 0, y: 0, dx: "0em", dy: "0.3em" }; + var tickMarkLength = this._maxLabelTickLength(); var tickLabelPadding = this.tickLabelPadding(); + var tickLabelTextAnchor = "middle"; + var labelGroupTransformX = 0; var labelGroupTransformY = 0; var labelGroupShiftX = 0; @@ -3834,8 +5456,7 @@ var Plottable; labelGroupShiftY = tickLabelPadding; break; } - } - else { + } else { switch (this.tickLabelPositioning) { case "top": tickLabelAttrHash["dy"] = "-0.3em"; @@ -3852,6 +5473,7 @@ var Plottable; break; } } + var tickMarkAttrHash = this._generateTickMarkAttrHash(); switch (this._orientation) { case "bottom": @@ -3859,46 +5481,53 @@ var Plottable; tickLabelAttrHash["dy"] = "0.95em"; labelGroupTransformY = tickMarkAttrHash["y1"] + labelGroupShiftY; break; + case "top": tickLabelAttrHash["x"] = tickMarkAttrHash["x1"]; tickLabelAttrHash["dy"] = "-.25em"; labelGroupTransformY = tickMarkAttrHash["y1"] - labelGroupShiftY; break; + case "left": tickLabelTextAnchor = "end"; labelGroupTransformX = tickMarkAttrHash["x1"] - labelGroupShiftX; tickLabelAttrHash["y"] = tickMarkAttrHash["y1"]; break; + case "right": tickLabelTextAnchor = "start"; labelGroupTransformX = tickMarkAttrHash["x1"] + labelGroupShiftX; tickLabelAttrHash["y"] = tickMarkAttrHash["y1"]; break; } + var tickLabelValues = this._getTickValues(); var tickLabels = this._tickLabelContainer.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).data(tickLabelValues); tickLabels.enter().append("text").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); tickLabels.exit().remove(); + tickLabels.style("text-anchor", tickLabelTextAnchor).style("visibility", "visible").attr(tickLabelAttrHash).text(this._formatter); + var labelGroupTransform = "translate(" + labelGroupTransformX + ", " + labelGroupTransformY + ")"; this._tickLabelContainer.attr("transform", labelGroupTransform); + if (!this.showEndTickLabels()) { this._hideEndTickLabels(); } + this._hideOverlappingTickLabels(); }; + Numeric.prototype.tickLabelPosition = function (position) { if (position == null) { return this.tickLabelPositioning; - } - else { + } else { var positionLC = position.toLowerCase(); if (this._isHorizontal()) { if (!(positionLC === "left" || positionLC === "center" || positionLC === "right")) { throw new Error(positionLC + " is not a valid tick label position for a horizontal NumericAxis"); } - } - else { + } else { if (!(positionLC === "top" || positionLC === "center" || positionLC === "bottom")) { throw new Error(positionLC + " is not a valid tick label position for a vertical NumericAxis"); } @@ -3908,28 +5537,25 @@ var Plottable; return this; } }; + Numeric.prototype.showEndTickLabel = function (orientation, show) { if ((this._isHorizontal() && orientation === "left") || (!this._isHorizontal() && orientation === "bottom")) { if (show === undefined) { return this.showFirstTickLabel; - } - else { + } else { this.showFirstTickLabel = show; this._render(); return this; } - } - else if ((this._isHorizontal() && orientation === "right") || (!this._isHorizontal() && orientation === "top")) { + } else if ((this._isHorizontal() && orientation === "right") || (!this._isHorizontal() && orientation === "top")) { if (show === undefined) { return this.showLastTickLabel; - } - else { + } else { this.showLastTickLabel = show; this._render(); return this; } - } - else { + } else { throw new Error("Attempt to show " + orientation + " tick label on a " + (this._isHorizontal() ? "horizontal" : "vertical") + " axis"); } }; @@ -3940,6 +5566,7 @@ var Plottable; var Axis = Plottable.Axis; })(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; } @@ -3951,35 +5578,51 @@ var Plottable; (function (Axis) { var Category = (function (_super) { __extends(Category, _super); + /** + * Creates a CategoryAxis. + * + * A CategoryAxis takes an OrdinalScale and includes word-wrapping algorithms and advanced layout logic to try to + * display the scale as efficiently as possible. + * + * @constructor + * @param {OrdinalScale} scale The scale to base the Axis on. + * @param {string} orientation The orientation of the Axis (top/bottom/left/right) + * @param {Formatter} [formatter] The Formatter for the Axis (default Formatters.identity()) + */ function Category(scale, orientation, formatter) { - if (orientation === void 0) { orientation = "bottom"; } - if (formatter === void 0) { formatter = Plottable.Formatters.identity(); } - _super.call(this, scale, orientation, formatter); + if (typeof orientation === "undefined") { orientation = "bottom"; } + if (typeof formatter === "undefined") { formatter = Plottable.Formatters.identity(); } var _this = this; + _super.call(this, scale, orientation, formatter); this.classed("category-axis", true); if (scale.rangeType() !== "bands") { throw new Error("Only rangeBands category axes are implemented"); } - this._scale.broadcaster.registerListener(this, function () { return _this._invalidateLayout(); }); + this._scale.broadcaster.registerListener(this, function () { + return _this._invalidateLayout(); + }); } Category.prototype._setup = function () { _super.prototype._setup.call(this); this.measurer = new Plottable.Util.Text.CachingCharacterMeasurer(this._tickLabelContainer.append("text")); }; + Category.prototype._requestedSpace = function (offeredWidth, offeredHeight) { var widthRequiredByTicks = this._isHorizontal() ? 0 : this._maxLabelTickLength() + this.tickLabelPadding(); var heightRequiredByTicks = this._isHorizontal() ? this._maxLabelTickLength() + this.tickLabelPadding() : 0; + if (this._scale.domain().length === 0) { return { width: 0, height: 0, wantsWidth: false, wantsHeight: false }; } + var fakeScale = this._scale.copy(); if (this._isHorizontal()) { fakeScale.range([0, offeredWidth]); - } - else { + } else { fakeScale.range([offeredHeight, 0]); } var textResult = this.measureTicks(offeredWidth, offeredHeight, fakeScale, this._scale.domain()); + return { width: textResult.usedWidth + widthRequiredByTicks, height: textResult.usedHeight + heightRequiredByTicks, @@ -3987,19 +5630,29 @@ var Plottable; wantsHeight: !textResult.textFits }; }; + Category.prototype._getTickValues = function () { return this._scale.domain(); }; + Category.prototype.measureTicks = function (axisWidth, axisHeight, scale, dataOrTicks) { var draw = typeof dataOrTicks[0] !== "string"; var self = this; var textWriteResults = []; - var tm = function (s) { return self.measurer.measure(s); }; - var iterator = draw ? function (f) { return dataOrTicks.each(f); } : function (f) { return dataOrTicks.forEach(f); }; + var tm = function (s) { + return self.measurer.measure(s); + }; + var iterator = draw ? function (f) { + return dataOrTicks.each(f); + } : function (f) { + return dataOrTicks.forEach(f); + }; + iterator(function (d) { var bandWidth = scale.fullBandStartAndWidth(d)[1]; var width = self._isHorizontal() ? bandWidth : axisWidth - self._maxLabelTickLength() - self.tickLabelPadding(); var height = self._isHorizontal() ? axisHeight - self._maxLabelTickLength() - self.tickLabelPadding() : bandWidth; + var textWriteResult; var formatter = self._formatter; if (draw) { @@ -4011,24 +5664,35 @@ var Plottable; xAlign: xAlign[self._orientation], yAlign: yAlign[self._orientation] }); - } - else { + } else { textWriteResult = Plottable.Util.Text.writeText(formatter(d), width, height, tm, true); } + textWriteResults.push(textWriteResult); }); + var widthFn = this._isHorizontal() ? d3.sum : d3.max; var heightFn = this._isHorizontal() ? d3.max : d3.sum; return { - textFits: textWriteResults.every(function (t) { return t.textFits; }), - usedWidth: widthFn(textWriteResults, function (t) { return t.usedWidth; }), - usedHeight: heightFn(textWriteResults, function (t) { return t.usedHeight; }) + textFits: textWriteResults.every(function (t) { + return t.textFits; + }), + usedWidth: widthFn(textWriteResults, function (t) { + return t.usedWidth; + }), + usedHeight: heightFn(textWriteResults, function (t) { + return t.usedHeight; + }) }; }; + Category.prototype._doRender = function () { var _this = this; _super.prototype._doRender.call(this); - var tickLabels = this._tickLabelContainer.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).data(this._scale.domain(), function (d) { return d; }); + var tickLabels = this._tickLabelContainer.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).data(this._scale.domain(), function (d) { + return d; + }); + var getTickLabelTransform = function (d, i) { var startAndWidth = _this._scale.fullBandStartAndWidth(d); var bandStartPosition = startAndWidth[0]; @@ -4039,15 +5703,22 @@ var Plottable; tickLabels.enter().append("g").classed(Plottable.Abstract.Axis.TICK_LABEL_CLASS, true); tickLabels.exit().remove(); tickLabels.attr("transform", getTickLabelTransform); + + // erase all text first, then rewrite tickLabels.text(""); this.measureTicks(this.availableWidth, this.availableHeight, this._scale, tickLabels); var translate = this._isHorizontal() ? [this._scale.rangeBand() / 2, 0] : [0, this._scale.rangeBand() / 2]; + var xTranslate = this._orientation === "right" ? this._maxLabelTickLength() + this.tickLabelPadding() : 0; var yTranslate = this._orientation === "bottom" ? this._maxLabelTickLength() + this.tickLabelPadding() : 0; Plottable.Util.DOM.translate(this._tickLabelContainer, xTranslate, yTranslate); Plottable.Util.DOM.translate(this._tickMarkContainer, translate[0], translate[1]); }; + Category.prototype._computeLayout = function (xOrigin, yOrigin, availableWidth, availableHeight) { + // When anyone calls _invalidateLayout, _computeLayout will be called + // on everyone, including this. Since CSS or something might have + // affected the size of the characters, clear the cache. this.measurer.clear(); return _super.prototype._computeLayout.call(this, xOrigin, yOrigin, availableWidth, availableHeight); }; @@ -4058,6 +5729,7 @@ var Plottable; var Axis = Plottable.Axis; })(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; } @@ -4069,9 +5741,16 @@ var Plottable; (function (Component) { var Label = (function (_super) { __extends(Label, _super); + /** + * Creates a Label. + * + * @constructor + * @param {string} [displayText] The text of the Label. + * @param {string} [orientation] The orientation of the Label (horizontal/vertical-left/vertical-right). + */ function Label(displayText, orientation) { - if (displayText === void 0) { displayText = ""; } - if (orientation === void 0) { orientation = "horizontal"; } + if (typeof displayText === "undefined") { displayText = ""; } + if (typeof orientation === "undefined") { orientation = "horizontal"; } _super.call(this); this.classed("label", true); this.text(displayText); @@ -4084,8 +5763,7 @@ var Plottable; } if (orientation === "horizontal" || orientation === "left" || orientation === "right") { this.orientation = orientation; - } - else { + } else { throw new Error(orientation + " is not a valid orientation for LabelComponent"); } this.xAlign("center").yAlign("center"); @@ -4104,10 +5782,12 @@ var Plottable; this.yAlignment = alignmentLC; return this; }; + Label.prototype._requestedSpace = function (offeredWidth, offeredHeight) { var desiredWH = this.measurer(this._text); var desiredWidth = (this.orientation === "horizontal" ? desiredWH.width : desiredWH.height); var desiredHeight = (this.orientation === "horizontal" ? desiredWH.height : desiredWH.width); + return { width: desiredWidth, height: desiredHeight, @@ -4115,22 +5795,24 @@ var Plottable; wantsHeight: desiredHeight > offeredHeight }; }; + Label.prototype._setup = function () { _super.prototype._setup.call(this); this.textContainer = this.content.append("g"); this.measurer = Plottable.Util.Text.getTextMeasurer(this.textContainer.append("text")); this.text(this._text); }; + Label.prototype.text = function (displayText) { if (displayText === undefined) { return this._text; - } - else { + } else { this._text = displayText; this._invalidateLayout(); return this; } }; + Label.prototype._doRender = function () { _super.prototype._doRender.call(this); this.textContainer.text(""); @@ -4138,19 +5820,20 @@ var Plottable; var truncatedText = Plottable.Util.Text.getTruncatedText(this._text, dimension, this.measurer); if (this.orientation === "horizontal") { Plottable.Util.Text.writeLineHorizontally(truncatedText, this.textContainer, this.availableWidth, this.availableHeight, this.xAlignment, this.yAlignment); - } - else { + } else { Plottable.Util.Text.writeLineVertically(truncatedText, this.textContainer, this.availableWidth, this.availableHeight, this.xAlignment, this.yAlignment, this.orientation); } }; + Label.prototype._computeLayout = function (xOffset, yOffset, availableWidth, availableHeight) { - this.measurer = Plottable.Util.Text.getTextMeasurer(this.textContainer.append("text")); + this.measurer = Plottable.Util.Text.getTextMeasurer(this.textContainer.append("text")); // reset it in case fonts have changed _super.prototype._computeLayout.call(this, xOffset, yOffset, availableWidth, availableHeight); return this; }; return Label; })(Plottable.Abstract.Component); Component.Label = Label; + var TitleLabel = (function (_super) { __extends(TitleLabel, _super); function TitleLabel(text, orientation) { @@ -4160,6 +5843,7 @@ var Plottable; return TitleLabel; })(Label); Component.TitleLabel = TitleLabel; + var AxisLabel = (function (_super) { __extends(AxisLabel, _super); function AxisLabel(text, orientation) { @@ -4173,6 +5857,7 @@ var Plottable; var Component = Plottable.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; } @@ -4184,6 +5869,17 @@ var Plottable; (function (Component) { var Legend = (function (_super) { __extends(Legend, _super); + /** + * Creates a Legend. + * + * A legend consists of a series of legend rows, each with a color and label taken from the `colorScale`. + * The rows will be displayed in the order of the `colorScale` domain. + * This legend also allows interactions, through the functions `toggleCallback` and `hoverCallback` + * Setting a callback will also put classes on the individual rows. + * + * @constructor + * @param {Scale.Color} colorScale + */ function Legend(colorScale) { _super.call(this); this.classed("legend", true); @@ -4199,6 +5895,7 @@ var Plottable; this.colorScale.broadcaster.deregisterListener(this); } }; + Legend.prototype.toggleCallback = function (callback) { if (callback !== undefined) { this._toggleCallback = callback; @@ -4206,11 +5903,11 @@ var Plottable; this.updateListeners(); this.updateClasses(); return this; - } - else { + } else { return this._toggleCallback; } }; + Legend.prototype.hoverCallback = function (callback) { if (callback !== undefined) { this._hoverCallback = callback; @@ -4218,11 +5915,11 @@ var Plottable; this.updateListeners(); this.updateClasses(); return this; - } - else { + } else { return this._hoverCallback; } }; + Legend.prototype.scale = function (scale) { var _this = this; if (scale != null) { @@ -4230,14 +5927,16 @@ var Plottable; this.colorScale.broadcaster.deregisterListener(this); } this.colorScale = scale; - this.colorScale.broadcaster.registerListener(this, function () { return _this.updateDomain(); }); + this.colorScale.broadcaster.registerListener(this, function () { + return _this.updateDomain(); + }); this.updateDomain(); return this; - } - else { + } else { return this.colorScale; } }; + Legend.prototype.updateDomain = function () { if (this._toggleCallback != null) { this.isOff = Plottable.Util.Methods.intersection(this.isOff, d3.set(this.scale().domain())); @@ -4247,19 +5946,23 @@ var Plottable; } this._invalidateLayout(); }; + Legend.prototype._computeLayout = function (xOrigin, yOrigin, availableWidth, availableHeight) { _super.prototype._computeLayout.call(this, xOrigin, yOrigin, availableWidth, availableHeight); var textHeight = this.measureTextHeight(); var totalNumRows = this.colorScale.domain().length; this.nRowsDrawn = Math.min(totalNumRows, Math.floor(this.availableHeight / textHeight)); }; + Legend.prototype._requestedSpace = function (offeredWidth, offeredHeight) { var textHeight = this.measureTextHeight(); var totalNumRows = this.colorScale.domain().length; var rowsICanFit = Math.min(totalNumRows, Math.floor((offeredHeight - 2 * Legend.MARGIN) / textHeight)); var fakeLegendEl = this.content.append("g").classed(Legend.SUBELEMENT_CLASS, true); var measure = Plottable.Util.Text.getTextMeasurer(fakeLegendEl.append("text")); - var maxWidth = d3.max(this.colorScale.domain(), function (d) { return measure(d).width; }); + var maxWidth = d3.max(this.colorScale.domain(), function (d) { + return measure(d).width; + }); fakeLegendEl.remove(); maxWidth = maxWidth === undefined ? 0 : maxWidth; var desiredWidth = rowsICanFit === 0 ? 0 : maxWidth + textHeight + 2 * Legend.MARGIN; @@ -4271,26 +5974,36 @@ var Plottable; wantsHeight: offeredHeight < desiredHeight }; }; + Legend.prototype.measureTextHeight = function () { + // note: can't be called before anchoring atm var fakeLegendEl = this.content.append("g").classed(Legend.SUBELEMENT_CLASS, true); var textHeight = Plottable.Util.Text.getTextMeasurer(fakeLegendEl.append("text"))(Plottable.Util.Text.HEIGHT_TEXT).height; + + // HACKHACK if (textHeight === 0) { textHeight = 1; } fakeLegendEl.remove(); return textHeight; }; + Legend.prototype._doRender = function () { _super.prototype._doRender.call(this); var domain = this.colorScale.domain().slice(0, this.nRowsDrawn); var textHeight = this.measureTextHeight(); var availableWidth = this.availableWidth - textHeight - Legend.MARGIN; var r = textHeight * 0.3; - var legend = this.content.selectAll("." + Legend.SUBELEMENT_CLASS).data(domain, function (d) { return d; }); + var legend = this.content.selectAll("." + Legend.SUBELEMENT_CLASS).data(domain, function (d) { + return d; + }); var legendEnter = legend.enter().append("g").classed(Legend.SUBELEMENT_CLASS, true); + legendEnter.append("circle"); legendEnter.append("g").classed("text-container", true); + legend.exit().remove(); + legend.selectAll("circle").attr("cx", textHeight / 2).attr("cy", textHeight / 2).attr("r", r).attr("fill", this.colorScale._d3Scale); legend.selectAll("g.text-container").text("").attr("transform", "translate(" + textHeight + ", 0)").each(function (d) { var d3this = d3.select(this); @@ -4299,12 +6012,15 @@ var Plottable; var writeLineMeasure = measure(writeLine); Plottable.Util.Text.writeLineHorizontally(writeLine, d3this, writeLineMeasure.width, writeLineMeasure.height); }); + legend.attr("transform", function (d) { return "translate(" + Legend.MARGIN + "," + (domain.indexOf(d) * textHeight + Legend.MARGIN) + ")"; }); + this.updateClasses(); this.updateListeners(); }; + Legend.prototype.updateListeners = function () { var _this = this; if (!this._isSetup) { @@ -4312,35 +6028,40 @@ var Plottable; } var dataSelection = this.content.selectAll("." + Legend.SUBELEMENT_CLASS); if (this._hoverCallback != null) { - var hoverRow = function (mouseover) { return function (datum) { - _this.datumCurrentlyFocusedOn = mouseover ? datum : undefined; - _this._hoverCallback(_this.datumCurrentlyFocusedOn); - _this.updateClasses(); - }; }; + // tag the element that is being hovered over with the class "focus" + // this callback will trigger with the specific element being hovered over. + var hoverRow = function (mouseover) { + return function (datum) { + _this.datumCurrentlyFocusedOn = mouseover ? datum : undefined; + _this._hoverCallback(_this.datumCurrentlyFocusedOn); + _this.updateClasses(); + }; + }; dataSelection.on("mouseover", hoverRow(true)); dataSelection.on("mouseout", hoverRow(false)); - } - else { + } else { + // remove all mouseover/mouseout listeners dataSelection.on("mouseover", null); dataSelection.on("mouseout", null); } + if (this._toggleCallback != null) { dataSelection.on("click", function (datum) { var turningOn = _this.isOff.has(datum); if (turningOn) { _this.isOff.remove(datum); - } - else { + } else { _this.isOff.add(datum); } _this._toggleCallback(datum, turningOn); _this.updateClasses(); }); - } - else { + } else { + // remove all click listeners dataSelection.on("click", null); } }; + Legend.prototype.updateClasses = function () { var _this = this; if (!this._isSetup) { @@ -4348,18 +6069,22 @@ var Plottable; } var dataSelection = this.content.selectAll("." + Legend.SUBELEMENT_CLASS); if (this._hoverCallback != null) { - dataSelection.classed("focus", function (d) { return _this.datumCurrentlyFocusedOn === d; }); + dataSelection.classed("focus", function (d) { + return _this.datumCurrentlyFocusedOn === d; + }); dataSelection.classed("hover", this.datumCurrentlyFocusedOn !== undefined); - } - else { + } else { dataSelection.classed("hover", false); dataSelection.classed("focus", false); } if (this._toggleCallback != null) { - dataSelection.classed("toggled-on", function (d) { return !_this.isOff.has(d); }); - dataSelection.classed("toggled-off", function (d) { return _this.isOff.has(d); }); - } - else { + dataSelection.classed("toggled-on", function (d) { + return !_this.isOff.has(d); + }); + dataSelection.classed("toggled-off", function (d) { + return _this.isOff.has(d); + }); + } else { dataSelection.classed("toggled-on", false); dataSelection.classed("toggled-off", false); } @@ -4373,6 +6098,7 @@ var Plottable; var Component = Plottable.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; } @@ -4384,9 +6110,16 @@ var Plottable; (function (Component) { var Gridlines = (function (_super) { __extends(Gridlines, _super); + /** + * Creates a set of Gridlines. + * @constructor + * + * @param {QuantitativeScale} xScale The scale to base the x gridlines on. Pass null if no gridlines are desired. + * @param {QuantitativeScale} yScale The scale to base the y gridlines on. Pass null if no gridlines are desired. + */ function Gridlines(xScale, yScale) { - _super.call(this); var _this = this; + _super.call(this); if (xScale == null && yScale == null) { throw new Error("Gridlines must have at least one scale"); } @@ -4394,10 +6127,14 @@ var Plottable; this.xScale = xScale; this.yScale = yScale; if (this.xScale != null) { - this.xScale.broadcaster.registerListener(this, function () { return _this._render(); }); + this.xScale.broadcaster.registerListener(this, function () { + return _this._render(); + }); } if (this.yScale != null) { - this.yScale.broadcaster.registerListener(this, function () { return _this._render(); }); + this.yScale.broadcaster.registerListener(this, function () { + return _this._render(); + }); } } Gridlines.prototype.remove = function () { @@ -4410,35 +6147,47 @@ var Plottable; } return this; }; + Gridlines.prototype._setup = function () { _super.prototype._setup.call(this); this.xLinesContainer = this.content.append("g").classed("x-gridlines", true); this.yLinesContainer = this.content.append("g").classed("y-gridlines", true); }; + Gridlines.prototype._doRender = function () { _super.prototype._doRender.call(this); this.redrawXLines(); this.redrawYLines(); }; + Gridlines.prototype.redrawXLines = function () { var _this = this; if (this.xScale != null) { var xTicks = this.xScale.ticks(); - var getScaledXValue = function (tickVal) { return _this.xScale.scale(tickVal); }; + var getScaledXValue = function (tickVal) { + return _this.xScale.scale(tickVal); + }; var xLines = this.xLinesContainer.selectAll("line").data(xTicks); xLines.enter().append("line"); - xLines.attr("x1", getScaledXValue).attr("y1", 0).attr("x2", getScaledXValue).attr("y2", this.availableHeight).classed("zeroline", function (t) { return t === 0; }); + xLines.attr("x1", getScaledXValue).attr("y1", 0).attr("x2", getScaledXValue).attr("y2", this.availableHeight).classed("zeroline", function (t) { + return t === 0; + }); xLines.exit().remove(); } }; + Gridlines.prototype.redrawYLines = function () { var _this = this; if (this.yScale != null) { var yTicks = this.yScale.ticks(); - var getScaledYValue = function (tickVal) { return _this.yScale.scale(tickVal); }; + var getScaledYValue = function (tickVal) { + return _this.yScale.scale(tickVal); + }; var yLines = this.yLinesContainer.selectAll("line").data(yTicks); yLines.enter().append("line"); - yLines.attr("x1", 0).attr("y1", getScaledYValue).attr("x2", this.availableWidth).attr("y2", getScaledYValue).classed("zeroline", function (t) { return t === 0; }); + yLines.attr("x1", 0).attr("y1", getScaledYValue).attr("x2", this.availableWidth).attr("y2", getScaledYValue).classed("zeroline", function (t) { + return t === 0; + }); yLines.exit().remove(); } }; @@ -4449,6 +6198,7 @@ var Plottable; var Component = Plottable.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; } @@ -4460,6 +6210,14 @@ var Plottable; (function (Plot) { var Scatter = (function (_super) { __extends(Scatter, _super); + /** + * Creates a ScatterPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function Scatter(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this._animators = { @@ -4467,9 +6225,11 @@ var Plottable; "circles": new Plottable.Animator.IterativeDelay().duration(250).delay(5) }; this.classed("scatter-plot", true); - this.project("r", 3); - this.project("opacity", 0.6); - this.project("fill", function () { return Plottable.Core.Colors.INDIGO; }); + this.project("r", 3); // default + this.project("opacity", 0.6); // default + this.project("fill", function () { + return Plottable.Core.Colors.INDIGO; + }); // default } Scatter.prototype.project = function (attrToSet, accessor, scale) { attrToSet = attrToSet === "cx" ? "x" : attrToSet; @@ -4477,21 +6237,28 @@ var Plottable; _super.prototype.project.call(this, attrToSet, accessor, scale); return this; }; + Scatter.prototype._paint = function () { _super.prototype._paint.call(this); + var attrToProjector = this._generateAttrToProjector(); attrToProjector["cx"] = attrToProjector["x"]; attrToProjector["cy"] = attrToProjector["y"]; delete attrToProjector["x"]; delete attrToProjector["y"]; + var circles = this.renderArea.selectAll("circle").data(this._dataSource.data()); circles.enter().append("circle"); + if (this._dataChanged) { var rFunction = attrToProjector["r"]; - attrToProjector["r"] = function () { return 0; }; + attrToProjector["r"] = function () { + return 0; + }; this._applyAnimatedAttributes(circles, "circles-reset", attrToProjector); attrToProjector["r"] = rFunction; } + this._applyAnimatedAttributes(circles, "circles", attrToProjector); circles.exit().remove(); }; @@ -4502,6 +6269,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4513,16 +6281,29 @@ var Plottable; (function (Plot) { var Grid = (function (_super) { __extends(Grid, _super); + /** + * Creates a GridPlot. + * + * @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. + */ function Grid(dataset, xScale, yScale, colorScale) { _super.call(this, dataset, xScale, yScale); this._animators = { "cells": new Plottable.Animator.Null() }; this.classed("grid-plot", 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", "value", colorScale); + this.project("fill", "value", colorScale); // default } Grid.prototype.project = function (attrToSet, accessor, scale) { _super.prototype.project.call(this, attrToSet, accessor, scale); @@ -4531,15 +6312,24 @@ var Plottable; } return this; }; + Grid.prototype._paint = function () { _super.prototype._paint.call(this); + var cells = this.renderArea.selectAll("rect").data(this._dataSource.data()); cells.enter().append("rect"); + var xStep = this.xScale.rangeBand(); var yStep = this.yScale.rangeBand(); + var attrToProjector = this._generateAttrToProjector(); - attrToProjector["width"] = function () { return xStep; }; - attrToProjector["height"] = function () { return yStep; }; + attrToProjector["width"] = function () { + return xStep; + }; + attrToProjector["height"] = function () { + return yStep; + }; + this._applyAnimatedAttributes(cells, "cells", attrToProjector); cells.exit().remove(); }; @@ -4550,6 +6340,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4559,8 +6350,20 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Abstract) { + /* + * An Abstract.BarPlot is the base implementation for HorizontalBarPlot and + * VerticalBarPlot. It should not be used on its own. + */ var BarPlot = (function (_super) { __extends(BarPlot, _super); + /** + * Creates an AbstractBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function BarPlot(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this._baselineValue = 0; @@ -4571,7 +6374,13 @@ var Plottable; "baseline": new Plottable.Animator.Null() }; this.classed("bar-plot", true); - this.project("fill", function () { return Plottable.Core.Colors.INDIGO; }); + this.project("fill", function () { + return Plottable.Core.Colors.INDIGO; + }); + + // because this._baselineValue was not initialized during the super() + // call, we must call this in order to get this._baselineValue + // to be used by the Domainer. this.baseline(this._baselineValue); } BarPlot.prototype._setup = function () { @@ -4579,34 +6388,52 @@ var Plottable; this._baseline = this.renderArea.append("line").classed("baseline", true); this._bars = this.renderArea.selectAll("rect").data([]); }; + BarPlot.prototype._paint = function () { _super.prototype._paint.call(this); this._bars = this.renderArea.selectAll("rect").data(this._dataSource.data()); this._bars.enter().append("rect"); + var primaryScale = this._isVertical ? this.yScale : this.xScale; var scaledBaseline = primaryScale.scale(this._baselineValue); var positionAttr = this._isVertical ? "y" : "x"; var dimensionAttr = this._isVertical ? "height" : "width"; + if (this._dataChanged && this._animate) { var resetAttrToProjector = this._generateAttrToProjector(); - resetAttrToProjector[positionAttr] = function () { return scaledBaseline; }; - resetAttrToProjector[dimensionAttr] = function () { return 0; }; + resetAttrToProjector[positionAttr] = function () { + return scaledBaseline; + }; + resetAttrToProjector[dimensionAttr] = function () { + return 0; + }; this._applyAnimatedAttributes(this._bars, "bars-reset", resetAttrToProjector); } + var attrToProjector = this._generateAttrToProjector(); if (attrToProjector["fill"] != null) { - this._bars.attr("fill", attrToProjector["fill"]); + this._bars.attr("fill", attrToProjector["fill"]); // so colors don't animate } this._applyAnimatedAttributes(this._bars, "bars", attrToProjector); + this._bars.exit().remove(); + var baselineAttr = { "x1": this._isVertical ? 0 : scaledBaseline, "y1": this._isVertical ? scaledBaseline : 0, "x2": this._isVertical ? this.availableWidth : scaledBaseline, "y2": this._isVertical ? scaledBaseline : this.availableHeight }; + this._applyAnimatedAttributes(this._baseline, "baseline", baselineAttr); }; + + /** + * Sets the baseline for the bars to the specified value. + * + * @param {number} value The value to position the baseline at. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ BarPlot.prototype.baseline = function (value) { this._baselineValue = value; this._updateXDomainer(); @@ -4614,6 +6441,15 @@ var Plottable; this._render(); return this; }; + + /** + * Sets the bar alignment relative to the independent axis. + * VerticalBarPlot supports "left", "center", "right" + * HorizontalBarPlot supports "top", "center", "bottom" + * + * @param {string} alignment The desired alignment. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ BarPlot.prototype.barAlignment = function (alignment) { var alignmentLC = alignment.toLowerCase(); var align2factor = this.constructor._BarAlignmentToFactor; @@ -4621,83 +6457,103 @@ var Plottable; throw new Error("unsupported bar alignment"); } this._barAlignmentFactor = align2factor[alignmentLC]; + this._render(); return this; }; + BarPlot.prototype.parseExtent = function (input) { if (typeof (input) === "number") { return { min: input, max: input }; - } - else if (input instanceof Object && "min" in input && "max" in input) { + } else if (input instanceof Object && "min" in input && "max" in input) { return input; - } - else { + } else { throw new Error("input '" + input + "' can't be parsed as an IExtent"); } }; + BarPlot.prototype.selectBar = function (xValOrExtent, yValOrExtent, select) { - if (select === void 0) { select = true; } + if (typeof select === "undefined") { select = true; } if (!this._isSetup) { return null; } + var selectedBars = []; + var xExtent = this.parseExtent(xValOrExtent); var yExtent = this.parseExtent(yValOrExtent); + + // the SVGRects are positioned with sub-pixel accuracy (the default unit + // for the x, y, height & width attributes), but user selections (e.g. via + // mouse events) usually have pixel accuracy. A tolerance of half-a-pixel + // seems appropriate: var tolerance = 0.5; + + // currently, linear scan the bars. If inversion is implemented on non-numeric scales we might be able to do better. this._bars.each(function (d) { var bbox = this.getBBox(); if (bbox.x + bbox.width >= xExtent.min - tolerance && bbox.x <= xExtent.max + tolerance && bbox.y + bbox.height >= yExtent.min - tolerance && bbox.y <= yExtent.max + tolerance) { selectedBars.push(this); } }); + if (selectedBars.length > 0) { var selection = d3.selectAll(selectedBars); selection.classed("selected", select); return selection; - } - else { + } else { return null; } }; + + /** + * Deselects all bars. + * @return {AbstractBarPlot} The calling AbstractBarPlot. + */ BarPlot.prototype.deselectAll = function () { if (this._isSetup) { this._bars.classed("selected", false); } return this; }; + BarPlot.prototype._updateDomainer = function (scale) { if (scale instanceof Abstract.QuantitativeScale) { var qscale = scale; if (!qscale._userSetDomainer) { if (this._baselineValue != null) { qscale.domainer().addPaddingException(this._baselineValue, "BAR_PLOT+" + this._plottableID).addIncludedValue(this._baselineValue, "BAR_PLOT+" + this._plottableID); - } - else { + } else { qscale.domainer().removePaddingException("BAR_PLOT+" + this._plottableID).removeIncludedValue("BAR_PLOT+" + this._plottableID); } qscale.domainer().pad(); } + + // prepending "BAR_PLOT" is unnecessary but reduces likely of user accidentally creating collisions qscale._autoDomainIfAutomaticMode(); } }; + BarPlot.prototype._updateYDomainer = function () { if (this._isVertical) { this._updateDomainer(this.yScale); - } - else { + } else { _super.prototype._updateYDomainer.call(this); } }; + BarPlot.prototype._updateXDomainer = function () { if (!this._isVertical) { this._updateDomainer(this.xScale); - } - else { + } else { _super.prototype._updateXDomainer.call(this); } }; + BarPlot.prototype._generateAttrToProjector = function () { var _this = this; + // Primary scale/direction: the "length" of the bars + // Secondary scale/direction: the "width" of the bars var attrToProjector = _super.prototype._generateAttrToProjector.call(this); var primaryScale = this._isVertical ? this.yScale : this.xScale; var secondaryScale = this._isVertical ? this.xScale : this.yScale; @@ -4707,28 +6563,42 @@ var Plottable; var scaledBaseline = primaryScale.scale(this._baselineValue); if (attrToProjector["width"] == null) { var constantWidth = bandsMode ? secondaryScale.rangeBand() : BarPlot.DEFAULT_WIDTH; - attrToProjector["width"] = function (d, i) { return constantWidth; }; + attrToProjector["width"] = function (d, i) { + return constantWidth; + }; } + var positionF = attrToProjector[secondaryAttr]; var widthF = attrToProjector["width"]; if (!bandsMode) { - attrToProjector[secondaryAttr] = function (d, i) { return positionF(d, i) - widthF(d, i) * _this._barAlignmentFactor; }; - } - else { + attrToProjector[secondaryAttr] = function (d, i) { + return positionF(d, i) - widthF(d, i) * _this._barAlignmentFactor; + }; + } else { var bandWidth = secondaryScale.rangeBand(); - attrToProjector[secondaryAttr] = function (d, i) { return positionF(d, i) - widthF(d, i) / 2 + bandWidth / 2; }; + attrToProjector[secondaryAttr] = function (d, i) { + return positionF(d, i) - widthF(d, i) / 2 + bandWidth / 2; + }; } + var originalPositionFn = attrToProjector[primaryAttr]; attrToProjector[primaryAttr] = function (d, i) { var originalPos = originalPositionFn(d, i); + + // If it is past the baseline, it should start at the baselin then width/height + // carries it over. If it's not past the baseline, leave it at original position and + // then width/height carries it to baseline return (originalPos > scaledBaseline) ? scaledBaseline : originalPos; }; + attrToProjector["height"] = function (d, i) { return Math.abs(scaledBaseline - originalPositionFn(d, i)); }; + return attrToProjector; }; BarPlot.DEFAULT_WIDTH = 10; + BarPlot._BarAlignmentToFactor = {}; return BarPlot; })(Abstract.XYPlot); @@ -4737,6 +6607,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -4746,8 +6617,25 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Plot) { + /** + * A VerticalBarPlot draws bars vertically. + * Key projected attributes: + * - "width" - the horizontal width of a bar. + * - if an ordinal scale is attached, this defaults to ordinalScale.rangeBand() + * - if a quantitative scale is attached, this defaults to 10 + * - "x" - the horizontal position of a bar + * - "y" - the vertical height of a bar + */ var VerticalBar = (function (_super) { __extends(VerticalBar, _super); + /** + * Creates a VerticalBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {QuantitativeScale} yScale The y scale to use. + */ function VerticalBar(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this._isVertical = true; @@ -4763,6 +6651,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4772,8 +6661,25 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Plot) { + /** + * A HorizontalBarPlot draws bars horizontally. + * Key projected attributes: + * - "width" - the vertical height of a bar (since the bar is rotated horizontally) + * - if an ordinal scale is attached, this defaults to ordinalScale.rangeBand() + * - if a quantitative scale is attached, this defaults to 10 + * - "x" - the horizontal length of a bar + * - "y" - the vertical position of a bar + */ var HorizontalBar = (function (_super) { __extends(HorizontalBar, _super); + /** + * Creates a HorizontalBarPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {QuantitativeScale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function HorizontalBar(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this.isVertical = false; @@ -4781,8 +6687,12 @@ var Plottable; HorizontalBar.prototype._updateXDomainer = function () { this._updateDomainer(this.xScale); }; + HorizontalBar.prototype._generateAttrToProjector = function () { var attrToProjector = _super.prototype._generateAttrToProjector.call(this); + + // by convention, for API users the 2ndary dimension of a bar is always called its "width", so + // the "width" of a horziontal bar plot is actually its "height" from the perspective of a svg rect var widthF = attrToProjector["width"]; attrToProjector["width"] = attrToProjector["height"]; attrToProjector["height"] = widthF; @@ -4796,6 +6706,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4807,6 +6718,14 @@ var Plottable; (function (Plot) { var Line = (function (_super) { __extends(Line, _super); + /** + * Creates a LinePlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function Line(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this._animators = { @@ -4814,27 +6733,38 @@ var Plottable; "line": new Plottable.Animator.Default().duration(600).easing("exp-in-out") }; this.classed("line-plot", true); - this.project("stroke", function () { return Plottable.Core.Colors.INDIGO; }); - this.project("stroke-width", function () { return "2px"; }); + this.project("stroke", function () { + return Plottable.Core.Colors.INDIGO; + }); // default + this.project("stroke-width", function () { + return "2px"; + }); // default } Line.prototype._setup = function () { _super.prototype._setup.call(this); this.linePath = this.renderArea.append("path").classed("line", true); }; + Line.prototype._getResetYFunction = function () { + // gets the y-value generator for the animation start point var yDomain = this.yScale.domain(); var domainMax = Math.max(yDomain[0], yDomain[1]); var domainMin = Math.min(yDomain[0], yDomain[1]); + + // start from zero, or the closest domain value to zero + // avoids lines zooming on from offscreen. var startValue = 0; if (domainMax < 0) { startValue = domainMax; - } - else if (domainMin > 0) { + } else if (domainMin > 0) { startValue = domainMin; } var scaledStartValue = this.yScale.scale(startValue); - return function (d, i) { return scaledStartValue; }; + return function (d, i) { + return scaledStartValue; + }; }; + Line.prototype._generateAttrToProjector = function () { var attrToProjector = _super.prototype._generateAttrToProjector.call(this); var wholeDatumAttributes = this._wholeDatumAttributes(); @@ -4847,14 +6777,14 @@ var Plottable; attrToProjector[attribute] = function (data, i) { if (data.length > 0) { return projector(data[0], i); - } - else { + } else { return null; } }; }); return attrToProjector; }; + Line.prototype._paint = function () { _super.prototype._paint.call(this); var attrToProjector = this._generateAttrToProjector(); @@ -4862,14 +6792,18 @@ var Plottable; var yFunction = attrToProjector["y"]; delete attrToProjector["x"]; delete attrToProjector["y"]; + this.linePath.datum(this._dataSource.data()); + if (this._dataChanged) { attrToProjector["d"] = d3.svg.line().x(xFunction).y(this._getResetYFunction()); this._applyAnimatedAttributes(this.linePath, "line-reset", attrToProjector); } + attrToProjector["d"] = d3.svg.line().x(xFunction).y(yFunction); this._applyAnimatedAttributes(this.linePath, "line", attrToProjector); }; + Line.prototype._wholeDatumAttributes = function () { return ["x", "y"]; }; @@ -4880,6 +6814,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4889,15 +6824,32 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Plot) { + /** + * An AreaPlot draws a filled region (area) between the plot's projected "y" and projected "y0" values. + */ var Area = (function (_super) { __extends(Area, _super); + /** + * Creates an AreaPlot. + * + * @constructor + * @param {IDataset} dataset The dataset to render. + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function Area(dataset, xScale, yScale) { _super.call(this, dataset, xScale, yScale); this.classed("area-plot", true); - this.project("y0", 0, yScale); - this.project("fill", function () { return Plottable.Core.Colors.INDIGO; }); - this.project("fill-opacity", function () { return 0.25; }); - this.project("stroke", function () { return Plottable.Core.Colors.INDIGO; }); + this.project("y0", 0, yScale); // default + this.project("fill", function () { + return Plottable.Core.Colors.INDIGO; + }); // default + this.project("fill-opacity", function () { + return 0.25; + }); // default + this.project("stroke", function () { + return Plottable.Core.Colors.INDIGO; + }); // default this._animators["area-reset"] = new Plottable.Animator.Null(); this._animators["area"] = new Plottable.Animator.Default().duration(600).easing("exp-in-out"); } @@ -4905,29 +6857,35 @@ var Plottable; _super.prototype._setup.call(this); this.areaPath = this.renderArea.append("path").classed("area", true); }; + Area.prototype._onDataSourceUpdate = function () { _super.prototype._onDataSourceUpdate.call(this); if (this.yScale != null) { this._updateYDomainer(); } }; + Area.prototype._updateYDomainer = function () { _super.prototype._updateYDomainer.call(this); var scale = this.yScale; + var y0Projector = this._projectors["y0"]; var y0Accessor = y0Projector != null ? y0Projector.accessor : null; var extent = y0Accessor != null ? this.dataSource()._getExtent(y0Accessor) : []; var constantBaseline = (extent.length === 2 && extent[0] === extent[1]) ? extent[0] : null; + if (!scale._userSetDomainer) { if (constantBaseline != null) { scale.domainer().addPaddingException(constantBaseline, "AREA_PLOT+" + this._plottableID); - } - else { + } else { scale.domainer().removePaddingException("AREA_PLOT+" + this._plottableID); } + + // prepending "AREA_PLOT" is unnecessary but reduces likely of user accidentally creating collisions scale._autoDomainIfAutomaticMode(); } }; + Area.prototype.project = function (attrToSet, accessor, scale) { _super.prototype.project.call(this, attrToSet, accessor, scale); if (attrToSet === "y0") { @@ -4935,9 +6893,11 @@ var Plottable; } return this; }; + Area.prototype._getResetYFunction = function () { return this._generateAttrToProjector()["y0"]; }; + Area.prototype._paint = function () { _super.prototype._paint.call(this); var attrToProjector = this._generateAttrToProjector(); @@ -4947,14 +6907,18 @@ var Plottable; delete attrToProjector["x"]; delete attrToProjector["y0"]; delete attrToProjector["y"]; + this.areaPath.datum(this._dataSource.data()); + if (this._dataChanged) { attrToProjector["d"] = d3.svg.area().x(xFunction).y0(y0Function).y1(this._getResetYFunction()); this._applyAnimatedAttributes(this.areaPath, "area-reset", attrToProjector); } + attrToProjector["d"] = d3.svg.area().x(xFunction).y0(y0Function).y1(yFunction); this._applyAnimatedAttributes(this.areaPath, "area", attrToProjector); }; + Area.prototype._wholeDatumAttributes = function () { var wholeDatumAttributes = _super.prototype._wholeDatumAttributes.call(this); wholeDatumAttributes.push("y0"); @@ -4967,6 +6931,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -4978,6 +6943,13 @@ var Plottable; (function (Abstract) { var NewStyleBarPlot = (function (_super) { __extends(NewStyleBarPlot, _super); + /** + * Creates an NewStyleBarPlot. + * + * @constructor + * @param {Scale} xScale The x scale to use. + * @param {Scale} yScale The y scale to use. + */ function NewStyleBarPlot(xScale, yScale) { _super.call(this, xScale, yScale); this._baselineValue = 0; @@ -4988,18 +6960,25 @@ var Plottable; "baseline": new Plottable.Animator.Null() }; this.classed("bar-plot", true); - this.project("fill", function () { return Plottable.Core.Colors.INDIGO; }); + this.project("fill", function () { + return Plottable.Core.Colors.INDIGO; + }); + + // super() doesn't set baseline this.baseline(this._baselineValue); } NewStyleBarPlot.prototype._getDrawer = function (key) { return new Plottable._Drawer.Rect(key); }; + NewStyleBarPlot.prototype._setup = function () { _super.prototype._setup.call(this); this._baseline = this.renderArea.append("line").classed("baseline", true); }; + NewStyleBarPlot.prototype._paint = function () { _super.prototype._paint.call(this); + var primaryScale = this._isVertical ? this.yScale : this.xScale; var scaledBaseline = primaryScale.scale(this._baselineValue); var baselineAttr = { @@ -5010,15 +6989,25 @@ var Plottable; }; this._applyAnimatedAttributes(this._baseline, "baseline", baselineAttr); }; + + /** + * Sets the baseline for the bars to the specified value. + * + * @param {number} value The value to position the baseline at. + * @return {NewStyleBarPlot} The calling NewStyleBarPlot. + */ NewStyleBarPlot.prototype.baseline = function (value) { return Abstract.BarPlot.prototype.baseline.apply(this, [value]); }; + NewStyleBarPlot.prototype._updateDomainer = function (scale) { return Abstract.BarPlot.prototype._updateDomainer.apply(this, [scale]); }; + NewStyleBarPlot.prototype._generateAttrToProjector = function () { return Abstract.BarPlot.prototype._generateAttrToProjector.apply(this); }; + NewStyleBarPlot.prototype._updateXDomainer = function () { return Abstract.BarPlot.prototype._updateXDomainer.apply(this); }; @@ -5034,6 +7023,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -5053,23 +7043,35 @@ var Plottable; ClusteredBar.prototype._generateAttrToProjector = function () { var _this = this; var attrToProjector = _super.prototype._generateAttrToProjector.call(this); + + // the width is constant, so set the inner scale range to that var widthF = attrToProjector["width"]; this.innerScale.range([0, widthF(null, 0)]); - attrToProjector["width"] = function (d, i) { return _this.innerScale.rangeBand(); }; - attrToProjector["x"] = function (d) { return d._PLOTTABLE_PROTECTED_FIELD_X; }; + attrToProjector["width"] = function (d, i) { + return _this.innerScale.rangeBand(); + }; + attrToProjector["x"] = function (d) { + return d._PLOTTABLE_PROTECTED_FIELD_X; + }; return attrToProjector; }; + ClusteredBar.prototype.cluster = function (accessor) { var _this = this; this.innerScale.domain(this._datasetKeysInOrder); - var lengths = this._getDatasetsInOrder().map(function (d) { return d.data().length; }); + var lengths = this._getDatasetsInOrder().map(function (d) { + return d.data().length; + }); if (Plottable.Util.Methods.uniqNumbers(lengths).length > 1) { Plottable.Util.Methods.warn("Warning: Attempting to cluster data when datasets are of unequal length"); } var clusters = {}; this._datasetKeysInOrder.forEach(function (key) { var data = _this._key2DatasetDrawerKey[key].dataset.data(); - var vals = data.map(function (d) { return accessor(d); }); + var vals = data.map(function (d) { + return accessor(d); + }); + clusters[key] = data.map(function (d, i) { d["_PLOTTABLE_PROTECTED_FIELD_X"] = _this.xScale.scale(vals[i]) + _this.innerScale.scale(key); return d; @@ -5077,12 +7079,15 @@ var Plottable; }); return clusters; }; + ClusteredBar.prototype._paint = function () { _super.prototype._paint.call(this); var accessor = this._projectors["x"].accessor; var attrHash = this._generateAttrToProjector(); var clusteredData = this.cluster(accessor); - this._getDrawersInOrder().forEach(function (d) { return d.draw(clusteredData[d.key], attrHash); }); + this._getDrawersInOrder().forEach(function (d) { + return d.draw(clusteredData[d.key], attrHash); + }); }; ClusteredBar.DEFAULT_WIDTH = 10; return ClusteredBar; @@ -5092,6 +7097,7 @@ var Plottable; var Plot = Plottable.Plot; })(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; } @@ -5114,6 +7120,7 @@ var Plottable; _super.prototype._addDataset.call(this, key, dataset); this.stackedData = this.stack(this._projectors["y"].accessor); }; + StackedBar.prototype._updateAllProjectors = function () { _super.prototype._updateAllProjectors.call(this); if (this.yScale == null) { @@ -5121,23 +7128,34 @@ var Plottable; } if (this._isAnchored && this.stackedExtent.length > 0) { this.yScale.updateExtent(this._plottableID.toString(), "_PLOTTABLE_PROTECTED_FIELD_STACK_EXTENT", this.stackedExtent); - } - else { + } else { this.yScale.removeExtent(this._plottableID.toString(), "_PLOTTABLE_PROTECTED_FIELD_STACK_EXTENT"); } }; + StackedBar.prototype._generateAttrToProjector = function () { var attrToProjector = _super.prototype._generateAttrToProjector.call(this); var primaryScale = this._isVertical ? this.yScale : this.xScale; - var getY0 = function (d) { return primaryScale.scale(d._PLOTTABLE_PROTECTED_FIELD_Y0); }; - var getY = function (d) { return primaryScale.scale(d._PLOTTABLE_PROTECTED_FIELD_Y); }; - attrToProjector["height"] = function (d) { return Math.abs(getY(d) - getY0(d)); }; - attrToProjector["y"] = function (d) { return getY(d); }; + var getY0 = function (d) { + return primaryScale.scale(d._PLOTTABLE_PROTECTED_FIELD_Y0); + }; + var getY = function (d) { + return primaryScale.scale(d._PLOTTABLE_PROTECTED_FIELD_Y); + }; + attrToProjector["height"] = function (d) { + return Math.abs(getY(d) - getY0(d)); + }; + attrToProjector["y"] = function (d) { + return getY(d); + }; return attrToProjector; }; + StackedBar.prototype.stack = function (accessor) { var datasets = d3.values(this._key2DatasetDrawerKey); - var lengths = datasets.map(function (d) { return d.dataset.data().length; }); + var lengths = datasets.map(function (d) { + return d.dataset.data().length; + }); if (Plottable.Util.Methods.uniqNumbers(lengths).length > 1) { Plottable.Util.Methods.warn("Warning: Attempting to stack data when datasets are of unequal length"); } @@ -5146,10 +7164,13 @@ var Plottable; var data = dataset.data(); var base = currentBase.slice(); var vals = data.map(accessor); - if (vals.some(function (x) { return x < 0; })) { + if (vals.some(function (x) { + return x < 0; + })) { Plottable.Util.Methods.warn("Warning: Behavior for stacked bars undefined when data includes negative values"); } currentBase = Plottable.Util.Methods.addArrays(base, vals); + return data.map(function (d, i) { d["_PLOTTABLE_PROTECTED_FIELD_Y0"] = base[i]; d["_PLOTTABLE_PROTECTED_FIELD_Y"] = currentBase[i]; @@ -5160,10 +7181,13 @@ var Plottable; this._onDataSourceUpdate(); return stacks; }; + StackedBar.prototype._paint = function () { var _this = this; var attrHash = this._generateAttrToProjector(); - this._getDrawersInOrder().forEach(function (d, i) { return d.draw(_this.stackedData[i], attrHash); }); + this._getDrawersInOrder().forEach(function (d, i) { + return d.draw(_this.stackedData[i], attrHash); + }); }; return StackedBar; })(Plottable.Abstract.NewStyleBarPlot); @@ -5172,9 +7196,14 @@ var Plottable; var Plot = Plottable.Plot; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Animator) { + /** + * An animator implementation with no animation. The attributes are + * immediately set on the selection. + */ var Null = (function () { function Null() { } @@ -5188,9 +7217,13 @@ var Plottable; var Animator = Plottable.Animator; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Animator) { + /** + * The default animator implementation with easing, duration, and delay. + */ var Default = (function () { function Default() { this._durationMsec = 300; @@ -5200,29 +7233,29 @@ var Plottable; Default.prototype.animate = function (selection, attrToProjector, plot) { return selection.transition().ease(this._easing).duration(this._durationMsec).delay(this._delayMsec).attr(attrToProjector); }; + Default.prototype.duration = function (duration) { if (duration === undefined) { return this._durationMsec; - } - else { + } else { this._durationMsec = duration; return this; } }; + Default.prototype.delay = function (delay) { if (delay === undefined) { return this._delayMsec; - } - else { + } else { this._delayMsec = delay; return this; } }; + Default.prototype.easing = function (easing) { if (easing === undefined) { return this._easing; - } - else { + } else { this._easing = easing; return this; } @@ -5234,6 +7267,7 @@ var Plottable; var Animator = Plottable.Animator; })(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; } @@ -5243,6 +7277,12 @@ var __extends = this.__extends || function (d, b) { var Plottable; (function (Plottable) { (function (Animator) { + /** + * An animator that delays the animation of the attributes using the index + * of the selection data. + * + * The delay between animations can be configured with the .delay getter/setter. + */ var IterativeDelay = (function (_super) { __extends(IterativeDelay, _super); function IterativeDelay() { @@ -5251,7 +7291,9 @@ var Plottable; } IterativeDelay.prototype.animate = function (selection, attrToProjector, plot) { var _this = this; - return selection.transition().ease(this._easing).duration(this._durationMsec).delay(function (d, i) { return i * _this._delayMsec; }).attr(attrToProjector); + return selection.transition().ease(this._easing).duration(this._durationMsec).delay(function (d, i) { + return i * _this._delayMsec; + }).attr(attrToProjector); }; return IterativeDelay; })(Animator.Default); @@ -5260,12 +7302,14 @@ var Plottable; var Animator = Plottable.Animator; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Core) { (function (KeyEventListener) { var _initialized = false; var _callbacks = []; + function initialize() { if (_initialized) { return; @@ -5274,20 +7318,25 @@ var Plottable; _initialized = true; } KeyEventListener.initialize = initialize; + function addCallback(keyCode, cb) { if (!_initialized) { initialize(); } + if (_callbacks[keyCode] == null) { _callbacks[keyCode] = []; } + _callbacks[keyCode].push(cb); } KeyEventListener.addCallback = addCallback; + function processEvent() { if (_callbacks[d3.event.keyCode] == null) { return; } + _callbacks[d3.event.keyCode].forEach(function (cb) { cb(d3.event); }); @@ -5298,10 +7347,17 @@ var Plottable; var Core = Plottable.Core; })(Plottable || (Plottable = {})); +/// var Plottable; (function (Plottable) { (function (Abstract) { var Interaction = (function () { + /** + * Creates an Interaction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for interactions on. + */ function Interaction(componentToListenTo) { if (componentToListenTo == null) { throw new Error("Interactions require a component to listen to"); @@ -5311,6 +7367,11 @@ var Plottable; Interaction.prototype._anchor = function (hitBox) { this.hitBox = hitBox; }; + + /** + * Registers the Interaction on the Component it's listening to. + * This needs to be called to activate the interaction. + */ Interaction.prototype.registerWithComponent = function () { this.componentToListenTo.registerInteraction(this); return this; @@ -5322,6 +7383,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -5333,6 +7395,12 @@ var Plottable; (function (Interaction) { var Click = (function (_super) { __extends(Click, _super); + /** + * Creates a ClickInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for clicks on. + */ function Click(componentToListenTo) { _super.call(this, componentToListenTo); } @@ -5346,9 +7414,16 @@ var Plottable; _this._callback(x, y); }); }; + Click.prototype._listenTo = function () { return "click"; }; + + /** + * Sets an callback to be called when a click is received. + * + * @param {(x: number, y: number) => any} cb: Callback to be called. Takes click x and y in pixels. + */ Click.prototype.callback = function (cb) { this._callback = cb; return this; @@ -5356,8 +7431,15 @@ var Plottable; return Click; })(Plottable.Abstract.Interaction); Interaction.Click = Click; + var DoubleClick = (function (_super) { __extends(DoubleClick, _super); + /** + * Creates a DoubleClickInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for clicks on. + */ function DoubleClick(componentToListenTo) { _super.call(this, componentToListenTo); } @@ -5371,6 +7453,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5395,6 +7478,7 @@ var Plottable; _this.mousemove(x, y); }); }; + Mousemove.prototype.mousemove = function (x, y) { return; }; @@ -5405,6 +7489,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5416,6 +7501,13 @@ var Plottable; (function (Interaction) { var Key = (function (_super) { __extends(Key, _super); + /** + * Creates a KeyInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for keypresses on. + * @param {number} keyCode The key code to listen for. + */ function Key(componentToListenTo, keyCode) { _super.call(this, componentToListenTo); this.activated = false; @@ -5430,12 +7522,19 @@ var Plottable; hitBox.on("mouseout", function () { _this.activated = false; }); + Plottable.Core.KeyEventListener.addCallback(this.keyCode, function (e) { if (_this.activated && _this._callback != null) { _this._callback(); } }); }; + + /** + * Sets an callback to be called when the designated key is pressed. + * + * @param {() => any} cb: Callback to be called. + */ Key.prototype.callback = function (cb) { this._callback = cb; return this; @@ -5447,6 +7546,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5458,9 +7558,17 @@ var Plottable; (function (Interaction) { var PanZoom = (function (_super) { __extends(PanZoom, _super); + /** + * Creates a PanZoomInteraction. + * + * @constructor + * @param {Component} componentToListenTo The component to listen for interactions on. + * @param {QuantitativeScale} [xScale] The X scale to update on panning/zooming. + * @param {QuantitativeScale} [yScale] The Y scale to update on panning/zooming. + */ function PanZoom(componentToListenTo, xScale, yScale) { - _super.call(this, componentToListenTo); var _this = this; + _super.call(this, componentToListenTo); if (xScale == null) { xScale = new Plottable.Scale.Linear(); } @@ -5472,21 +7580,30 @@ var Plottable; this.zoom = d3.behavior.zoom(); this.zoom.x(this.xScale._d3Scale); this.zoom.y(this.yScale._d3Scale); - this.zoom.on("zoom", function () { return _this.rerenderZoomed(); }); + this.zoom.on("zoom", function () { + return _this.rerenderZoomed(); + }); } PanZoom.prototype.resetZoom = function () { var _this = this; + // HACKHACK #254 this.zoom = d3.behavior.zoom(); this.zoom.x(this.xScale._d3Scale); this.zoom.y(this.yScale._d3Scale); - this.zoom.on("zoom", function () { return _this.rerenderZoomed(); }); + this.zoom.on("zoom", function () { + return _this.rerenderZoomed(); + }); this.zoom(this.hitBox); }; + PanZoom.prototype._anchor = function (hitBox) { _super.prototype._anchor.call(this, hitBox); this.zoom(hitBox); }; + PanZoom.prototype.rerenderZoomed = function () { + // HACKHACK since the d3.zoom.x modifies d3 scales and not our TS scales, and the TS scales have the + // event listener machinery, let's grab the domain out of the d3 scale and pipe it back into the TS scale var xDomain = this.xScale._d3Scale.domain(); var yDomain = this.yScale._d3Scale.domain(); this.xScale.domain(xDomain); @@ -5499,6 +7616,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5510,6 +7628,11 @@ var Plottable; (function (Interaction) { var BarHover = (function (_super) { __extends(BarHover, _super); + /** + * Creates a new BarHover Interaction. + * + * @param {Abstract.BarPlot} barPlot The Bar Plot to listen for hover events on. + */ function BarHover(barPlot) { _super.call(this, barPlot); this.plotIsVertical = true; @@ -5520,65 +7643,92 @@ var Plottable; BarHover.prototype._anchor = function (hitBox) { var _this = this; this.dispatcher = new Plottable.Dispatcher.Mouse(hitBox); + this.dispatcher.mousemove(function (p) { var selectedBar = _this.getHoveredBar(p); + if (selectedBar == null) { _this._hoverOut(); - } - else { + } else { if (_this.currentBar != null) { if (_this.currentBar.node() === selectedBar.node()) { return; - } - else { + } else { _this._hoverOut(); } } + _this.componentToListenTo._bars.classed("not-hovered", true).classed("hovered", false); selectedBar.classed("not-hovered", false).classed("hovered", true); if (_this.hoverCallback != null) { _this.hoverCallback(selectedBar.data()[0], selectedBar); } } + _this.currentBar = selectedBar; }); - this.dispatcher.mouseout(function (p) { return _this._hoverOut(); }); + + this.dispatcher.mouseout(function (p) { + return _this._hoverOut(); + }); + this.dispatcher.connect(); }; + BarHover.prototype._hoverOut = function () { this.componentToListenTo._bars.classed("not-hovered hovered", false); if (this.unhoverCallback != null && this.currentBar != null) { - this.unhoverCallback(this.currentBar.data()[0], this.currentBar); + this.unhoverCallback(this.currentBar.data()[0], this.currentBar); // last known information } this.currentBar = null; }; + BarHover.prototype.getHoveredBar = function (p) { if (this._hoverMode === "point") { return this.componentToListenTo.selectBar(p.x, p.y, false); } + var maxExtent = { min: -Infinity, max: Infinity }; if (this.plotIsVertical) { return this.componentToListenTo.selectBar(p.x, maxExtent, false); - } - else { + } else { return this.componentToListenTo.selectBar(maxExtent, p.y, false); } }; + BarHover.prototype.hoverMode = function (mode) { if (mode == null) { return this._hoverMode; } + var modeLC = mode.toLowerCase(); if (modeLC !== "point" && modeLC !== "line") { throw new Error(mode + " is not a valid hover mode for Interaction.BarHover"); } + this._hoverMode = modeLC; return this; }; + + /** + * Attaches an callback to be called when the user mouses over a bar. + * + * @param {(datum: any, bar: D3.Selection) => any} The callback to be called. + * The callback will be passed the data from the hovered-over bar. + * @return {BarHover} The calling Interaction.BarHover. + */ BarHover.prototype.onHover = function (callback) { this.hoverCallback = callback; return this; }; + + /** + * Attaches a callback to be called when the user mouses off of a bar. + * + * @param {(datum: any, bar: D3.Selection) => any} The callback to be called. + * The callback will be passed the data from the last-hovered bar. + * @return {BarHover} The calling Interaction.BarHover. + */ BarHover.prototype.onUnhover = function (callback) { this.unhoverCallback = callback; return this; @@ -5590,6 +7740,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5601,35 +7752,94 @@ var Plottable; (function (Interaction) { var Drag = (function (_super) { __extends(Drag, _super); + /** + * Creates a Drag. + * + * @param {Component} componentToListenTo The component to listen for interactions on. + */ function Drag(componentToListenTo) { - _super.call(this, componentToListenTo); var _this = this; + _super.call(this, componentToListenTo); this.dragInitialized = false; this.origin = [0, 0]; this.location = [0, 0]; this.dragBehavior = d3.behavior.drag(); - this.dragBehavior.on("dragstart", function () { return _this._dragstart(); }); - this.dragBehavior.on("drag", function () { return _this._drag(); }); - this.dragBehavior.on("dragend", function () { return _this._dragend(); }); + this.dragBehavior.on("dragstart", function () { + return _this._dragstart(); + }); + this.dragBehavior.on("drag", function () { + return _this._drag(); + }); + this.dragBehavior.on("dragend", function () { + return _this._dragend(); + }); } - Drag.prototype.callback = function (cb) { - this.callbackToCall = cb; - return this; + Drag.prototype.dragstart = function (cb) { + if (cb === undefined) { + return this.ondragstart; + } else { + this.ondragstart = cb; + return this; + } }; + + Drag.prototype.drag = function (cb) { + if (cb === undefined) { + return this.ondrag; + } else { + this.ondrag = cb; + return this; + } + }; + + Drag.prototype.dragend = function (cb) { + if (cb === undefined) { + return this.ondragend; + } else { + this.ondragend = cb; + return this; + } + }; + Drag.prototype._dragstart = function () { var availableWidth = this.componentToListenTo.availableWidth; var availableHeight = this.componentToListenTo.availableHeight; - var constraintFunction = function (min, max) { return function (x) { return Math.min(Math.max(x, min), max); }; }; + + // the constraint functions ensure that the selection rectangle will not exceed the hit box + var constraintFunction = function (min, max) { + return function (x) { + return Math.min(Math.max(x, min), max); + }; + }; this.constrainX = constraintFunction(0, availableWidth); this.constrainY = constraintFunction(0, availableHeight); }; + + Drag.prototype._doDragstart = function () { + if (this.ondragstart != null) { + this.ondragstart({ x: this.origin[0], y: this.origin[1] }); + } + }; + Drag.prototype._drag = function () { if (!this.dragInitialized) { this.origin = [d3.event.x, d3.event.y]; this.dragInitialized = true; + this._doDragstart(); } + this.location = [this.constrainX(d3.event.x), this.constrainY(d3.event.y)]; + this._doDrag(); + }; + + Drag.prototype._doDrag = function () { + if (this.ondrag != null) { + var startLocation = { x: this.origin[0], y: this.origin[1] }; + var endLocation = { x: this.location[0], y: this.location[1] }; + this.ondrag(startLocation, endLocation); + } }; + Drag.prototype._dragend = function () { if (!this.dragInitialized) { return; @@ -5637,22 +7847,27 @@ var Plottable; this.dragInitialized = false; this._doDragend(); }; + Drag.prototype._doDragend = function () { - if (this.callbackToCall != null) { - this.callbackToCall([this.origin, this.location]); + if (this.ondragend != null) { + var startLocation = { x: this.origin[0], y: this.origin[1] }; + var endLocation = { x: this.location[0], y: this.location[1] }; + this.ondragend(startLocation, endLocation); } }; + Drag.prototype._anchor = function (hitBox) { _super.prototype._anchor.call(this, hitBox); hitBox.call(this.dragBehavior); return this; }; + Drag.prototype.setupZoomCallback = function (xScale, yScale) { var xDomainOriginal = xScale != null ? xScale.domain() : null; var yDomainOriginal = yScale != null ? yScale.domain() : null; var resetOnNextClick = false; - function callback(pixelArea) { - if (pixelArea == null) { + function callback(upperLeft, lowerRight) { + if (upperLeft == null || lowerRight == null) { if (resetOnNextClick) { if (xScale != null) { xScale.domain(xDomainOriginal); @@ -5666,15 +7881,16 @@ var Plottable; } resetOnNextClick = false; if (xScale != null) { - xScale.domain([xScale.invert(pixelArea.xMin), xScale.invert(pixelArea.xMax)]); + xScale.domain([xScale.invert(upperLeft.x), xScale.invert(lowerRight.x)]); } if (yScale != null) { - yScale.domain([yScale.invert(pixelArea.yMax), yScale.invert(pixelArea.yMin)]); + yScale.domain([yScale.invert(lowerRight.y), yScale.invert(upperLeft.y)]); } this.clearBox(); return; } - this.callback(callback); + this.drag(callback); + this.dragend(callback); return this; }; return Drag; @@ -5684,6 +7900,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5701,11 +7918,14 @@ var Plottable; } DragBox.prototype._dragstart = function () { _super.prototype._dragstart.call(this); - if (this.callbackToCall != null) { - this.callbackToCall(null); - } this.clearBox(); }; + + /** + * Clears the highlighted drag-selection box drawn by the AreaInteraction. + * + * @returns {AreaInteraction} The calling AreaInteraction. + */ DragBox.prototype.clearBox = function () { if (this.dragBox == null) { return; @@ -5714,6 +7934,7 @@ var Plottable; this.boxIsDrawn = false; return this; }; + DragBox.prototype.setBox = function (x0, x1, y0, y1) { if (this.dragBox == null) { return; @@ -5726,6 +7947,7 @@ var Plottable; this.boxIsDrawn = (w > 0 && h > 0); return this; }; + DragBox.prototype._anchor = function (hitBox) { _super.prototype._anchor.call(this, hitBox); var cname = DragBox.CLASS_DRAG_BOX; @@ -5741,6 +7963,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5759,15 +7982,7 @@ var Plottable; _super.prototype._drag.call(this); this.setBox(this.origin[0], this.location[0]); }; - XDragBox.prototype._doDragend = function () { - if (this.callbackToCall == null) { - return; - } - var xMin = Math.min(this.origin[0], this.location[0]); - var xMax = Math.max(this.origin[0], this.location[0]); - var pixelArea = { xMin: xMin, xMax: xMax }; - this.callbackToCall(pixelArea); - }; + XDragBox.prototype.setBox = function (x0, x1) { _super.prototype.setBox.call(this, x0, x1, 0, this.componentToListenTo.availableHeight); return this; @@ -5779,6 +7994,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5797,17 +8013,6 @@ var Plottable; _super.prototype._drag.call(this); this.setBox(this.origin[0], this.location[0], this.origin[1], this.location[1]); }; - XYDragBox.prototype._doDragend = function () { - if (this.callbackToCall == null) { - return; - } - var xMin = Math.min(this.origin[0], this.location[0]); - var xMax = Math.max(this.origin[0], this.location[0]); - var yMin = Math.min(this.origin[1], this.location[1]); - var yMax = Math.max(this.origin[1], this.location[1]); - var pixelArea = { xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax }; - this.callbackToCall(pixelArea); - }; return XYDragBox; })(Interaction.DragBox); Interaction.XYDragBox = XYDragBox; @@ -5815,6 +8020,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5833,15 +8039,7 @@ var Plottable; _super.prototype._drag.call(this); this.setBox(this.origin[1], this.location[1]); }; - YDragBox.prototype._doDragend = function () { - if (this.callbackToCall == null) { - return; - } - var yMin = Math.min(this.origin[1], this.location[1]); - var yMax = Math.max(this.origin[1], this.location[1]); - var pixelArea = { yMin: yMin, yMax: yMax }; - this.callbackToCall(pixelArea); - }; + YDragBox.prototype.setBox = function (y0, y1) { _super.prototype.setBox.call(this, 0, this.componentToListenTo.availableWidth, y0, y1); return this; @@ -5853,6 +8051,7 @@ var Plottable; var Interaction = Plottable.Interaction; })(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; } @@ -5864,6 +8063,11 @@ var Plottable; (function (Abstract) { var Dispatcher = (function (_super) { __extends(Dispatcher, _super); + /** + * Creates a Dispatcher with the specified target. + * + * @param {D3.Selection} target The selection to listen for events on. + */ function Dispatcher(target) { _super.call(this); this._event2Callback = {}; @@ -5878,13 +8082,24 @@ var Plottable; this.disconnect(); this._target = targetElement; if (wasConnected) { + // re-connect to the new target this.connect(); } return this; }; + + /** + * Gets a namespaced version of the event name. + */ Dispatcher.prototype.getEventString = function (eventName) { return eventName + ".dispatcher" + this._plottableID; }; + + /** + * Attaches the Dispatcher's listeners to the Dispatcher's target element. + * + * @returns {Dispatcher} The calling Dispatcher. + */ Dispatcher.prototype.connect = function () { var _this = this; if (this.connected) { @@ -5895,8 +8110,15 @@ var Plottable; var callback = _this._event2Callback[event]; _this._target.on(_this.getEventString(event), callback); }); + return this; }; + + /** + * Detaches the Dispatcher's listeners from the Dispatchers' target element. + * + * @returns {Dispatcher} The calling Dispatcher. + */ Dispatcher.prototype.disconnect = function () { var _this = this; this.connected = false; @@ -5912,6 +8134,7 @@ var Plottable; var Abstract = Plottable.Abstract; })(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; } @@ -5923,19 +8146,27 @@ var Plottable; (function (Dispatcher) { var Mouse = (function (_super) { __extends(Mouse, _super); + /** + * Creates a Mouse Dispatcher with the specified target. + * + * @param {D3.Selection} target The selection to listen for events on. + */ function Mouse(target) { - _super.call(this, target); var _this = this; + _super.call(this, target); + this._event2Callback["mouseover"] = function () { if (_this._mouseover != null) { _this._mouseover(_this.getMousePosition()); } }; + this._event2Callback["mousemove"] = function () { if (_this._mousemove != null) { _this._mousemove(_this.getMousePosition()); } }; + this._event2Callback["mouseout"] = function () { if (_this._mouseout != null) { _this._mouseout(_this.getMousePosition()); @@ -5949,6 +8180,7 @@ var Plottable; y: xy[1] }; }; + Mouse.prototype.mouseover = function (callback) { if (callback === undefined) { return this._mouseover; @@ -5956,6 +8188,7 @@ var Plottable; this._mouseover = callback; return this; }; + Mouse.prototype.mousemove = function (callback) { if (callback === undefined) { return this._mousemove; @@ -5963,6 +8196,7 @@ var Plottable; this._mousemove = callback; return this; }; + Mouse.prototype.mouseout = function (callback) { if (callback === undefined) { return this._mouseout; @@ -5977,6 +8211,7 @@ var Plottable; var Dispatcher = Plottable.Dispatcher; })(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; } @@ -6004,11 +8239,11 @@ var Plottable; this._yAxis = y; this.yTable.addComponent(0, 1, this._yAxis); return this; - } - else { + } else { return this._yAxis; } }; + StandardChart.prototype.xAxis = function (x) { if (x != null) { if (this._xAxis != null) { @@ -6017,19 +8252,18 @@ var Plottable; this._xAxis = x; this.xTable.addComponent(0, 0, this._xAxis); return this; - } - else { + } else { return this._xAxis; } }; + StandardChart.prototype.yLabel = function (y) { if (y != null) { if (this._yLabel != null) { if (typeof (y) === "string") { this._yLabel.text(y); return this; - } - else { + } else { throw new Error("yLabel already assigned!"); } } @@ -6039,19 +8273,18 @@ var Plottable; this._yLabel = y; this.yTable.addComponent(0, 0, this._yLabel); return this; - } - else { + } else { return this._yLabel; } }; + StandardChart.prototype.xLabel = function (x) { if (x != null) { if (this._xLabel != null) { if (typeof (x) === "string") { this._xLabel.text(x); return this; - } - else { + } else { throw new Error("xLabel already assigned!"); } } @@ -6061,19 +8294,18 @@ var Plottable; this._xLabel = x; this.xTable.addComponent(1, 0, this._xLabel); return this; - } - else { + } else { return this._xLabel; } }; + StandardChart.prototype.titleLabel = function (x) { if (x != null) { if (this._titleLabel != null) { if (typeof (x) === "string") { this._titleLabel.text(x); return this; - } - else { + } else { throw new Error("titleLabel already assigned!"); } } @@ -6083,11 +8315,11 @@ var Plottable; this._titleLabel = x; this.addComponent(0, 0, this._titleLabel); return this; - } - else { + } else { return this._titleLabel; } }; + StandardChart.prototype.center = function (c) { this.centerComponent.merge(c); return this; diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 26a085c02f..77f6d025ca 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -46,13 +46,6 @@ module Plottable { wantsHeight: boolean; } - export interface IPixelArea { - xMin: number; - xMax: number; - yMin: number; - yMax: number; - } - export interface IExtent { min: number; max: number; diff --git a/src/interactions/drag/dragBoxInteraction.ts b/src/interactions/drag/dragBoxInteraction.ts index 0ada3a04b5..bf69ae5a20 100644 --- a/src/interactions/drag/dragBoxInteraction.ts +++ b/src/interactions/drag/dragBoxInteraction.ts @@ -9,9 +9,6 @@ export module Interaction { public _dragstart() { super._dragstart(); - if (this.callbackToCall != null) { - this.callbackToCall(null); - } this.clearBox(); } diff --git a/src/interactions/drag/dragInteraction.ts b/src/interactions/drag/dragInteraction.ts index 9eb30693c5..22810b7991 100644 --- a/src/interactions/drag/dragInteraction.ts +++ b/src/interactions/drag/dragInteraction.ts @@ -7,9 +7,11 @@ export module Interaction { private dragBehavior: D3.Behavior.Drag; public origin = [0,0]; public location = [0,0]; - private constrainX: (n: number) => number; - private constrainY: (n: number) => number; - public callbackToCall: (dragInfo: any) => any; + private constrainX: (n: number) => number; + private constrainY: (n: number) => number; + private ondragstart: (startLocation: Point) => void; + private ondrag: (startLocation: Point, endLocation: Point) => void; + private ondragend: (startLocation: Point, endLocation: Point) => void; /** * Creates a Drag. @@ -25,14 +27,69 @@ export module Interaction { } /** - * Adds a callback to be called when the AreaInteraction triggers. + * Gets the callback that is called when dragging starts. * - * @param {(a: SelectionArea) => any} cb The function to be called. Takes in a SelectionArea in pixels. - * @returns {AreaInteraction} The calling AreaInteraction. + * @returns {(startLocation: Point) => void} */ - public callback(cb?: (a: any) => any) { - this.callbackToCall = cb; - return this; + public dragstart(): (startLocation: Point) => void; + /** + * Sets the callback to be called when dragging starts. + * + * @param {(startLocation: Point) => any} cb The function to be called. + * @returns {Drag} + */ + public dragstart(cb: (startLocation: Point) => any): Drag; + public dragstart(cb?: (startLocation: Point) => any): any { + if (cb === undefined) { + return this.ondragstart; + } else { + this.ondragstart = cb; + return this; + } + } + + /** + * Gets the callback that is called during dragging. + * + * @returns {(startLocation: Point, endLocation: Point) => void} + */ + public drag(): (startLocation: Point, endLocation: Point) => void; + /** + * Adds a callback to be called during dragging. + * + * @param {(startLocation: Point, endLocation: Point) => any} cb The function to be called. + * @returns {Drag} + */ + public drag(cb: (startLocation: Point, endLocation: Point) => any): Drag; + public drag(cb?: (startLocation: Point, endLocation: Point) => any): any { + if (cb === undefined) { + return this.ondrag; + } else { + this.ondrag = cb; + return this; + } + } + + /** + * Gets the callback that is called when dragging ends. + * + * @returns {(startLocation: Point, endLocation: Point) => void} + */ + public dragend(): (startLocation: Point, endLocation: Point) => void; + /** + * Adds a callback to be called when the dragging ends. + * + * @param {(startLocation: Point, endLocation: Point) => any} cb The function to be called. Takes in a SelectionArea in pixels. + * @returns {Drag} The calling Drag. + */ + public dragend(cb: (startLocation: Point, endLocation: Point) => any): Drag; + public dragend(cb?: (startLocation: Point, endLocation: Point) => any): any { + if (cb === undefined) { + return this.ondragend; + } else { + this.ondragend = cb; + return this; + } } public _dragstart(){ @@ -44,13 +101,29 @@ export module Interaction { this.constrainY = constraintFunction(0, availableHeight); } + public _doDragstart() { + if (this.ondragstart != null) { + this.ondragstart({x: this.origin[0], y: this.origin[1]}); + } + } + public _drag(){ if (!this.dragInitialized) { this.origin = [d3.event.x, d3.event.y]; this.dragInitialized = true; + this._doDragstart(); } this.location = [this.constrainX(d3.event.x), this.constrainY(d3.event.y)]; + this._doDrag(); + } + + public _doDrag() { + if (this.ondrag != null) { + var startLocation = {x: this.origin[0], y: this.origin[1]}; + var endLocation = {x: this.location[0], y: this.location[1]}; + this.ondrag(startLocation, endLocation); + } } public _dragend(){ @@ -62,10 +135,10 @@ export module Interaction { } public _doDragend() { - // seperated out so it can be over-ridden by dragInteractions that want to pass out diff information - // eg just x values for an xSelectionInteraction - if (this.callbackToCall != null) { - this.callbackToCall([this.origin, this.location]); + if (this.ondragend != null) { + var startLocation = {x: this.origin[0], y: this.origin[1]}; + var endLocation = {x: this.location[0], y: this.location[1]}; + this.ondragend(startLocation, endLocation); } } @@ -79,8 +152,8 @@ export module Interaction { var xDomainOriginal = xScale != null ? xScale.domain() : null; var yDomainOriginal = yScale != null ? yScale.domain() : null; var resetOnNextClick = false; - function callback(pixelArea: IPixelArea) { - if (pixelArea == null) { + function callback(upperLeft: Point, lowerRight: Point) { + if (upperLeft == null || lowerRight == null) { if (resetOnNextClick) { if (xScale != null) { xScale.domain(xDomainOriginal); @@ -94,15 +167,16 @@ export module Interaction { } resetOnNextClick = false; if (xScale != null) { - xScale.domain([xScale.invert(pixelArea.xMin), xScale.invert(pixelArea.xMax)]); + xScale.domain([xScale.invert(upperLeft.x), xScale.invert(lowerRight.x)]); } if (yScale != null) { - yScale.domain([yScale.invert(pixelArea.yMax), yScale.invert(pixelArea.yMin)]); + yScale.domain([yScale.invert(lowerRight.y), yScale.invert(upperLeft.y)]); } this.clearBox(); return; } - this.callback(callback); + this.drag(callback); + this.dragend(callback); return this; } } diff --git a/src/interactions/drag/xDragBoxInteraction.ts b/src/interactions/drag/xDragBoxInteraction.ts index e6fd7c50e7..6608063429 100644 --- a/src/interactions/drag/xDragBoxInteraction.ts +++ b/src/interactions/drag/xDragBoxInteraction.ts @@ -8,16 +8,6 @@ export module Interaction { this.setBox(this.origin[0], this.location[0]); } - public _doDragend(){ - if (this.callbackToCall == null) { - return; - } - var xMin = Math.min(this.origin[0], this.location[0]); - var xMax = Math.max(this.origin[0], this.location[0]); - var pixelArea = {xMin: xMin, xMax: xMax}; - this.callbackToCall(pixelArea); - } - public setBox(x0: number, x1: number) { super.setBox(x0, x1, 0, this.componentToListenTo.availableHeight); return this; diff --git a/src/interactions/drag/xyDragBoxInteraction.ts b/src/interactions/drag/xyDragBoxInteraction.ts index 3952d87223..d270fd693b 100644 --- a/src/interactions/drag/xyDragBoxInteraction.ts +++ b/src/interactions/drag/xyDragBoxInteraction.ts @@ -7,18 +7,6 @@ export module Interaction { super._drag(); this.setBox(this.origin[0], this.location[0], this.origin[1], this.location[1]); } - - public _doDragend(){ - if (this.callbackToCall == null) { - return; - } - var xMin = Math.min(this.origin[0], this.location[0]); - var xMax = Math.max(this.origin[0], this.location[0]); - var yMin = Math.min(this.origin[1], this.location[1]); - var yMax = Math.max(this.origin[1], this.location[1]); - var pixelArea = {xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax}; - this.callbackToCall(pixelArea); - } } } } diff --git a/src/interactions/drag/yDragBoxInteraction.ts b/src/interactions/drag/yDragBoxInteraction.ts index d7a3fe344a..9207bf6d09 100644 --- a/src/interactions/drag/yDragBoxInteraction.ts +++ b/src/interactions/drag/yDragBoxInteraction.ts @@ -8,16 +8,6 @@ export module Interaction { this.setBox(this.origin[1], this.location[1]); } - public _doDragend(){ - if (this.callbackToCall == null) { - return; - } - var yMin = Math.min(this.origin[1], this.location[1]); - var yMax = Math.max(this.origin[1], this.location[1]); - var pixelArea = {yMin: yMin, yMax: yMax}; - this.callbackToCall(pixelArea); - } - public setBox(y0: number, y1: number) { super.setBox(0, this.componentToListenTo.availableWidth, y0, y1); return this; diff --git a/test/interactions/interactionTests.ts b/test/interactions/interactionTests.ts index 53787eebf9..055dd870ee 100644 --- a/test/interactions/interactionTests.ts +++ b/test/interactions/interactionTests.ts @@ -105,35 +105,37 @@ describe("Interactions", () => { }); afterEach(() => { - interaction.callback(); + interaction.dragstart(null); + interaction.drag(null); + interaction.dragend(null); interaction.clearBox(); }); - it("All callbacks are notified with appropriate data when a drag finishes", () => { + it("All callbacks are notified with appropriate data on drag", () => { var timesCalled = 0; - var areaCallback = (a: Plottable.SelectionArea) => { + interaction.dragstart(function(a: Plottable.Point) { timesCalled++; - if (timesCalled === 1) { - assert.deepEqual(a, null, "areaCallback called with null arg on dragstart"); - } - if (timesCalled === 2) { - var expectedPixelArea = { - xMin: dragstartX, - xMax: dragendX, - yMin: dragstartY, - yMax: dragendY - }; - assert.deepEqual(a, expectedPixelArea, "areaCallback was passed the correct pixel area"); - } - }; - - - interaction.callback(areaCallback); + var expectedStartLocation = { x: dragstartX, y: dragstartY }; + assert.deepEqual(a, expectedStartLocation, "areaCallback called with null arg on dragstart"); + }); + interaction.dragend(function(a: Plottable.Point, b: Plottable.Point) { + timesCalled++; + var expectedStart = { + x: dragstartX, + y: dragstartY, + }; + var expectedEnd = { + x: dragendX, + y: dragendY + }; + assert.deepEqual(a, expectedStart, "areaCallback was passed the correct starting point"); + assert.deepEqual(b, expectedEnd, "areaCallback was passed the correct ending point"); + }); // fake a drag event fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); - assert.equal(timesCalled, 2, "areaCallback was called twice"); + assert.equal(timesCalled, 2, "drag callbacks are called twice"); }); it("Highlights and un-highlights areas appropriately", () => { @@ -184,33 +186,31 @@ describe("Interactions", () => { }); afterEach(() => { - interaction.callback(); + interaction.dragstart(null); + interaction.drag(null); + interaction.dragend(null); interaction.clearBox(); }); it("All callbacks are notified with appropriate data when a drag finishes", () => { var timesCalled = 0; - var areaCallback = (a: Plottable.SelectionArea) => { + interaction.dragstart(function(a: Plottable.Point) { timesCalled++; - if (timesCalled === 1) { - assert.deepEqual(a, null, "areaCallback called with null arg on dragstart"); - } - if (timesCalled === 2) { - var expectedPixelArea = { - yMin: dragstartY, - yMax: dragendY - }; - assert.deepEqual(a, expectedPixelArea, "areaCallback was passed the correct pixel area"); - } - }; - - - interaction.callback(areaCallback); + var expectedY = dragstartY; + assert.deepEqual(a.y, expectedY, "areaCallback called with null arg on dragstart"); + }) + interaction.dragend(function(a: Plottable.Point, b: Plottable.Point) { + timesCalled++; + var expectedStartY = dragstartY; + var expectedEndY = dragendY; + assert.deepEqual(a.y, expectedStartY); + assert.deepEqual(b.y, expectedEndY); + }); // fake a drag event fakeDragSequence(( interaction), dragstartX, dragstartY, dragendX, dragendY); - assert.equal(timesCalled, 2, "areaCallback was called twice"); + assert.equal(timesCalled, 2, "drag callbacks area called twice"); }); it("Highlights and un-highlights areas appropriately", () => { diff --git a/test/tests.js b/test/tests.js index d0f5930d2a..748cdd440d 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,26 +1,29 @@ +/// function generateSVG(width, height) { - if (width === void 0) { width = 400; } - if (height === void 0) { height = 400; } + if (typeof width === "undefined") { width = 400; } + if (typeof height === "undefined") { height = 400; } var parent = getSVGParent(); return parent.append("svg").attr("width", width).attr("height", height).attr("class", "svg"); } + 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]); return lastSuite.selectAll("ul"); - } - else { + } else { return d3.select("body"); } } + function verifySpaceRequest(sr, w, h, ww, wh, id) { assert.equal(sr.width, w, "width requested is as expected #" + id); assert.equal(sr.height, h, "height requested is as expected #" + id); assert.equal(sr.wantsWidth, ww, "needs more width is as expected #" + id); assert.equal(sr.wantsHeight, wh, "needs more height is as expected #" + id); } + function fixComponentSize(c, fixedWidth, fixedHeight) { c._requestedSpace = function (w, h) { return { @@ -34,18 +37,22 @@ function fixComponentSize(c, fixedWidth, fixedHeight) { c._fixedHeightFlag = fixedHeight == null ? false : true; return c; } + function makeFixedSizeComponent(fixedWidth, fixedHeight) { return fixComponentSize(new Plottable.Abstract.Component(), fixedWidth, fixedHeight); } + function getTranslate(element) { return d3.transform(element.attr("transform")).translate; } + function assertBBoxEquivalence(bbox, widthAndHeightPair, message) { var width = widthAndHeightPair[0]; var height = widthAndHeightPair[1]; assert.equal(bbox.width, width, "width: " + message); assert.equal(bbox.height, height, "height: " + message); } + function assertBBoxInclusion(outerEl, innerEl) { var outerBox = outerEl.node().getBoundingClientRect(); var innerBox = innerEl.node().getBoundingClientRect(); @@ -54,30 +61,35 @@ function assertBBoxInclusion(outerEl, innerEl) { assert.operator(Math.ceil(outerBox.right) + window.Pixel_CloseTo_Requirement, ">=", Math.floor(innerBox.right), "bounding rect right included"); assert.operator(Math.ceil(outerBox.bottom) + window.Pixel_CloseTo_Requirement, ">=", Math.floor(innerBox.bottom), "bounding rect bottom included"); } + function assertXY(el, xExpected, yExpected, message) { var x = el.attr("x"); var y = el.attr("y"); assert.equal(x, xExpected, "x: " + message); assert.equal(y, yExpected, "y: " + message); } + function assertWidthHeight(el, widthExpected, heightExpected, message) { var width = el.attr("width"); var height = el.attr("height"); assert.equal(width, widthExpected, "width: " + message); assert.equal(height, heightExpected, "height: " + message); } + function makeLinearSeries(n) { function makePoint(x) { return { x: x, y: x }; } return d3.range(n).map(makePoint); } + function makeQuadraticSeries(n) { function makeQuadraticPoint(x) { return { x: x, y: x * x }; } return d3.range(n).map(makeQuadraticPoint); } + var MultiTestVerifier = (function () { function MultiTestVerifier() { this.passed = true; @@ -86,16 +98,19 @@ var MultiTestVerifier = (function () { this.temp = this.passed; this.passed = false; }; + MultiTestVerifier.prototype.end = function () { this.passed = this.temp; }; return MultiTestVerifier; })(); + function triggerFakeUIEvent(type, target) { var e = document.createEvent("UIEvents"); e.initUIEvent(type, true, true, window, 1); target.node().dispatchEvent(e); } + function triggerFakeMouseEvent(type, target, relativeX, relativeY) { var clientRect = target.node().getBoundingClientRect(); var xPos = clientRect.left + relativeX; @@ -105,10 +120,14 @@ function triggerFakeMouseEvent(type, target, relativeX, relativeY) { target.node().dispatchEvent(e); } +/// + before(function () { + // Set the render policy to immediate to make sure ETE tests can check DOM change immediately Plottable.Core.RenderController.setRenderPolicy(new Plottable.Core.RenderController.RenderPolicy.Immediate()); window.Pixel_CloseTo_Requirement = window.PHANTOMJS ? 2 : 0.5; }); + after(function () { var parent = getSVGParent(); var mocha = d3.select("#mocha-report"); @@ -118,28 +137,40 @@ after(function () { var curSuite = d3.select(suites[0][i]); assert(curSuite.selectAll("ul").selectAll("svg").node() === null, "all svgs have been removed"); } - } - else { + } else { assert(d3.select("body").selectAll("svg").node() === null, "all svgs have been removed"); } }); +/// var assert = chai.assert; + describe("BaseAxis", function () { it("orientation", function () { var scale = new Plottable.Scale.Linear(); - assert.throws(function () { return new Plottable.Abstract.Axis(scale, "blargh"); }, "unsupported"); + assert.throws(function () { + return new Plottable.Abstract.Axis(scale, "blargh"); + }, "unsupported"); }); + it("tickLabelPadding() rejects negative values", function () { var scale = new Plottable.Scale.Linear(); var baseAxis = new Plottable.Abstract.Axis(scale, "bottom"); - assert.throws(function () { return baseAxis.tickLabelPadding(-1); }, "must be positive"); + + assert.throws(function () { + return baseAxis.tickLabelPadding(-1); + }, "must be positive"); }); + it("gutter() rejects negative values", function () { var scale = new Plottable.Scale.Linear(); var axis = new Plottable.Abstract.Axis(scale, "right"); - assert.throws(function () { return axis.gutter(-1); }, "must be positive"); + + assert.throws(function () { + return axis.gutter(-1); + }, "must be positive"); }); + it("width() + gutter()", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 500; @@ -147,21 +178,35 @@ describe("BaseAxis", function () { var scale = new Plottable.Scale.Linear(); var verticalAxis = new Plottable.Abstract.Axis(scale, "right"); verticalAxis.renderTo(svg); + var expectedWidth = verticalAxis.tickLength() + verticalAxis.gutter(); assert.strictEqual(verticalAxis.width(), expectedWidth, "calling width() with no arguments returns currently used width"); + verticalAxis.gutter(20); expectedWidth = verticalAxis.tickLength() + verticalAxis.gutter(); assert.strictEqual(verticalAxis.width(), expectedWidth, "changing the gutter size updates the width"); + verticalAxis.width(20); assert.strictEqual(verticalAxis.width(), 20, "width was set to user-specified value"); - verticalAxis.width(10 * SVG_WIDTH); + + verticalAxis.width(10 * SVG_WIDTH); // way too big assert.strictEqual(verticalAxis.width(), SVG_WIDTH, "returns actual used width if requested width is too large"); - assert.doesNotThrow(function () { return verticalAxis.width("auto"); }, Error, "can be set to auto mode"); - assert.throws(function () { return verticalAxis.width(-999); }, Error, "invalid"); + + assert.doesNotThrow(function () { + return verticalAxis.width("auto"); + }, Error, "can be set to auto mode"); + assert.throws(function () { + return verticalAxis.width(-999); + }, Error, "invalid"); + var horizontalAxis = new Plottable.Abstract.Axis(scale, "bottom"); - assert.throws(function () { return horizontalAxis.width(2014); }, Error, "horizontal"); + assert.throws(function () { + return horizontalAxis.width(2014); + }, Error, "horizontal"); + svg.remove(); }); + it("height() + gutter()", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -169,21 +214,35 @@ describe("BaseAxis", function () { var scale = new Plottable.Scale.Linear(); var horizontalAxis = new Plottable.Abstract.Axis(scale, "bottom"); horizontalAxis.renderTo(svg); + var expectedHeight = horizontalAxis.tickLength() + horizontalAxis.gutter(); assert.strictEqual(horizontalAxis.height(), expectedHeight, "calling height() with no arguments returns currently used height"); + horizontalAxis.gutter(20); expectedHeight = horizontalAxis.tickLength() + horizontalAxis.gutter(); assert.strictEqual(horizontalAxis.height(), expectedHeight, "changing the gutter size updates the height"); + horizontalAxis.height(20); assert.strictEqual(horizontalAxis.height(), 20, "height was set to user-specified value"); - horizontalAxis.height(10 * SVG_HEIGHT); + + horizontalAxis.height(10 * SVG_HEIGHT); // way too big assert.strictEqual(horizontalAxis.height(), SVG_HEIGHT, "returns actual used height if requested height is too large"); - assert.doesNotThrow(function () { return horizontalAxis.height("auto"); }, Error, "can be set to auto mode"); - assert.throws(function () { return horizontalAxis.height(-999); }, Error, "invalid"); + + assert.doesNotThrow(function () { + return horizontalAxis.height("auto"); + }, Error, "can be set to auto mode"); + assert.throws(function () { + return horizontalAxis.height(-999); + }, Error, "invalid"); + var verticalAxis = new Plottable.Abstract.Axis(scale, "right"); - assert.throws(function () { return verticalAxis.height(2014); }, Error, "vertical"); + assert.throws(function () { + return verticalAxis.height(2014); + }, Error, "vertical"); + svg.remove(); }); + it("draws ticks and baseline (horizontal)", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -198,22 +257,27 @@ describe("BaseAxis", function () { return tickValues; }; baseAxis.renderTo(svg); + var tickMarks = svg.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); assert.strictEqual(tickMarks[0].length, tickValues.length, "A tick mark was created for each value"); var baseline = svg.select(".baseline"); + assert.isNotNull(baseline.node(), "baseline was drawn"); assert.strictEqual(baseline.attr("x1"), "0"); assert.strictEqual(baseline.attr("x2"), String(SVG_WIDTH)); assert.strictEqual(baseline.attr("y1"), "0"); assert.strictEqual(baseline.attr("y2"), "0"); + baseAxis.orient("top"); assert.isNotNull(baseline.node(), "baseline was drawn"); assert.strictEqual(baseline.attr("x1"), "0"); assert.strictEqual(baseline.attr("x2"), String(SVG_WIDTH)); assert.strictEqual(baseline.attr("y1"), String(SVG_HEIGHT)); assert.strictEqual(baseline.attr("y2"), String(SVG_HEIGHT)); + svg.remove(); }); + it("draws ticks and baseline (vertical)", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 500; @@ -228,22 +292,27 @@ describe("BaseAxis", function () { return tickValues; }; baseAxis.renderTo(svg); + var tickMarks = svg.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); assert.strictEqual(tickMarks[0].length, tickValues.length, "A tick mark was created for each value"); var baseline = svg.select(".baseline"); + assert.isNotNull(baseline.node(), "baseline was drawn"); assert.strictEqual(baseline.attr("x1"), String(SVG_WIDTH)); assert.strictEqual(baseline.attr("x2"), String(SVG_WIDTH)); assert.strictEqual(baseline.attr("y1"), "0"); assert.strictEqual(baseline.attr("y2"), String(SVG_HEIGHT)); + baseAxis.orient("right"); assert.isNotNull(baseline.node(), "baseline was drawn"); assert.strictEqual(baseline.attr("x1"), "0"); assert.strictEqual(baseline.attr("x2"), "0"); assert.strictEqual(baseline.attr("y1"), "0"); assert.strictEqual(baseline.attr("y2"), String(SVG_HEIGHT)); + svg.remove(); }); + it("tickLength()", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -262,11 +331,17 @@ describe("BaseAxis", function () { assert.strictEqual(secondTickMark.attr("x2"), "50"); assert.strictEqual(secondTickMark.attr("y1"), "0"); assert.strictEqual(secondTickMark.attr("y2"), String(baseAxis.tickLength())); + baseAxis.tickLength(10); assert.strictEqual(secondTickMark.attr("y2"), String(baseAxis.tickLength()), "tick length was updated"); - assert.throws(function () { return baseAxis.tickLength(-1); }, "must be positive"); + + assert.throws(function () { + return baseAxis.tickLength(-1); + }, "must be positive"); + svg.remove(); }); + it("endTickLength()", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -276,18 +351,27 @@ describe("BaseAxis", function () { scale.range([0, SVG_WIDTH]); var baseAxis = new Plottable.Abstract.Axis(scale, "bottom"); var tickValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - baseAxis._getTickValues = function () { return tickValues; }; + baseAxis._getTickValues = function () { + return tickValues; + }; baseAxis.renderTo(svg); + var firstTickMark = svg.selectAll("." + Plottable.Abstract.Axis.END_TICK_MARK_CLASS); assert.strictEqual(firstTickMark.attr("x1"), "0"); assert.strictEqual(firstTickMark.attr("x2"), "0"); assert.strictEqual(firstTickMark.attr("y1"), "0"); assert.strictEqual(firstTickMark.attr("y2"), String(baseAxis.endTickLength())); + baseAxis.endTickLength(10); assert.strictEqual(firstTickMark.attr("y2"), String(baseAxis.endTickLength()), "end tick length was updated"); - assert.throws(function () { return baseAxis.endTickLength(-1); }, "must be positive"); + + assert.throws(function () { + return baseAxis.endTickLength(-1); + }, "must be positive"); + svg.remove(); }); + it("height is adjusted to greater of tickLength or endTickLength", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -296,47 +380,72 @@ describe("BaseAxis", function () { var baseAxis = new Plottable.Abstract.Axis(scale, "bottom"); baseAxis.showEndTickLabels(true); baseAxis.renderTo(svg); + var expectedHeight = Math.max(baseAxis.tickLength(), baseAxis.endTickLength()) + baseAxis.gutter(); assert.strictEqual(baseAxis.height(), expectedHeight, "height should be equal to the maximum of the two"); + baseAxis.tickLength(20); assert.strictEqual(baseAxis.height(), 20 + baseAxis.gutter(), "height should increase to tick length"); + baseAxis.endTickLength(30); assert.strictEqual(baseAxis.height(), 30 + baseAxis.gutter(), "height should increase to end tick length"); + baseAxis.tickLength(10); assert.strictEqual(baseAxis.height(), 30 + baseAxis.gutter(), "height should not decrease"); + svg.remove(); }); }); +/// var assert = chai.assert; + describe("TimeAxis", function () { it("can not initialize vertical time axis", function () { var scale = new Plottable.Scale.Time(); - assert.throws(function () { return new Plottable.Axis.Time(scale, "left"); }, "unsupported"); - assert.throws(function () { return new Plottable.Axis.Time(scale, "right"); }, "unsupported"); + assert.throws(function () { + return new Plottable.Axis.Time(scale, "left"); + }, "unsupported"); + assert.throws(function () { + return new Plottable.Axis.Time(scale, "right"); + }, "unsupported"); }); + it("major and minor intervals arrays are the same length", function () { assert.equal(Plottable.Axis.Time.majorIntervals.length, Plottable.Axis.Time.minorIntervals.length, "major and minor interval arrays must be same size"); }); + it("Computing the default ticks doesn't error out for edge cases", function () { var svg = generateSVG(400, 100); var scale = new Plottable.Scale.Time(); var axis = new Plottable.Axis.Time(scale, "bottom"); scale.range([0, 400]); - assert.doesNotThrow(function () { return scale.domain([new Date(0, 0, 1, 0, 0, 0, 0), new Date(50000, 0, 1, 0, 0, 0, 0)]); }); + + // very large time span + assert.doesNotThrow(function () { + return scale.domain([new Date(0, 0, 1, 0, 0, 0, 0), new Date(50000, 0, 1, 0, 0, 0, 0)]); + }); axis.renderTo(svg); - assert.doesNotThrow(function () { return scale.domain([new Date(0, 0, 1, 0, 0, 0, 0), new Date(0, 0, 1, 0, 0, 0, 100)]); }); + + // very small time span + assert.doesNotThrow(function () { + return scale.domain([new Date(0, 0, 1, 0, 0, 0, 0), new Date(0, 0, 1, 0, 0, 0, 100)]); + }); axis.renderTo(svg); + svg.remove(); }); + it("Tick labels don't overlap", function () { var svg = generateSVG(400, 100); var scale = new Plottable.Scale.Time(); scale.range([0, 400]); var axis = new Plottable.Axis.Time(scale, "bottom"); + function checkDomain(domain) { scale.domain(domain); axis.renderTo(svg); + function checkLabelsForContainer(container) { var visibleTickLabels = container.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; @@ -348,25 +457,44 @@ describe("TimeAxis", function () { for (var j = i + 1; j < numLabels; j++) { box1 = visibleTickLabels[0][i].getBoundingClientRect(); box2 = visibleTickLabels[0][j].getBoundingClientRect(); + assert.isFalse(Plottable.Util.DOM.boxesOverlap(box1, box2), "tick labels don't overlap"); } } } + checkLabelsForContainer(axis._minorTickLabels); checkLabelsForContainer(axis._majorTickLabels); } + + // 100 year span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2100, 0, 1, 0, 0, 0, 0)]); + + // 1 year span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 11, 31, 0, 0, 0, 0)]); + + // 1 month span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 1, 1, 0, 0, 0, 0)]); + + // 1 day span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 23, 0, 0, 0)]); + + // 1 hour span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 1, 0, 0, 0)]); + + // 1 minute span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 0, 1, 0, 0)]); + + // 1 second span checkDomain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 0, 0, 1, 0)]); + svg.remove(); }); }); +/// var assert = chai.assert; + describe("NumericAxis", function () { function boxesOverlap(boxA, boxB) { if (boxA.right < boxB.left) { @@ -383,8 +511,9 @@ describe("NumericAxis", function () { } return true; } + function boxIsInside(inner, outer, epsilon) { - if (epsilon === void 0) { epsilon = 0; } + if (typeof epsilon === "undefined") { epsilon = 0; } if (inner.left < outer.left - epsilon) { return false; } @@ -399,15 +528,26 @@ describe("NumericAxis", function () { } return true; } + it("tickLabelPosition() input validation", function () { var scale = new Plottable.Scale.Linear(); var horizontalAxis = new Plottable.Axis.Numeric(scale, "bottom"); - assert.throws(function () { return horizontalAxis.tickLabelPosition("top"); }, "horizontal"); - assert.throws(function () { return horizontalAxis.tickLabelPosition("bottom"); }, "horizontal"); + assert.throws(function () { + return horizontalAxis.tickLabelPosition("top"); + }, "horizontal"); + assert.throws(function () { + return horizontalAxis.tickLabelPosition("bottom"); + }, "horizontal"); + var verticalAxis = new Plottable.Axis.Numeric(scale, "left"); - assert.throws(function () { return verticalAxis.tickLabelPosition("left"); }, "vertical"); - assert.throws(function () { return verticalAxis.tickLabelPosition("right"); }, "vertical"); + assert.throws(function () { + return verticalAxis.tickLabelPosition("left"); + }, "vertical"); + assert.throws(function () { + return verticalAxis.tickLabelPosition("right"); + }, "vertical"); }); + it("draws tick labels correctly (horizontal)", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -416,10 +556,12 @@ describe("NumericAxis", function () { scale.range([0, SVG_WIDTH]); var numericAxis = new Plottable.Axis.Numeric(scale, "bottom"); numericAxis.renderTo(svg); + var tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); assert.operator(tickLabels[0].length, ">=", 2, "at least two tick labels were drawn"); var tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); assert.strictEqual(tickLabels[0].length, tickMarks[0].length, "there is one label per mark"); + var i; var markBB; var labelBB; @@ -430,6 +572,8 @@ describe("NumericAxis", function () { var labelCenter = (labelBB.left + labelBB.right) / 2; assert.closeTo(labelCenter, markCenter, 1, "tick label is centered on mark"); } + + // labels to left numericAxis.tickLabelPosition("left"); tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); @@ -438,6 +582,8 @@ describe("NumericAxis", function () { labelBB = tickLabels[0][i].getBoundingClientRect(); assert.operator(labelBB.left, "<=", markBB.right, "tick label is to left of mark"); } + + // labels to right numericAxis.tickLabelPosition("right"); tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); @@ -446,8 +592,10 @@ describe("NumericAxis", function () { labelBB = tickLabels[0][i].getBoundingClientRect(); assert.operator(markBB.right, "<=", labelBB.left, "tick label is to right of mark"); } + svg.remove(); }); + it("draws ticks correctly (vertical)", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 500; @@ -456,10 +604,12 @@ describe("NumericAxis", function () { scale.range([0, SVG_HEIGHT]); var numericAxis = new Plottable.Axis.Numeric(scale, "left"); numericAxis.renderTo(svg); + var tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); assert.operator(tickLabels[0].length, ">=", 2, "at least two tick labels were drawn"); var tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); assert.strictEqual(tickLabels[0].length, tickMarks[0].length, "there is one label per mark"); + var i; var markBB; var labelBB; @@ -470,6 +620,8 @@ describe("NumericAxis", function () { var labelCenter = (labelBB.top + labelBB.bottom) / 2; assert.closeTo(labelCenter, markCenter, 1, "tick label is centered on mark"); } + + // labels to top numericAxis.tickLabelPosition("top"); tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); @@ -478,6 +630,8 @@ describe("NumericAxis", function () { labelBB = tickLabels[0][i].getBoundingClientRect(); assert.operator(labelBB.bottom, "<=", markBB.top, "tick label is above mark"); } + + // labels to bottom numericAxis.tickLabelPosition("bottom"); tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); tickMarks = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS); @@ -486,25 +640,32 @@ describe("NumericAxis", function () { labelBB = tickLabels[0][i].getBoundingClientRect(); assert.operator(markBB.bottom, "<=", labelBB.top, "tick label is below mark"); } + svg.remove(); }); + it("uses the supplied Formatter", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 500; var svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); var scale = new Plottable.Scale.Linear(); scale.range([0, SVG_HEIGHT]); + var formatter = Plottable.Formatters.fixed(2); + var numericAxis = new Plottable.Axis.Numeric(scale, "left", formatter); numericAxis.renderTo(svg); + var tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); tickLabels.each(function (d, i) { var labelText = d3.select(this).text(); var formattedValue = formatter(d); assert.strictEqual(labelText, formattedValue, "The supplied Formatter was used to format the tick label"); }); + svg.remove(); }); + it("can hide tick labels that don't fit", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -512,20 +673,29 @@ describe("NumericAxis", function () { var scale = new Plottable.Scale.Linear(); scale.range([0, SVG_WIDTH]); var numericAxis = new Plottable.Axis.Numeric(scale, "bottom"); + numericAxis.showEndTickLabel("left", false); assert.isFalse(numericAxis.showEndTickLabel("left"), "retrieve showEndTickLabel setting"); numericAxis.showEndTickLabel("right", true); assert.isTrue(numericAxis.showEndTickLabel("right"), "retrieve showEndTickLabel setting"); - assert.throws(function () { return numericAxis.showEndTickLabel("top", true); }, Error); - assert.throws(function () { return numericAxis.showEndTickLabel("bottom", true); }, Error); + assert.throws(function () { + return numericAxis.showEndTickLabel("top", true); + }, Error); + assert.throws(function () { + return numericAxis.showEndTickLabel("bottom", true); + }, Error); + numericAxis.renderTo(svg); + var tickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS); var firstLabel = d3.select(tickLabels[0][0]); assert.strictEqual(firstLabel.style("visibility"), "hidden", "first label is hidden"); var lastLabel = d3.select(tickLabels[0][tickLabels[0].length - 1]); assert.strictEqual(lastLabel.style("visibility"), "hidden", "last label is hidden"); + svg.remove(); }); + it("tick labels don't overlap in a constrained space", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 100; @@ -535,6 +705,7 @@ describe("NumericAxis", function () { var numericAxis = new Plottable.Axis.Numeric(scale, "bottom"); numericAxis.showEndTickLabel("left", false).showEndTickLabel("right", false); numericAxis.renderTo(svg); + var visibleTickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; }); @@ -545,9 +716,11 @@ describe("NumericAxis", function () { for (var j = i + 1; j < numLabels; j++) { box1 = visibleTickLabels[0][i].getBoundingClientRect(); box2 = visibleTickLabels[0][j].getBoundingClientRect(); + assert.isFalse(Plottable.Util.DOM.boxesOverlap(box1, box2), "tick labels don't overlap"); } } + numericAxis.orient("bottom"); visibleTickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; @@ -557,11 +730,14 @@ describe("NumericAxis", function () { for (j = i + 1; j < numLabels; j++) { box1 = visibleTickLabels[0][i].getBoundingClientRect(); box2 = visibleTickLabels[0][j].getBoundingClientRect(); + assert.isFalse(Plottable.Util.DOM.boxesOverlap(box1, box2), "tick labels don't overlap"); } } + svg.remove(); }); + it("allocates enough width to show all tick labels when vertical", function () { var SVG_WIDTH = 100; var SVG_HEIGHT = 500; @@ -569,14 +745,17 @@ describe("NumericAxis", function () { var scale = new Plottable.Scale.Linear(); scale.domain([5, -5]); scale.range([0, SVG_HEIGHT]); + var formatter = function (d) { if (d === 0) { return "This is zero"; } return String(d); }; + var numericAxis = new Plottable.Axis.Numeric(scale, "left", formatter); numericAxis.renderTo(svg); + var visibleTickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; }); @@ -589,6 +768,7 @@ describe("NumericAxis", function () { } svg.remove(); }); + it("allocates enough height to show all tick labels when horizontal", function () { var SVG_WIDTH = 500; var SVG_HEIGHT = 100; @@ -596,9 +776,12 @@ describe("NumericAxis", function () { var scale = new Plottable.Scale.Linear(); scale.domain([5, -5]); scale.range([0, SVG_WIDTH]); + var formatter = Plottable.Formatters.fixed(2); + var numericAxis = new Plottable.Axis.Numeric(scale, "bottom", formatter); numericAxis.renderTo(svg); + var visibleTickLabels = numericAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_LABEL_CLASS).filter(function (d, i) { return d3.select(this).style("visibility") === "visible"; }); @@ -613,6 +796,7 @@ describe("NumericAxis", function () { }); }); +/// var assert = chai.assert; describe("Category Axes", function () { it("re-renders appropriately when data is changed", function () { @@ -621,10 +805,13 @@ describe("Category Axes", function () { var ca = new Plottable.Axis.Category(xScale, "left"); ca.renderTo(svg); assert.deepEqual(ca._tickLabelContainer.selectAll(".tick-label").data(), xScale.domain(), "tick labels render domain"); - assert.doesNotThrow(function () { return xScale.domain(["bar", "baz", "bam"]); }); + assert.doesNotThrow(function () { + return xScale.domain(["bar", "baz", "bam"]); + }); assert.deepEqual(ca._tickLabelContainer.selectAll(".tick-label").data(), xScale.domain(), "tick labels render domain"); svg.remove(); }); + it("requests appropriate space when the scale has no domain", function () { var svg = generateSVG(400, 400); var scale = new Plottable.Scale.Ordinal(); @@ -639,23 +826,29 @@ describe("Category Axes", function () { }); }); +/// var assert = chai.assert; + describe("Gridlines", function () { it("Gridlines and axis tick marks align", function () { var svg = generateSVG(640, 480); var xScale = new Plottable.Scale.Linear(); - xScale.domain([0, 10]); + xScale.domain([0, 10]); // manually set domain since we won't have a renderer var xAxis = new Plottable.Axis.Numeric(xScale, "bottom"); + var yScale = new Plottable.Scale.Linear(); yScale.domain([0, 10]); var yAxis = new Plottable.Axis.Numeric(yScale, "left"); + var gridlines = new Plottable.Component.Gridlines(xScale, yScale); var basicTable = new Plottable.Component.Table().addComponent(0, 0, yAxis).addComponent(0, 1, gridlines).addComponent(1, 1, xAxis); + basicTable._anchor(svg); basicTable._computeLayout(); - xScale.range([0, xAxis.availableWidth]); + xScale.range([0, xAxis.availableWidth]); // manually set range since we don't have a renderer yScale.range([yAxis.availableHeight, 0]); basicTable._render(); + var xAxisTickMarks = xAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS)[0]; var xGridlines = gridlines.element.select(".x-gridlines").selectAll("line")[0]; assert.equal(xAxisTickMarks.length, xGridlines.length, "There is an x gridline for each x tick"); @@ -664,6 +857,7 @@ describe("Gridlines", function () { var xGridlineRect = xGridlines[i].getBoundingClientRect(); assert.closeTo(xTickMarkRect.left, xGridlineRect.left, 1, "x tick and gridline align"); } + var yAxisTickMarks = yAxis.element.selectAll("." + Plottable.Abstract.Axis.TICK_MARK_CLASS)[0]; var yGridlines = gridlines.element.select(".y-gridlines").selectAll("line")[0]; assert.equal(yAxisTickMarks.length, yGridlines.length, "There is an x gridline for each x tick"); @@ -672,32 +866,40 @@ describe("Gridlines", function () { var yGridlineRect = yGridlines[j].getBoundingClientRect(); assert.closeTo(yTickMarkRect.top, yGridlineRect.top, 1, "y tick and gridline align"); } + svg.remove(); }); + it("Unanchored Gridlines don't throw an error when scale updates", function () { var xScale = new Plottable.Scale.Linear(); var gridlines = new Plottable.Component.Gridlines(xScale, null); xScale.domain([0, 1]); + // test passes if error is not thrown. }); }); +/// var assert = chai.assert; + describe("Labels", function () { it("Standard text title label generates properly", function () { var svg = generateSVG(400, 80); var label = new Plottable.Component.TitleLabel("A CHART TITLE"); label.renderTo(svg); + var content = label.content; assert.isTrue(label.element.classed("label"), "title element has label css class"); assert.isTrue(label.element.classed("title-label"), "title element has title-label css class"); var textChildren = content.selectAll("text"); assert.lengthOf(textChildren, 1, "There is one text node in the parent element"); + var text = content.select("text"); var bbox = Plottable.Util.DOM.getBBox(text); assert.closeTo(bbox.height, label.availableHeight, 0.5, "text height === label.minimumHeight()"); assert.equal(text.node().textContent, "A CHART TITLE", "node's text content is as expected"); svg.remove(); }); + it("Left-rotated text is handled properly", function () { var svg = generateSVG(100, 400); var label = new Plottable.Component.AxisLabel("LEFT-ROTATED LABEL", "vertical-left"); @@ -709,6 +911,7 @@ describe("Labels", function () { assert.closeTo(textBBox.height, label.availableWidth, window.Pixel_CloseTo_Requirement, "text height"); svg.remove(); }); + it("Right-rotated text is handled properly", function () { var svg = generateSVG(100, 400); var label = new Plottable.Component.AxisLabel("RIGHT-ROTATED LABEL", "vertical-right"); @@ -720,6 +923,7 @@ describe("Labels", function () { assert.closeTo(textBBox.height, label.availableWidth, window.Pixel_CloseTo_Requirement, "text height"); svg.remove(); }); + it("Label text can be changed after label is created", function () { var svg = generateSVG(400, 80); var label = new Plottable.Component.TitleLabel(); @@ -732,6 +936,8 @@ describe("Labels", function () { assert.operator(label.availableHeight, ">", 0, "rowMin is > 0 for non-empty string"); svg.remove(); }); + + // skipping because Dan is rewriting labels and the height test fails it.skip("Superlong text is handled in a sane fashion", function () { var svgWidth = 400; var svg = generateSVG(svgWidth, 80); @@ -744,6 +950,7 @@ describe("Labels", function () { assert.operator(bbox.width, "<=", svgWidth, "the text is not wider than the SVG width"); svg.remove(); }); + it("text in a tiny box is truncated to empty string", function () { var svg = generateSVG(10, 10); var label = new Plottable.Component.TitleLabel("Yeah, not gonna fit..."); @@ -752,6 +959,7 @@ describe("Labels", function () { assert.equal(text.text(), "", "text was truncated to empty string"); svg.remove(); }); + it("centered text in a table is positioned properly", function () { var svg = generateSVG(400, 400); var label = new Plottable.Component.TitleLabel("X"); @@ -763,6 +971,7 @@ describe("Labels", function () { assert.closeTo(eleTranslate[0] + textTranslate[0] + textWidth / 2, 200, 5, "label is centered"); svg.remove(); }); + it("if a label text is changed to empty string, width updates to 0", function () { var svg = generateSVG(400, 400); var label = new Plottable.Component.TitleLabel("foo"); @@ -771,26 +980,34 @@ describe("Labels", function () { assert.equal(label.availableWidth, 0, "width updated to 0"); svg.remove(); }); + it("unsupported alignments and orientations are unsupported", function () { - assert.throws(function () { return new Plottable.Component.Label("foo", "bar"); }, Error, "not a valid orientation"); + assert.throws(function () { + return new Plottable.Component.Label("foo", "bar"); + }, Error, "not a valid orientation"); }); }); +/// var assert = chai.assert; + describe("Legends", function () { var svg; var color; var legend; + beforeEach(function () { svg = generateSVG(400, 400); color = new Plottable.Scale.Color("Category10"); legend = new Plottable.Component.Legend(color); }); + it("a basic legend renders", function () { color.domain(["foo", "bar", "baz"]); legend.renderTo(svg); var rows = legend.content.selectAll(".legend-row"); assert.lengthOf(rows[0], 3, "there are 3 legend entries"); + rows.each(function (d, i) { assert.equal(d, color.domain()[i], "the data is set properly"); var d3this = d3.select(this); @@ -801,6 +1018,7 @@ describe("Legends", function () { }); svg.remove(); }); + it("legend domain can be updated after initialization, and height updates as well", function () { legend.renderTo(svg); legend.scale(color); @@ -817,16 +1035,20 @@ describe("Legends", function () { assert.equal(numRows, 3, "there are 3 rows"); svg.remove(); }); + it("a legend with many labels does not overflow vertically", function () { color.domain(["alpha", "beta", "gamma", "delta", "omega", "omicron", "persei", "eight"]); legend.renderTo(svg); + var contentBBox = Plottable.Util.DOM.getBBox(legend.content); var contentBottomEdge = contentBBox.y + contentBBox.height; var bboxBBox = Plottable.Util.DOM.getBBox(legend.element.select(".bounding-box")); var bboxBottomEdge = bboxBBox.y + bboxBBox.height; + assert.operator(contentBottomEdge, "<=", bboxBottomEdge, "content does not extend past bounding box"); svg.remove(); }); + it("a legend with a long label does not overflow horizontally", function () { color.domain(["foooboooloonoogoorooboopoo"]); svg.attr("width", 100); @@ -839,6 +1061,7 @@ describe("Legends", function () { assert.operator(rightEdge, "<=", rightEdgeBBox, "the long text did not overflow the legend"); svg.remove(); }); + it("calling legend.render multiple times does not add more elements", function () { color.domain(["foo", "bar", "baz"]); legend.renderTo(svg); @@ -849,11 +1072,14 @@ describe("Legends", function () { assert.equal(numRows, 3, "there are 3 legend rows after second render"); svg.remove(); }); + it("re-rendering the legend with a new domain will do the right thing", function () { color.domain(["foo", "bar", "baz"]); legend.renderTo(svg); var newDomain = ["mushu", "foo", "persei", "baz", "eight"]; color.domain(newDomain); + + // due to how joins work, this is how the elements should be arranged by d3 var newDomainActualOrder = ["foo", "baz", "mushu", "persei", "eight"]; legend.content.selectAll(".legend-row").each(function (d, i) { assert.equal(d, newDomainActualOrder[i], "the data is set correctly"); @@ -865,13 +1091,16 @@ describe("Legends", function () { assert.lengthOf(legend.content.selectAll(".legend-row")[0], 5, "there are the right number of legend elements"); svg.remove(); }); + it("legend.scale() replaces domain", function () { color.domain(["foo", "bar", "baz"]); legend.renderTo(svg); + var newDomain = ["a", "b", "c"]; var newColorScale = new Plottable.Scale.Color("20"); newColorScale.domain(newDomain); legend.scale(newColorScale); + legend.content.selectAll(".legend-row").each(function (d, i) { assert.equal(d, newDomain[i], "the data is set correctly"); var text = d3.select(this).select("text").text(); @@ -879,15 +1108,19 @@ describe("Legends", function () { var fill = d3.select(this).select("circle").attr("fill"); assert.equal(fill, newColorScale.scale(d), "the fill was set properly"); }); + svg.remove(); }); + it("legend.scale() correctly reregisters listeners", function () { color.domain(["foo", "bar", "baz"]); legend.renderTo(svg); + var tempDomain = ["a", "b", "c"]; var newColorScale = new Plottable.Scale.Color("20"); newColorScale.domain(tempDomain); legend.scale(newColorScale); + var newDomain = ["a", "foo", "d"]; newColorScale.domain(newDomain); legend.content.selectAll(".legend-row").each(function (d, i) { @@ -899,11 +1132,13 @@ describe("Legends", function () { }); svg.remove(); }); + it("icon radius is not too small or too big", function () { color.domain(["foo"]); legend.renderTo(svg); var style = legend.element.append("style"); style.attr("type", "text/css"); + function verifyCircleHeight() { var text = legend.content.select("text"); var circle = legend.content.select("circle"); @@ -912,38 +1147,51 @@ describe("Legends", function () { assert.operator(circleHeight, "<", textHeight, "icons are too big. iconHeight = " + circleHeight + " vs circleHeight = " + circleHeight); assert.operator(circleHeight, ">", textHeight / 2, "icons are too small. iconHeight = " + circleHeight + " vs circleHeight = " + circleHeight); } + verifyCircleHeight(); + style.text(".plottable .legend text { font-size: 60px; }"); legend._computeLayout(); legend._render(); verifyCircleHeight(); + style.text(".plottable .legend text { font-size: 10px; }"); legend._computeLayout(); legend._render(); verifyCircleHeight(); + svg.remove(); }); + describe("Legend toggle tests", function () { var toggleLegend; + beforeEach(function () { toggleLegend = new Plottable.Component.Legend(color); toggleLegend.toggleCallback(function (d, b) { }); }); + function verifyState(selection, b, msg) { assert.equal(selection.classed("toggled-on"), b, msg); assert.equal(selection.classed("toggled-off"), !b, msg); } + function getSelection(datum) { - var selection = toggleLegend.content.selectAll(".legend-row").filter(function (d, i) { return d === datum; }); + var selection = toggleLegend.content.selectAll(".legend-row").filter(function (d, i) { + return d === datum; + }); return selection; } + function verifyEntry(datum, b, msg) { verifyState(getSelection(datum), b, msg); } + function toggleEntry(datum, index) { getSelection(datum).on("click")(datum, index); } + it("basic initialization test", function () { color.domain(["a", "b", "c", "d", "e"]); toggleLegend.renderTo(svg); @@ -953,6 +1201,7 @@ describe("Legends", function () { }); svg.remove(); }); + it("basic toggling test", function () { color.domain(["a"]); toggleLegend.renderTo(svg); @@ -965,6 +1214,7 @@ describe("Legends", function () { }); svg.remove(); }); + it("scale() works as intended with toggling", function () { var domain = ["a", "b", "c", "d", "e"]; color.domain(domain); @@ -972,22 +1222,27 @@ describe("Legends", function () { toggleEntry("a", 0); toggleEntry("d", 3); toggleEntry("c", 2); + var newDomain = ["r", "a", "d", "g"]; var newColorScale = new Plottable.Scale.Color("Category10"); newColorScale.domain(newDomain); toggleLegend.scale(newColorScale); + verifyEntry("r", true); verifyEntry("a", false); verifyEntry("g", true); verifyEntry("d", false); + svg.remove(); }); + it("listeners on scale will correctly update states", function () { color.domain(["a", "b", "c", "d", "e"]); toggleLegend.renderTo(svg); toggleEntry("a", 0); toggleEntry("d", 3); toggleEntry("c", 2); + color.domain(["e", "d", "b", "a", "c"]); verifyEntry("a", false); verifyEntry("b", true); @@ -996,101 +1251,132 @@ describe("Legends", function () { verifyEntry("e", true); svg.remove(); }); + it("Testing callback works correctly", function () { var domain = ["a", "b", "c", "d", "e"]; color.domain(domain); var state = [true, true, true, true, true]; + toggleLegend.toggleCallback(function (d, b) { state[domain.indexOf(d)] = b; }); toggleLegend.renderTo(svg); + toggleEntry("a", 0); verifyEntry("a", false); assert.equal(state[0], false, "callback was successful"); + toggleEntry("d", 3); verifyEntry("d", false); assert.equal(state[3], false, "callback was successful"); + toggleEntry("a", 0); verifyEntry("a", true); assert.equal(state[0], true, "callback was successful"); + toggleEntry("c", 2); verifyEntry("c", false); assert.equal(state[2], false, "callback was successful"); svg.remove(); }); + it("Overwriting callback is successfull", function () { var domain = ["a"]; color.domain(domain); var state = true; toggleLegend.renderTo(svg); + toggleLegend.toggleCallback(function (d, b) { state = b; }); + toggleEntry("a", 0); assert.equal(state, false, "callback was successful"); + var count = 0; toggleLegend.toggleCallback(function (d, b) { count++; }); + toggleEntry("a", 0); assert.equal(state, false, "callback was overwritten"); assert.equal(count, 1, "new callback was successfully called"); svg.remove(); }); + it("Removing callback is successful", function () { var domain = ["a"]; color.domain(domain); var state = true; toggleLegend.renderTo(svg); + toggleLegend.toggleCallback(function (d, b) { state = b; }); + toggleEntry("a", 0); assert.equal(state, false, "callback was successful"); - toggleLegend.toggleCallback(); + + toggleLegend.toggleCallback(); // this should not remove the callback toggleEntry("a", 0); assert.equal(state, true, "callback was successful"); - toggleLegend.toggleCallback(null); + + toggleLegend.toggleCallback(null); // this should remove the callback assert.throws(function () { toggleEntry("a", 0); }); var selection = getSelection("a"); + + // should have no classes assert.equal(selection.classed("toggled-on"), false, "is not toggled-on"); assert.equal(selection.classed("toggled-off"), false, "is not toggled-off"); + svg.remove(); }); }); + describe("Legend hover tests", function () { var hoverLegend; + beforeEach(function () { hoverLegend = new Plottable.Component.Legend(color); hoverLegend.hoverCallback(function (d) { }); }); + function _verifyFocus(selection, b, msg) { assert.equal(selection.classed("hover"), true, msg); assert.equal(selection.classed("focus"), b, msg); } + function _verifyEmpty(selection, msg) { assert.equal(selection.classed("hover"), false, msg); assert.equal(selection.classed("focus"), false, msg); } + function getSelection(datum) { - var selection = hoverLegend.content.selectAll(".legend-row").filter(function (d, i) { return d === datum; }); + var selection = hoverLegend.content.selectAll(".legend-row").filter(function (d, i) { + return d === datum; + }); return selection; } + function verifyFocus(datum, b, msg) { _verifyFocus(getSelection(datum), b, msg); } + function verifyEmpty(datum, msg) { _verifyEmpty(getSelection(datum), msg); } + function hoverEntry(datum, index) { getSelection(datum).on("mouseover")(datum, index); } + function leaveEntry(datum, index) { getSelection(datum).on("mouseout")(datum, index); } + it("basic initialization test", function () { color.domain(["a", "b", "c", "d", "e"]); hoverLegend.renderTo(svg); @@ -1099,6 +1385,7 @@ describe("Legends", function () { }); svg.remove(); }); + it("basic hover test", function () { color.domain(["a"]); hoverLegend.renderTo(svg); @@ -1108,30 +1395,38 @@ describe("Legends", function () { verifyEmpty("a"); svg.remove(); }); + it("scale() works as intended with hovering", function () { var domain = ["a", "b", "c", "d", "e"]; color.domain(domain); hoverLegend.renderTo(svg); + hoverEntry("a", 0); + var newDomain = ["r", "a", "d", "g"]; var newColorScale = new Plottable.Scale.Color("Category10"); newColorScale.domain(newDomain); hoverLegend.scale(newColorScale); + verifyFocus("r", false, "r"); verifyFocus("a", true, "a"); verifyFocus("g", false, "g"); verifyFocus("d", false, "d"); + leaveEntry("a", 0); verifyEmpty("r"); verifyEmpty("a"); verifyEmpty("g"); verifyEmpty("d"); + svg.remove(); }); + it("listeners on scale will correctly update states", function () { color.domain(["a", "b", "c", "d", "e"]); hoverLegend.renderTo(svg); hoverEntry("c", 2); + color.domain(["e", "d", "b", "a", "c"]); verifyFocus("a", false); verifyFocus("b", false); @@ -1140,39 +1435,49 @@ describe("Legends", function () { verifyFocus("e", false); svg.remove(); }); + it("Testing callback works correctly", function () { var domain = ["a", "b", "c", "d", "e"]; color.domain(domain); var focused = undefined; + hoverLegend.hoverCallback(function (d) { focused = d; }); hoverLegend.renderTo(svg); + hoverEntry("a", 0); verifyFocus("a", true); assert.equal(focused, "a", "callback was successful"); + leaveEntry("a", 0); assert.equal(focused, undefined, "callback was successful"); + hoverEntry("d", 3); verifyFocus("d", true); assert.equal(focused, "d", "callback was successful"); svg.remove(); }); + it("Overwriting callback is successfull", function () { var domain = ["a"]; color.domain(domain); var focused = undefined; hoverLegend.renderTo(svg); + hoverLegend.hoverCallback(function (d) { focused = d; }); + hoverEntry("a", 0); assert.equal(focused, "a", "callback was successful"); leaveEntry("a", 0); + var count = 0; hoverLegend.hoverCallback(function (d) { count++; }); + hoverEntry("a", 0); assert.equal(focused, undefined, "old callback was not called"); assert.equal(count, 1, "new callbcak was called"); @@ -1180,29 +1485,36 @@ describe("Legends", function () { assert.equal(count, 2, "new callback was called"); svg.remove(); }); + it("Removing callback is successful", function () { var domain = ["a"]; color.domain(domain); var focused = undefined; hoverLegend.renderTo(svg); + hoverLegend.hoverCallback(function (d) { focused = d; }); + hoverEntry("a", 0); assert.equal(focused, "a", "callback was successful"); - hoverLegend.hoverCallback(); + + hoverLegend.hoverCallback(); // this should not remove the callback leaveEntry("a", 0); assert.equal(focused, undefined, "callback was successful"); - hoverLegend.hoverCallback(null); + + hoverLegend.hoverCallback(null); // this should remove the callback assert.throws(function () { hoverEntry("a", 0); }); verifyEmpty("a"); + svg.remove(); }); }); }); +/// var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -1222,12 +1534,14 @@ var CountingPlot = (function (_super) { }; return CountingPlot; })(Plottable.Abstract.Plot); + describe("Plots", function () { describe("Abstract Plot", function () { it("Plots default correctly", function () { var r = new Plottable.Abstract.Plot(); assert.isTrue(r.clipPathEnabled, "clipPathEnabled defaults to true"); }); + it("Base Plot functionality works", function () { var svg = generateSVG(400, 300); var d1 = new Plottable.DataSource(["foo"], { cssClass: "bar" }); @@ -1238,38 +1552,51 @@ describe("Plots", function () { assert.isNotNull(renderArea.node(), "there is a render-area"); svg.remove(); }); + it("Allows the DataSource to be changed", function () { var d1 = new Plottable.DataSource(["foo"], { cssClass: "bar" }); var r = new Plottable.Abstract.Plot(d1); assert.equal(d1, r.dataSource(), "returns the original"); + var d2 = new Plottable.DataSource(["bar"], { cssClass: "boo" }); r.dataSource(d2); assert.equal(d2, r.dataSource(), "returns new datasource"); }); + it("Changes DataSource listeners when the DataSource is changed", function () { var d1 = new Plottable.DataSource(["foo"], { cssClass: "bar" }); var r = new CountingPlot(d1); + assert.equal(0, r.renders, "initially hasn't rendered anything"); + d1.broadcaster.broadcast(); assert.equal(1, r.renders, "we re-render when our datasource changes"); + r.dataSource(); assert.equal(1, r.renders, "we shouldn't redraw when querying the datasource"); + var d2 = new Plottable.DataSource(["bar"], { cssClass: "boo" }); r.dataSource(d2); assert.equal(2, r.renders, "we should redraw when we change datasource"); + d1.broadcaster.broadcast(); assert.equal(2, r.renders, "we shouldn't listen to the old datasource"); + d2.broadcaster.broadcast(); assert.equal(3, r.renders, "we should listen to the new datasource"); }); + it("Updates its projectors when the DataSource is changed", function () { var d1 = new Plottable.DataSource([{ x: 5, y: 6 }], { cssClass: "bar" }); var r = new Plottable.Abstract.Plot(d1); + var xScaleCalls = 0; var yScaleCalls = 0; var xScale = new Plottable.Scale.Linear(); var yScale = new Plottable.Scale.Linear(); - var metadataProjector = function (d, i, m) { return m.cssClass; }; + var metadataProjector = function (d, i, m) { + return m.cssClass; + }; r.project("x", "x", xScale); r.project("y", "y", yScale); r.project("meta", metadataProjector); @@ -1281,26 +1608,34 @@ describe("Plots", function () { assert.equal(listenable, yScale, "Callback received the calling scale as the first argument"); ++yScaleCalls; }); + assert.equal(0, xScaleCalls, "initially hasn't made any X callbacks"); assert.equal(0, yScaleCalls, "initially hasn't made any Y callbacks"); + d1.broadcaster.broadcast(); assert.equal(1, xScaleCalls, "X scale was wired up to datasource correctly"); assert.equal(1, yScaleCalls, "Y scale was wired up to datasource correctly"); + var metaProjector = r._generateAttrToProjector()["meta"]; assert.equal(metaProjector(null, 0), "bar", "plot projector used the right metadata"); + var d2 = new Plottable.DataSource([{ x: 7, y: 8 }], { cssClass: "boo" }); r.dataSource(d2); assert.equal(2, xScaleCalls, "Changing datasource fires X scale listeners (but doesn't coalesce callbacks)"); assert.equal(2, yScaleCalls, "Changing datasource fires Y scale listeners (but doesn't coalesce callbacks)"); + d1.broadcaster.broadcast(); assert.equal(2, xScaleCalls, "X scale was unhooked from old datasource"); assert.equal(2, yScaleCalls, "Y scale was unhooked from old datasource"); + d2.broadcaster.broadcast(); assert.equal(3, xScaleCalls, "X scale was hooked into new datasource"); assert.equal(3, yScaleCalls, "Y scale was hooked into new datasource"); + metaProjector = r._generateAttrToProjector()["meta"]; assert.equal(metaProjector(null, 0), "boo", "plot projector used the right metadata"); }); + it("Plot automatically generates a DataSource if only data is provided", function () { var data = ["foo", "bar"]; var r = new Plottable.Abstract.Plot(data); @@ -1308,6 +1643,7 @@ describe("Plots", function () { assert.isNotNull(dataSource, "A DataSource was automatically generated"); assert.deepEqual(dataSource.data(), data, "The generated DataSource has the correct data"); }); + it("Plot.project works as intended", function () { var r = new Plottable.Abstract.Plot(); var s = new Plottable.Scale.Linear().domain([0, 1]).range([0, 10]); @@ -1316,20 +1652,26 @@ describe("Plots", function () { var projector = attrToProjector["attr"]; assert.equal(projector({ "a": 0.5 }, 0), 5, "projector works as intended"); }); + it("Changing Plot.dataSource().data to [] causes scale to contract", function () { var ds1 = new Plottable.DataSource([0, 1, 2]); var ds2 = new Plottable.DataSource([1, 2, 3]); var s = new Plottable.Scale.Linear(); var svg1 = generateSVG(100, 100); var svg2 = generateSVG(100, 100); - var r1 = new Plottable.Abstract.Plot().dataSource(ds1).project("x", function (x) { return x; }, s).renderTo(svg1); - var r2 = new Plottable.Abstract.Plot().dataSource(ds2).project("x", function (x) { return x; }, s).renderTo(svg2); + var r1 = new Plottable.Abstract.Plot().dataSource(ds1).project("x", function (x) { + return x; + }, s).renderTo(svg1); + var r2 = new Plottable.Abstract.Plot().dataSource(ds2).project("x", function (x) { + return x; + }, s).renderTo(svg2); assert.deepEqual(s.domain(), [0, 3], "Simple domain combining"); ds1.data([]); assert.deepEqual(s.domain(), [1, 3], "Contracting domain due to projection becoming empty"); svg1.remove(); svg2.remove(); }); + it("remove() disconnects plots from its scales", function () { var r = new Plottable.Abstract.Plot(); var s = new Plottable.Scale.Linear(); @@ -1341,20 +1683,26 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; describe("Plots", function () { describe("New Style Plots", function () { var p; var oldWarn = Plottable.Util.Methods.warn; + beforeEach(function () { var xScale = new Plottable.Scale.Linear(); var yScale = new Plottable.Scale.Linear(); p = new Plottable.Abstract.NewStylePlot(xScale, yScale); - p._getDrawer = function (k) { return new Plottable._Drawer.Rect(k); }; + p._getDrawer = function (k) { + return new Plottable._Drawer.Rect(k); + }; }); + afterEach(function () { Plottable.Util.Methods.warn = oldWarn; }); + it("Datasets can be added and removed as expected", function () { p.addDataset("foo", [1, 2, 3]); var d2 = new Plottable.DataSource([4, 5, 6]); @@ -1362,20 +1710,26 @@ describe("Plots", function () { p.addDataset([7, 8, 9]); var d4 = new Plottable.DataSource([10, 11, 12]); p.addDataset(d4); + assert.deepEqual(p._datasetKeysInOrder, ["foo", "bar", "_0", "_1"], "dataset keys as expected"); var datasets = p._getDatasetsInOrder(); assert.deepEqual(datasets[0].data(), [1, 2, 3]); assert.equal(datasets[1], d2); assert.deepEqual(datasets[2].data(), [7, 8, 9]); assert.equal(datasets[3], d4); + p.removeDataset("foo"); p.removeDataset("_0"); + assert.deepEqual(p._datasetKeysInOrder, ["bar", "_1"]); assert.lengthOf(p._getDatasetsInOrder(), 2); }); + it("Datasets are listened to appropriately", function () { var callbackCounter = 0; - var callback = function () { return callbackCounter++; }; + var callback = function () { + return callbackCounter++; + }; p._onDataSourceUpdate = callback; var d = new Plottable.DataSource([1, 2, 3]); p.addDataset("foo", d); @@ -1385,6 +1739,7 @@ describe("Plots", function () { p.removeDataset("foo"); assert.equal(callbackCounter, 3, "removing dataset triggers listener"); }); + it("Datasets can be reordered", function () { p.addDataset("foo", [1]); p.addDataset("bar", [2]); @@ -1393,27 +1748,38 @@ describe("Plots", function () { p.datasetOrder(["bar", "baz", "foo"]); assert.deepEqual(p.datasetOrder(), ["bar", "baz", "foo"]); var warned = 0; - Plottable.Util.Methods.warn = function () { return warned++; }; + Plottable.Util.Methods.warn = function () { + return warned++; + }; // suppress expected warnings p.datasetOrder(["blah", "blee", "bar", "baz", "foo"]); assert.equal(warned, 1); assert.deepEqual(p.datasetOrder(), ["bar", "baz", "foo"]); }); + it("Has proper warnings", function () { var warned = 0; - Plottable.Util.Methods.warn = function () { return warned++; }; + Plottable.Util.Methods.warn = function () { + return warned++; + }; p.addDataset("_foo", []); assert.equal(warned, 1); p.addDataset("2", []); p.addDataset("4", []); + + // get warning for not a permutation p.datasetOrder(["_bar", "4", "2"]); assert.equal(warned, 2); + + // do not get warning for a permutation p.datasetOrder(["2", "_foo", "4"]); assert.equal(warned, 2); }); }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("LinePlot", function () { var svg; @@ -1426,23 +1792,36 @@ describe("Plots", function () { var linePlot; var renderArea; var verifier; - var normalizePath = function (s) { return s.replace(/ *([A-Z]) */g, "$1").replace(/ /g, ","); }; + + // for IE, whose paths look like "M 0 500 L" instead of "M0,500L" + var normalizePath = function (s) { + return s.replace(/ *([A-Z]) */g, "$1").replace(/ /g, ","); + }; + before(function () { svg = generateSVG(500, 500); verifier = new MultiTestVerifier(); xScale = new Plottable.Scale.Linear().domain([0, 1]); yScale = new Plottable.Scale.Linear().domain([0, 1]); - xAccessor = function (d) { return d.foo; }; - yAccessor = function (d) { return d.bar; }; - colorAccessor = function (d, i, m) { return d3.rgb(d.foo, d.bar, i).toString(); }; + xAccessor = function (d) { + return d.foo; + }; + yAccessor = function (d) { + return d.bar; + }; + colorAccessor = function (d, i, m) { + return d3.rgb(d.foo, d.bar, i).toString(); + }; simpleDataset = new Plottable.DataSource([{ foo: 0, bar: 0 }, { foo: 1, bar: 1 }]); linePlot = new Plottable.Plot.Line(simpleDataset, xScale, yScale); linePlot.project("x", xAccessor, xScale).project("y", yAccessor, yScale).project("stroke", colorAccessor).renderTo(svg); renderArea = linePlot.renderArea; }); + beforeEach(function () { verifier.start(); }); + it("draws a line correctly", function () { var linePath = renderArea.select(".line"); assert.strictEqual(normalizePath(linePath.attr("d")), "M0,500L500,0", "line d was set correctly"); @@ -1450,19 +1829,24 @@ describe("Plots", function () { assert.strictEqual(lineComputedStyle.fill, "none", "line fill renders as \"none\""); verifier.end(); }); + it("attributes set appropriately from accessor", function () { var areaPath = renderArea.select(".line"); assert.equal(areaPath.attr("stroke"), "#000000", "stroke set correctly"); verifier.end(); }); + it("attributes can be changed by projecting new accessor and re-render appropriately", function () { - var newColorAccessor = function () { return "pink"; }; + var newColorAccessor = function () { + return "pink"; + }; linePlot.project("stroke", newColorAccessor); linePlot.renderTo(svg); var linePath = renderArea.select(".line"); assert.equal(linePath.attr("stroke"), "pink", "stroke changed correctly"); verifier.end(); }); + it("attributes can be changed by projecting attribute accessor (sets to first datum attribute)", function () { var data = simpleDataset.data(); data.forEach(function (d) { @@ -1472,11 +1856,13 @@ describe("Plots", function () { linePlot.project("stroke", "stroke"); var areaPath = renderArea.select(".line"); assert.equal(areaPath.attr("stroke"), "pink", "stroke set to uniform stroke color"); + data[0].stroke = "green"; simpleDataset.data(data); assert.equal(areaPath.attr("stroke"), "green", "stroke set to first datum stroke color"); verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); @@ -1486,7 +1872,9 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("AreaPlot", function () { var svg; @@ -1501,31 +1889,49 @@ describe("Plots", function () { var areaPlot; var renderArea; var verifier; - var normalizePath = function (s) { return s.replace(/ *([A-Z]) */g, "$1").replace(/ /g, ","); }; + + // for IE, whose paths look like "M 0 500 L" instead of "M0,500L" + var normalizePath = function (s) { + return s.replace(/ *([A-Z]) */g, "$1").replace(/ /g, ","); + }; + before(function () { svg = generateSVG(500, 500); verifier = new MultiTestVerifier(); xScale = new Plottable.Scale.Linear().domain([0, 1]); yScale = new Plottable.Scale.Linear().domain([0, 1]); - xAccessor = function (d) { return d.foo; }; - yAccessor = function (d) { return d.bar; }; - y0Accessor = function () { return 0; }; - colorAccessor = function (d, i, m) { return d3.rgb(d.foo, d.bar, i).toString(); }; - fillAccessor = function () { return "steelblue"; }; + xAccessor = function (d) { + return d.foo; + }; + yAccessor = function (d) { + return d.bar; + }; + y0Accessor = function () { + return 0; + }; + colorAccessor = function (d, i, m) { + return d3.rgb(d.foo, d.bar, i).toString(); + }; + fillAccessor = function () { + return "steelblue"; + }; simpleDataset = new Plottable.DataSource([{ foo: 0, bar: 0 }, { foo: 1, bar: 1 }]); areaPlot = new Plottable.Plot.Area(simpleDataset, xScale, yScale); areaPlot.project("x", xAccessor, xScale).project("y", yAccessor, yScale).project("y0", y0Accessor, yScale).project("fill", fillAccessor).project("stroke", colorAccessor).renderTo(svg); renderArea = areaPlot.renderArea; }); + beforeEach(function () { verifier.start(); }); + it("draws area and line correctly", function () { var areaPath = renderArea.select(".area"); assert.strictEqual(normalizePath(areaPath.attr("d")), "M0,500L500,0L500,500L0,500Z", "area d was set correctly"); assert.strictEqual(areaPath.attr("fill"), "steelblue", "area fill was set correctly"); var areaComputedStyle = window.getComputedStyle(areaPath.node()); assert.strictEqual(areaComputedStyle.stroke, "none", "area stroke renders as \"none\""); + var linePath = renderArea.select(".line"); assert.strictEqual(normalizePath(linePath.attr("d")), "M0,500L500,0", "line d was set correctly"); assert.strictEqual(linePath.attr("stroke"), "#000000", "line stroke was set correctly"); @@ -1533,14 +1939,18 @@ describe("Plots", function () { assert.strictEqual(lineComputedStyle.fill, "none", "line fill renders as \"none\""); verifier.end(); }); + it("area fill works for non-zero floor values appropriately, e.g. half the height of the line", function () { - areaPlot.project("y0", function (d) { return d.bar / 2; }, yScale); + areaPlot.project("y0", function (d) { + return d.bar / 2; + }, yScale); areaPlot.renderTo(svg); renderArea = areaPlot.renderArea; var areaPath = renderArea.select(".area"); assert.equal(normalizePath(areaPath.attr("d")), "M0,500L500,0L500,250L0,500Z"); verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); @@ -1550,7 +1960,9 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("Bar Plot", function () { describe("Vertical Bar Plot in points mode", function () { @@ -1562,6 +1974,7 @@ describe("Plots", function () { var renderer; var SVG_WIDTH = 600; var SVG_HEIGHT = 400; + before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new Plottable.Scale.Ordinal().domain(["A", "B"]).rangeType("points"); @@ -1572,15 +1985,18 @@ describe("Plots", function () { { x: "B", y: 1 } ]; dataset = new Plottable.DataSource(data); + renderer = new Plottable.Plot.VerticalBar(dataset, xScale, yScale); renderer.animate(false); renderer.renderTo(svg); }); + beforeEach(function () { yScale.domain([-2, 2]); renderer.baseline(0); verifier.start(); }); + it("renders correctly", function () { var renderArea = renderer.renderArea; var bars = renderArea.selectAll("rect"); @@ -1595,6 +2011,7 @@ describe("Plots", function () { assert.equal(bar1.attr("x"), "450", "bar1 x is correct"); assert.equal(bar0.attr("y"), "100", "bar0 y is correct"); assert.equal(bar1.attr("y"), "200", "bar1 y is correct"); + var baseline = renderArea.select(".baseline"); assert.equal(baseline.attr("y1"), "200", "the baseline is in the correct vertical position"); assert.equal(baseline.attr("y2"), "200", "the baseline is in the correct vertical position"); @@ -1602,8 +2019,10 @@ describe("Plots", function () { assert.equal(baseline.attr("x2"), SVG_WIDTH, "the baseline ends at the edge of the chart"); verifier.end(); }); + it("baseline value can be changed; renderer updates appropriately", function () { renderer.baseline(-1); + var renderArea = renderer.renderArea; var bars = renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -1612,6 +2031,7 @@ describe("Plots", function () { assert.equal(bar1.attr("height"), "50", "bar1 height is correct"); assert.equal(bar0.attr("y"), "100", "bar0 y is correct"); assert.equal(bar1.attr("y"), "300", "bar1 y is correct"); + var baseline = renderArea.select(".baseline"); assert.equal(baseline.attr("y1"), "300", "the baseline is in the correct vertical position"); assert.equal(baseline.attr("y2"), "300", "the baseline is in the correct vertical position"); @@ -1619,6 +2039,7 @@ describe("Plots", function () { assert.equal(baseline.attr("x2"), SVG_WIDTH, "the baseline ends at the edge of the chart"); verifier.end(); }); + it("bar alignment can be changed; renderer updates appropriately", function () { renderer.barAlignment("center"); var renderArea = renderer.renderArea; @@ -1629,6 +2050,7 @@ describe("Plots", function () { assert.equal(bar1.attr("width"), "10", "bar1 width is correct"); assert.equal(bar0.attr("x"), "145", "bar0 x is correct"); assert.equal(bar1.attr("x"), "445", "bar1 x is correct"); + renderer.barAlignment("right"); renderArea = renderer.renderArea; bars = renderArea.selectAll("rect"); @@ -1638,29 +2060,42 @@ describe("Plots", function () { assert.equal(bar1.attr("width"), "10", "bar1 width is correct"); assert.equal(bar0.attr("x"), "140", "bar0 x is correct"); assert.equal(bar1.attr("x"), "440", "bar1 x is correct"); - assert.throws(function () { return renderer.barAlignment("blargh"); }, Error); + + assert.throws(function () { + return renderer.barAlignment("blargh"); + }, Error); assert.equal(renderer._barAlignmentFactor, 1, "the bad barAlignment didnt break internal state"); verifier.end(); }); + it("can select and deselect bars", function () { var selectedBar = renderer.selectBar(145, 150); + assert.isNotNull(selectedBar, "clicked on a bar"); assert.equal(selectedBar.data()[0], dataset.data()[0], "the data in the bar matches the datasource"); assert.isTrue(selectedBar.classed("selected"), "the bar was classed \"selected\""); + renderer.deselectAll(); assert.isFalse(selectedBar.classed("selected"), "the bar is no longer selected"); - selectedBar = renderer.selectBar(-1, -1); + + selectedBar = renderer.selectBar(-1, -1); // no bars here assert.isNull(selectedBar, "returns null if no bar was selected"); - selectedBar = renderer.selectBar(200, 50); + + selectedBar = renderer.selectBar(200, 50); // between the two bars assert.isNull(selectedBar, "returns null if no bar was selected"); - selectedBar = renderer.selectBar(145, 10); + + selectedBar = renderer.selectBar(145, 10); // above bar 0 assert.isNull(selectedBar, "returns null if no bar was selected"); + + // the bars are now (140,100),(150,300) and (440,300),(450,350) - the + // origin is at the top left! selectedBar = renderer.selectBar({ min: 145, max: 445 }, { min: 150, max: 150 }, true); assert.isNotNull(selectedBar, "line between middle of two bars"); assert.lengthOf(selectedBar.data(), 2, "selected 2 bars (not the negative one)"); assert.equal(selectedBar.data()[0], dataset.data()[0], "the data in bar 0 matches the datasource"); assert.equal(selectedBar.data()[1], dataset.data()[2], "the data in bar 1 matches the datasource"); assert.isTrue(selectedBar.classed("selected"), "the bar was classed \"selected\""); + selectedBar = renderer.selectBar({ min: 145, max: 445 }, { min: 150, max: 350 }, true); assert.isNotNull(selectedBar, "square between middle of two bars, & over the whole area"); assert.lengthOf(selectedBar.data(), 3, "selected all the bars"); @@ -1668,19 +2103,33 @@ describe("Plots", function () { assert.equal(selectedBar.data()[1], dataset.data()[1], "the data in bar 1 matches the datasource"); assert.equal(selectedBar.data()[2], dataset.data()[2], "the data in bar 2 matches the datasource"); assert.isTrue(selectedBar.classed("selected"), "the bar was classed \"selected\""); - assert.throws(function () { return renderer.selectBar("blargh", 150); }, Error); - assert.throws(function () { return renderer.selectBar({ min: 150 }, 150); }, Error); + + // the runtime parameter validation should be strict, so no strings or + // mangled objects + assert.throws(function () { + return renderer.selectBar("blargh", 150); + }, Error); + assert.throws(function () { + return renderer.selectBar({ min: 150 }, 150); + }, Error); + verifier.end(); }); + it("shouldn't blow up if members called before the first render", function () { var brandNew = new Plottable.Plot.VerticalBar(dataset, xScale, yScale); + assert.isNotNull(brandNew.deselectAll(), "deselects return self"); assert.isNull(brandNew.selectBar(0, 0), "selects return empty"); - brandNew._anchor(d3.select(document.createElement("svg"))); + + brandNew._anchor(d3.select(document.createElement("svg"))); // calls `_setup()` + assert.isNotNull(brandNew.deselectAll(), "deselects return self after setup"); assert.isNull(brandNew.selectBar(0, 0), "selects return empty after setup"); + verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); @@ -1688,6 +2137,7 @@ describe("Plots", function () { ; }); }); + describe("Horizontal Bar Plot in Points Mode", function () { var verifier = new MultiTestVerifier(); var svg; @@ -1701,21 +2151,25 @@ describe("Plots", function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); yScale = new Plottable.Scale.Ordinal().domain(["A", "B"]).rangeType("points"); xScale = new Plottable.Scale.Linear(); + var data = [ { y: "A", x: 1 }, { y: "B", x: -1.5 }, { y: "B", x: 1 } ]; dataset = new Plottable.DataSource(data); + renderer = new Plottable.Plot.HorizontalBar(dataset, xScale, yScale); renderer.animate(false); renderer.renderTo(svg); }); + beforeEach(function () { xScale.domain([-3, 3]); renderer.baseline(0); verifier.start(); }); + it("renders correctly", function () { var renderArea = renderer.renderArea; var bars = renderArea.selectAll("rect"); @@ -1730,6 +2184,7 @@ describe("Plots", function () { assert.equal(bar1.attr("y"), "100", "bar1 y is correct"); assert.equal(bar0.attr("x"), "300", "bar0 x is correct"); assert.equal(bar1.attr("x"), "150", "bar1 x is correct"); + var baseline = renderArea.select(".baseline"); assert.equal(baseline.attr("x1"), "300", "the baseline is in the correct horizontal position"); assert.equal(baseline.attr("x2"), "300", "the baseline is in the correct horizontal position"); @@ -1737,8 +2192,10 @@ describe("Plots", function () { assert.equal(baseline.attr("y2"), SVG_HEIGHT, "the baseline ends at the bottom of the chart"); verifier.end(); }); + it("baseline value can be changed; renderer updates appropriately", function () { renderer.baseline(-1); + var renderArea = renderer.renderArea; var bars = renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -1747,6 +2204,7 @@ describe("Plots", function () { assert.equal(bar1.attr("width"), "50", "bar1 width is correct"); assert.equal(bar0.attr("x"), "200", "bar0 x is correct"); assert.equal(bar1.attr("x"), "150", "bar1 x is correct"); + var baseline = renderArea.select(".baseline"); assert.equal(baseline.attr("x1"), "200", "the baseline is in the correct horizontal position"); assert.equal(baseline.attr("x2"), "200", "the baseline is in the correct horizontal position"); @@ -1754,6 +2212,7 @@ describe("Plots", function () { assert.equal(baseline.attr("y2"), SVG_HEIGHT, "the baseline ends at the bottom of the chart"); verifier.end(); }); + it("bar alignment can be changed; renderer updates appropriately", function () { renderer.barAlignment("center"); var renderArea = renderer.renderArea; @@ -1764,6 +2223,7 @@ describe("Plots", function () { assert.equal(bar1.attr("height"), "10", "bar1 height is correct"); assert.equal(bar0.attr("y"), "295", "bar0 y is correct"); assert.equal(bar1.attr("y"), "95", "bar1 y is correct"); + renderer.barAlignment("bottom"); renderArea = renderer.renderArea; bars = renderArea.selectAll("rect"); @@ -1773,9 +2233,14 @@ describe("Plots", function () { assert.equal(bar1.attr("height"), "10", "bar1 height is correct"); assert.equal(bar0.attr("y"), "290", "bar0 y is correct"); assert.equal(bar1.attr("y"), "90", "bar1 y is correct"); - assert.throws(function () { return renderer.barAlignment("blargh"); }, Error); + + assert.throws(function () { + return renderer.barAlignment("blargh"); + }, Error); + verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); @@ -1783,6 +2248,7 @@ describe("Plots", function () { ; }); }); + describe("Horizontal Bar Plot in Bands mode", function () { var verifier = new MultiTestVerifier(); var svg; @@ -1794,16 +2260,22 @@ describe("Plots", function () { var SVG_HEIGHT = 400; var axisWidth = 0; var bandWidth = 0; - var numAttr = function (s, a) { return parseFloat(s.attr(a)); }; + + var numAttr = function (s, a) { + return parseFloat(s.attr(a)); + }; + before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); yScale = new Plottable.Scale.Ordinal().domain(["A", "B"]); xScale = new Plottable.Scale.Linear(); + var data = [ { y: "A", x: 1 }, - { y: "B", x: 2 }, + { y: "B", x: 2 } ]; dataset = new Plottable.DataSource(data); + renderer = new Plottable.Plot.HorizontalBar(dataset, xScale, yScale); renderer.baseline(0); renderer.animate(false); @@ -1822,6 +2294,7 @@ describe("Plots", function () { } ; }); + it("renders correctly", function () { var bars = renderer.renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -1832,10 +2305,13 @@ describe("Plots", function () { assert.closeTo(numAttr(bar1, "height"), 104, 2); assert.closeTo(numAttr(bar0, "width"), (600 - axisWidth) / 2, 0.01, "width is correct for bar0"); assert.closeTo(numAttr(bar1, "width"), 600 - axisWidth, 0.01, "width is correct for bar1"); + + // check that bar is aligned on the center of the scale assert.closeTo(numAttr(bar0, "y") + numAttr(bar0, "height") / 2, yScale.scale(bar0y) + bandWidth / 2, 0.01, "y pos correct for bar0"); assert.closeTo(numAttr(bar1, "y") + numAttr(bar1, "height") / 2, yScale.scale(bar1y) + bandWidth / 2, 0.01, "y pos correct for bar1"); verifier.end(); }); + it("width projector may be overwritten, and calling project queues rerender", function () { var bars = renderer.renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -1855,7 +2331,9 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("GridPlot", function () { var SVG_WIDTH = 400; @@ -1864,35 +2342,42 @@ describe("Plots", function () { { x: "A", y: "U", magnitude: 0 }, { x: "B", y: "U", magnitude: 2 }, { x: "A", y: "V", magnitude: 16 }, - { x: "B", y: "V", magnitude: 8 }, + { x: "B", y: "V", magnitude: 8 } ]; + var VERIFY_CELLS = function (cells) { 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"), "100", "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"), "100", "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"), "0", "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"), "0", "cell 'BV' x coord is correct"); assert.equal(cellBV.attr("fill"), "#777777", "cell 'BV' color is correct"); }; + it("renders correctly", function () { var xScale = new Plottable.Scale.Ordinal(); var yScale = new Plottable.Scale.Ordinal(); @@ -1903,6 +2388,7 @@ describe("Plots", function () { VERIFY_CELLS(renderer.renderArea.selectAll("rect")[0]); svg.remove(); }); + it("renders correctly when data is set after construction", function () { var xScale = new Plottable.Scale.Ordinal(); var yScale = new Plottable.Scale.Ordinal(); @@ -1914,6 +2400,7 @@ describe("Plots", function () { VERIFY_CELLS(renderer.renderArea.selectAll("rect")[0]); svg.remove(); }); + it("can invert y axis correctly", function () { var xScale = new Plottable.Scale.Ordinal(); var yScale = new Plottable.Scale.Ordinal(); @@ -1921,17 +2408,21 @@ describe("Plots", function () { var svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); var renderer = new Plottable.Plot.Grid(null, xScale, yScale, colorScale).project("fill", "magnitude"); renderer.renderTo(svg); + yScale.domain(["U", "V"]); renderer.dataSource().data(DATA); + var cells = renderer.renderArea.selectAll("rect")[0]; var cellAU = d3.select(cells[0]); var cellAV = d3.select(cells[2]); cellAU.attr("fill", "#000000"); cellAU.attr("x", "0"); cellAU.attr("y", "100"); + cellAV.attr("fill", "#ffffff"); cellAV.attr("x", "0"); cellAV.attr("y", "0"); + yScale.domain(["V", "U"]); cells = renderer.renderArea.selectAll("rect")[0]; cellAU = d3.select(cells[0]); @@ -1939,15 +2430,19 @@ describe("Plots", function () { cellAU.attr("fill", "#000000"); cellAU.attr("x", "0"); cellAU.attr("y", "0"); + cellAV.attr("fill", "#ffffff"); cellAV.attr("x", "0"); cellAV.attr("y", "100"); + svg.remove(); }); }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("ScatterPlot", function () { it("the accessors properly access data, index, and metadata", function () { @@ -1958,8 +2453,12 @@ describe("Plots", function () { 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) { return d.x + i * m.foo; }; - var yAccessor = function (d, i, m) { return m.bar; }; + var xAccessor = function (d, i, m) { + return d.x + i * m.foo; + }; + var yAccessor = function (d, i, m) { + return m.bar; + }; var dataSource = new Plottable.DataSource(data, metadata); var renderer = new Plottable.Plot.Scatter(dataSource, xScale, yScale).project("x", xAccessor).project("y", yAccessor); renderer.renderTo(svg); @@ -1970,20 +2469,24 @@ describe("Plots", function () { assert.closeTo(parseFloat(c1.attr("cy")), 20, 0.01, "first circle cy is correct"); assert.closeTo(parseFloat(c2.attr("cx")), 11, 0.01, "second circle cx is correct"); assert.closeTo(parseFloat(c2.attr("cy")), 20, 0.01, "second circle cy is correct"); + data = [{ x: 2, y: 2 }, { x: 4, y: 4 }]; 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 }; 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"); assert.closeTo(parseFloat(c2.attr("cy")), 0, 0.01, "second circle cy is correct after metadata change"); + svg.remove(); }); + describe("Example ScatterPlot with quadratic series", function () { var svg; var xScale; @@ -1996,16 +2499,23 @@ describe("Plots", function () { var pixelAreaPart = { xMin: 200, xMax: 600, yMin: 100, yMax: 200 }; var dataAreaFull = { xMin: 0, xMax: 9, yMin: 81, yMax: 0 }; var dataAreaPart = { xMin: 3, xMax: 9, yMin: 54, yMax: 27 }; - var colorAccessor = function (d, i, m) { return d3.rgb(d.x, d.y, i).toString(); }; + var colorAccessor = function (d, i, m) { + return d3.rgb(d.x, d.y, i).toString(); + }; var circlesInArea; var quadraticDataset = makeQuadraticSeries(10); + function getCirclePlotVerifier() { + // creates a function that verifies that circles are drawn properly after accounting for svg transform + // and then modifies circlesInArea to contain the number of circles that were discovered in the plot area circlesInArea = 0; var renderArea = circlePlot.renderArea; var renderAreaTransform = d3.transform(renderArea.attr("transform")); var translate = renderAreaTransform.translate; var scale = renderAreaTransform.scale; return function (datum, index) { + // This function takes special care to compute the position of circles after taking svg transformation + // into account. var selection = d3.select(this); var elementTransform = d3.transform(selection.attr("transform")); var elementTranslate = elementTransform.translate; @@ -2021,9 +2531,11 @@ describe("Plots", function () { }; } ; + beforeEach(function () { verifier.start(); }); + before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new Plottable.Scale.Linear().domain([0, 9]); @@ -2032,6 +2544,7 @@ describe("Plots", function () { circlePlot.project("fill", colorAccessor); circlePlot.renderTo(svg); }); + it("setup is handled properly", function () { 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"); @@ -2039,6 +2552,7 @@ describe("Plots", function () { assert.equal(circlesInArea, 10, "10 circles were drawn"); verifier.end(); }); + it("rendering is idempotent", function () { circlePlot._render(); circlePlot._render(); @@ -2046,6 +2560,7 @@ describe("Plots", function () { assert.equal(circlesInArea, 10, "10 circles were drawn"); verifier.end(); }); + describe("after the scale has changed", function () { before(function () { xScale.domain([0, 3]); @@ -2053,6 +2568,7 @@ describe("Plots", function () { dataAreaFull = { xMin: 0, xMax: 3, yMin: 9, yMax: 0 }; dataAreaPart = { xMin: 1, xMax: 3, yMin: 6, yMax: 3 }; }); + it("the circles re-rendered properly", function () { var renderArea = circlePlot.renderArea; var circles = renderArea.selectAll("circle"); @@ -2061,6 +2577,7 @@ describe("Plots", function () { verifier.end(); }); }); + after(function () { if (verifier.passed) { svg.remove(); @@ -2071,7 +2588,9 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("Stacked Bar Plot", function () { var verifier = new MultiTestVerifier(); @@ -2085,11 +2604,16 @@ describe("Plots", function () { var SVG_HEIGHT = 400; var axisHeight = 0; var bandWidth = 0; - var numAttr = function (s, a) { return parseFloat(s.attr(a)); }; + + var numAttr = function (s, a) { + return parseFloat(s.attr(a)); + }; + before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new Plottable.Scale.Ordinal(); yScale = new Plottable.Scale.Linear().domain([0, 3]); + var data1 = [ { x: "A", y: 1 }, { x: "B", y: 2 } @@ -2100,6 +2624,7 @@ describe("Plots", function () { ]; dataset1 = new Plottable.DataSource(data1); dataset2 = new Plottable.DataSource(data2); + renderer = new Plottable.Plot.StackedBar(xScale, yScale); renderer.addDataset(data1); renderer.addDataset(data2); @@ -2109,18 +2634,22 @@ describe("Plots", function () { axisHeight = xAxis.availableHeight; bandWidth = xScale.rangeBand(); }); + beforeEach(function () { verifier.start(); }); + afterEach(function () { verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); } ; }); + it("renders correctly", function () { var bars = renderer.renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -2131,18 +2660,26 @@ describe("Plots", function () { var bar1X = bar1.data()[0].x; var bar2X = bar2.data()[0].x; var bar3X = bar3.data()[0].x; + + // check widths assert.closeTo(numAttr(bar0, "width"), bandWidth, 2); assert.closeTo(numAttr(bar1, "width"), bandWidth, 2); assert.closeTo(numAttr(bar2, "width"), bandWidth, 2); assert.closeTo(numAttr(bar3, "width"), bandWidth, 2); + + // check heights assert.closeTo(numAttr(bar0, "height"), (400 - axisHeight) / 3, 0.01, "height is correct for bar0"); assert.closeTo(numAttr(bar1, "height"), (400 - axisHeight) / 3 * 2, 0.01, "height is correct for bar1"); assert.closeTo(numAttr(bar2, "height"), (400 - axisHeight) / 3 * 2, 0.01, "height is correct for bar2"); assert.closeTo(numAttr(bar3, "height"), (400 - axisHeight) / 3, 0.01, "height is correct for bar3"); + + // check that bar is aligned on the center of the scale assert.closeTo(numAttr(bar0, "x") + numAttr(bar0, "width") / 2, xScale.scale(bar0X) + bandWidth / 2, 0.01, "x pos correct for bar0"); assert.closeTo(numAttr(bar1, "x") + numAttr(bar1, "width") / 2, xScale.scale(bar1X) + bandWidth / 2, 0.01, "x pos correct for bar1"); assert.closeTo(numAttr(bar2, "x") + numAttr(bar2, "width") / 2, xScale.scale(bar2X) + bandWidth / 2, 0.01, "x pos correct for bar2"); assert.closeTo(numAttr(bar3, "x") + numAttr(bar3, "width") / 2, xScale.scale(bar3X) + bandWidth / 2, 0.01, "x pos correct for bar3"); + + // now check y values to ensure they do indeed stack assert.closeTo(numAttr(bar0, "y"), (400 - axisHeight) / 3 * 2, 0.01, "y is correct for bar0"); assert.closeTo(numAttr(bar1, "y"), (400 - axisHeight) / 3, 0.01, "y is correct for bar1"); assert.closeTo(numAttr(bar2, "y"), 0, 0.01, "y is correct for bar2"); @@ -2151,7 +2688,9 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Plots", function () { describe("Clustered Bar Plot", function () { var verifier = new MultiTestVerifier(); @@ -2165,11 +2704,16 @@ describe("Plots", function () { var SVG_HEIGHT = 400; var axisHeight = 0; var bandWidth = 0; - var numAttr = function (s, a) { return parseFloat(s.attr(a)); }; + + var numAttr = function (s, a) { + return parseFloat(s.attr(a)); + }; + before(function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); xScale = new Plottable.Scale.Ordinal(); yScale = new Plottable.Scale.Linear().domain([0, 2]); + var data1 = [ { x: "A", y: 1 }, { x: "B", y: 2 } @@ -2180,6 +2724,7 @@ describe("Plots", function () { ]; dataset1 = new Plottable.DataSource(data1); dataset2 = new Plottable.DataSource(data2); + renderer = new Plottable.Plot.ClusteredBar(xScale, yScale); renderer.addDataset(data1); renderer.addDataset(data2); @@ -2189,18 +2734,22 @@ describe("Plots", function () { axisHeight = xAxis.availableHeight; bandWidth = xScale.rangeBand(); }); + beforeEach(function () { verifier.start(); }); + afterEach(function () { verifier.end(); }); + after(function () { if (verifier.passed) { svg.remove(); } ; }); + it("renders correctly", function () { var bars = renderer.renderArea.selectAll("rect"); var bar0 = d3.select(bars[0][0]); @@ -2211,15 +2760,21 @@ describe("Plots", function () { var bar1X = bar1.data()[0].x; var bar2X = bar2.data()[0].x; var bar3X = bar3.data()[0].x; + + // check widths var width = bandWidth / 2 * .518; assert.closeTo(numAttr(bar0, "width"), width, 2); assert.closeTo(numAttr(bar1, "width"), width, 2); assert.closeTo(numAttr(bar2, "width"), width, 2); assert.closeTo(numAttr(bar3, "width"), width, 2); + + // check heights assert.closeTo(numAttr(bar0, "height"), (400 - axisHeight) / 2, 0.01, "height is correct for bar0"); assert.closeTo(numAttr(bar1, "height"), (400 - axisHeight), 0.01, "height is correct for bar1"); assert.closeTo(numAttr(bar2, "height"), (400 - axisHeight), 0.01, "height is correct for bar2"); assert.closeTo(numAttr(bar3, "height"), (400 - axisHeight) / 2, 0.01, "height is correct for bar3"); + + // check that clustering is correct var off = renderer.innerScale.scale("_0"); assert.closeTo(numAttr(bar0, "x") + numAttr(bar0, "width") / 2, xScale.scale(bar0X) + bandWidth / 2 - off, 0.01, "x pos correct for bar0"); assert.closeTo(numAttr(bar1, "x") + numAttr(bar1, "width") / 2, xScale.scale(bar1X) + bandWidth / 2 - off, 0.01, "x pos correct for bar1"); @@ -2229,12 +2784,15 @@ describe("Plots", function () { }); }); +/// var assert = chai.assert; + describe("Broadcasters", function () { var b; var called; var cb; var listenable = { broadcaster: null }; + beforeEach(function () { b = new Plottable.Core.Broadcaster(listenable); listenable.broadcaster = b; @@ -2248,6 +2806,7 @@ describe("Broadcasters", function () { 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 () { @@ -2260,23 +2819,27 @@ describe("Broadcasters", function () { 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 not called after deregistering only listener"); + b.registerListener(5, cb); b.registerListener(6, cb); b.deregisterAllListeners(); b.broadcast(); assert.isFalse(called, "callback was not called after deregistering all listeners"); + b.registerListener(5, cb); b.registerListener(6, cb); b.deregisterListener(5); b.broadcast(); assert.isTrue(called, "callback was called even after 1/2 listeners were deregistered"); }); + it("arguments are passed through to callback", function () { var g2 = {}; var g3 = "foo"; @@ -2290,40 +2853,54 @@ describe("Broadcasters", function () { b.broadcast(g2, g3); assert.isTrue(called, "the cb was called"); }); + it("deregistering an unregistered listener doesn't throw an error", function () { - assert.doesNotThrow(function () { return b.deregisterListener({}); }); + assert.doesNotThrow(function () { + return b.deregisterListener({}); + }); }); }); +/// var assert = chai.assert; + describe("ComponentContainer", function () { it("_addComponent()", function () { var container = new Plottable.Abstract.ComponentContainer(); var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); var c3 = new Plottable.Abstract.Component(); + assert.isTrue(container._addComponent(c1), "returns true on successful adding"); assert.deepEqual(container.components(), [c1], "component was added"); + container._addComponent(c2); assert.deepEqual(container.components(), [c1, c2], "can append components"); + container._addComponent(c3, true); assert.deepEqual(container.components(), [c3, c1, c2], "can prepend components"); + assert.isFalse(container._addComponent(null), "returns false for null arguments"); assert.deepEqual(container.components(), [c3, c1, c2], "component list was unchanged"); + assert.isFalse(container._addComponent(c1), "returns false if adding an already-added component"); assert.deepEqual(container.components(), [c3, c1, c2], "component list was unchanged"); }); + it("_removeComponent()", function () { var container = new Plottable.Abstract.ComponentContainer(); var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); container._addComponent(c1); container._addComponent(c2); + container._removeComponent(c2); assert.deepEqual(container.components(), [c1], "component 2 was removed"); + container._removeComponent(c2); assert.deepEqual(container.components(), [c1], "there are no side effects from removing already-removed components"); }); + it("empty()", function () { var container = new Plottable.Abstract.ComponentContainer(); assert.isTrue(container.empty()); @@ -2331,6 +2908,7 @@ describe("ComponentContainer", function () { container._addComponent(c1); assert.isFalse(container.empty()); }); + it("detachAll()", function () { var container = new Plottable.Abstract.ComponentContainer(); var c1 = new Plottable.Abstract.Component(); @@ -2338,26 +2916,32 @@ describe("ComponentContainer", function () { container._addComponent(c1); container._addComponent(c2); container.detachAll(); + assert.deepEqual(container.components(), [], "container was cleared of components"); }); + it("components() returns a shallow copy", function () { var container = new Plottable.Abstract.ComponentContainer(); var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); container._addComponent(c1); container._addComponent(c2); + var componentList = container.components(); componentList.pop(); assert.deepEqual(container.components(), [c1, c2], "internal list of components was not changed"); }); }); +/// var assert = chai.assert; + describe("ComponentGroups", function () { it("components in componentGroups overlap", function () { var c1 = makeFixedSizeComponent(10, 10); var c2 = new Plottable.Abstract.Component(); var c3 = new Plottable.Abstract.Component(); + var cg = new Plottable.Component.Group([c1, c2, c3]); var svg = generateSVG(400, 400); cg._anchor(svg); @@ -2373,10 +2957,12 @@ describe("ComponentGroups", function () { assertWidthHeight(t3, 400, 400, "rect3 sized correctly"); svg.remove(); }); + it("components can be added before and after anchoring", function () { var c1 = makeFixedSizeComponent(10, 10); var c2 = makeFixedSizeComponent(20, 20); var c3 = new Plottable.Abstract.Component(); + var cg = new Plottable.Component.Group([c1]); var svg = generateSVG(400, 400); cg.merge(c2)._anchor(svg); @@ -2394,28 +2980,35 @@ describe("ComponentGroups", function () { assertWidthHeight(t3, 400, 400, "rect3 sized correctly"); svg.remove(); }); + it("component fixity is computed appropriately", function () { var cg = new Plottable.Component.Group(); var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); + cg.merge(c1).merge(c2); assert.isFalse(cg._isFixedHeight(), "height not fixed when both components unfixed"); assert.isFalse(cg._isFixedWidth(), "width not fixed when both components unfixed"); + fixComponentSize(c1, 10, 10); assert.isFalse(cg._isFixedHeight(), "height not fixed when one component unfixed"); assert.isFalse(cg._isFixedWidth(), "width not fixed when one component unfixed"); + fixComponentSize(c2, null, 10); assert.isTrue(cg._isFixedHeight(), "height fixed when both components fixed"); assert.isFalse(cg._isFixedWidth(), "width unfixed when one component unfixed"); }); + it("componentGroup subcomponents have xOffset, yOffset of 0", function () { var cg = new Plottable.Component.Group(); var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); cg.merge(c1).merge(c2); + var svg = generateSVG(); cg._anchor(svg); cg._computeLayout(50, 50, 350, 350); + var cgTranslate = d3.transform(cg.element.attr("transform")).translate; var c1Translate = d3.transform(c1.element.attr("transform")).translate; var c2Translate = d3.transform(c2.element.attr("transform")).translate; @@ -2427,33 +3020,46 @@ describe("ComponentGroups", function () { assert.equal(c2Translate[1], 0, "componentGroup has 0 yOffset"); svg.remove(); }); + it("detach() and _removeComponent work correctly for componentGroup", function () { var c1 = new Plottable.Abstract.Component().classed("component-1", true); var c2 = new Plottable.Abstract.Component().classed("component-2", true); var cg = new Plottable.Component.Group([c1, c2]); + var svg = generateSVG(200, 200); cg.renderTo(svg); + var c1Node = svg.select(".component-1").node(); var c2Node = svg.select(".component-2").node(); + assert.isNotNull(c1Node, "component 1 was added to the DOM"); assert.isNotNull(c2Node, "component 2 was added to the DOM"); + c2.detach(); + c1Node = svg.select(".component-1").node(); c2Node = svg.select(".comopnent-2").node(); + assert.isNotNull(c1Node, "component 1 is still in the DOM"); assert.isNull(c2Node, "component 2 was removed from the DOM"); + cg.detach(); var cgNode = svg.select(".component-group").node(); c1Node = svg.select(".component-1").node(); + assert.isNull(cgNode, "component group was removed from the DOM"); assert.isNull(c1Node, "componet 1 was also removed from the DOM"); + cg.renderTo(svg); cgNode = svg.select(".component-group").node(); c1Node = svg.select(".component-1").node(); + assert.isNotNull(cgNode, "component group was added back to the DOM"); assert.isNotNull(c1Node, "componet 1 was also added back to the DOM"); + svg.remove(); }); + it("detachAll() works as expected", function () { var cg = new Plottable.Component.Group(); var c1 = new Plottable.Abstract.Component(); @@ -2469,12 +3075,14 @@ describe("ComponentGroups", function () { assert.isFalse(c3._isAnchored, "c3 was detached"); assert.lengthOf(cg.components(), 0, "cg has no components"); }); + describe("ComponentGroup._requestedSpace works as expected", function () { it("_works for an empty ComponentGroup", function () { var cg = new Plottable.Component.Group(); var request = cg._requestedSpace(10, 10); verifySpaceRequest(request, 0, 0, false, false, ""); }); + it("works for a ComponentGroup with only proportional-size components", function () { var cg = new Plottable.Component.Group(); var c1 = new Plottable.Abstract.Component(); @@ -2483,6 +3091,7 @@ describe("ComponentGroups", function () { var request = cg._requestedSpace(10, 10); verifySpaceRequest(request, 0, 0, false, false, ""); }); + it("works when there are fixed-size components", function () { var cg = new Plottable.Component.Group(); var c1 = new Plottable.Abstract.Component(); @@ -2495,11 +3104,13 @@ describe("ComponentGroups", function () { verifySpaceRequest(request, 0, 50, false, true, ""); }); }); + describe("Component.merge works as expected", function () { var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); var c3 = new Plottable.Abstract.Component(); var c4 = new Plottable.Abstract.Component(); + it("Component.merge works as expected (Component.merge Component)", function () { var cg = c1.merge(c2); var innerComponents = cg._components; @@ -2507,6 +3118,7 @@ describe("ComponentGroups", function () { assert.equal(innerComponents[0], c1, "first component correct"); assert.equal(innerComponents[1], c2, "second component correct"); }); + it("Component.merge works as expected (Component.merge ComponentGroup)", function () { var cg = new Plottable.Component.Group([c2, c3, c4]); var cg2 = c1.merge(cg); @@ -2516,6 +3128,7 @@ describe("ComponentGroups", function () { assert.equal(components[0], c1, "first component in front"); assert.equal(components[1], c2, "second component is second"); }); + it("Component.merge works as expected (ComponentGroup.merge Component)", function () { var cg = new Plottable.Component.Group([c1, c2, c3]); var cg2 = cg.merge(c4); @@ -2525,6 +3138,7 @@ describe("ComponentGroups", function () { assert.equal(components[0], c1, "first is first"); assert.equal(components[3], c4, "fourth is fourth"); }); + it("Component.merge works as expected (ComponentGroup.merge ComponentGroup)", function () { var cg1 = new Plottable.Component.Group([c1, c2]); var cg2 = new Plottable.Component.Group([c3, c4]); @@ -2540,14 +3154,18 @@ describe("ComponentGroups", function () { }); }); +/// var assert = chai.assert; + function assertComponentXY(component, x, y, message) { + // use to examine the private variables var translate = d3.transform(component.element.attr("transform")).translate; var xActual = translate[0]; var yActual = translate[1]; assert.equal(xActual, x, "X: " + message); assert.equal(yActual, y, "Y: " + message); } + describe("Component behavior", function () { var svg; var c; @@ -2557,6 +3175,7 @@ describe("Component behavior", function () { svg = generateSVG(SVG_WIDTH, SVG_HEIGHT); c = new Plottable.Abstract.Component(); }); + describe("anchor", function () { it("anchoring works as expected", function () { c._anchor(svg); @@ -2564,16 +3183,20 @@ describe("Component behavior", function () { assert.isTrue(svg.classed("plottable"), " was given \"plottable\" CSS class"); svg.remove(); }); + it("can re-anchor to a different element", function () { c._anchor(svg); + var svg2 = generateSVG(SVG_WIDTH, SVG_HEIGHT); c._anchor(svg2); assert.equal(c.element.node(), svg2.select("g").node(), "the component re-achored under the second "); assert.isTrue(svg2.classed("plottable"), "second was given \"plottable\" CSS class"); + svg.remove(); svg2.remove(); }); }); + describe("computeLayout", function () { it("computeLayout defaults and updates intelligently", function () { c._anchor(svg); @@ -2582,18 +3205,24 @@ describe("Component behavior", function () { assert.equal(c.availableHeight, SVG_HEIGHT, "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.attr("width", 2 * SVG_WIDTH).attr("height", 2 * SVG_HEIGHT); c._computeLayout(); assert.equal(c.availableWidth, 2 * SVG_WIDTH, "computeLayout updated width to new svg width"); assert.equal(c.availableHeight, 2 * SVG_HEIGHT, "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 works with CSS layouts", function () { + // Manually size parent var parent = d3.select(svg.node().parentNode); parent.style("width", "400px"); parent.style("height", "200px"); + + // Remove width/height attributes and style with CSS svg.attr("width", null).attr("height", null); c._anchor(svg); c._computeLayout(); @@ -2601,32 +3230,46 @@ describe("Component behavior", function () { assert.equal(c.availableHeight, 200, "defaults to height of parent if width is not specified on "); assert.equal(c.xOrigin, 0, "xOrigin defaulted to 0"); assert.equal(c.yOrigin, 0, "yOrigin defaulted to 0"); + svg.style("width", "50%").style("height", "50%"); c._computeLayout(); + assert.equal(c.availableWidth, 200, "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, 100, "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"); + + // reset test page DOM parent.style("width", "auto"); parent.style("height", "auto"); svg.remove(); }); + it("computeLayout will not default when attached to non-root node", function () { var g = svg.append("g"); c._anchor(g); - assert.throws(function () { return c._computeLayout(); }, "null arguments"); + assert.throws(function () { + return c._computeLayout(); + }, "null arguments"); svg.remove(); }); + it("computeLayout throws an error when called on un-anchored component", function () { - assert.throws(function () { return c._computeLayout(); }, Error, "anchor must be called before computeLayout"); + assert.throws(function () { + return c._computeLayout(); + }, Error, "anchor must be called before computeLayout"); svg.remove(); }); + it("computeLayout uses its arguments apropriately", function () { var g = svg.append("g"); var xOff = 10; @@ -2642,6 +3285,7 @@ describe("Component behavior", function () { svg.remove(); }); }); + it("subelement containers are ordered properly", function () { c.renderTo(svg); var gs = c.element.selectAll("g"); @@ -2655,39 +3299,49 @@ describe("Component behavior", function () { assert.isTrue(g3.classed("box-container"), "the fourth g is a box container"); svg.remove(); }); + it("fixed-width component will align to the right spot", function () { fixComponentSize(c, 100, 100); c._anchor(svg); c._computeLayout(); assertComponentXY(c, 0, 0, "top-left component aligns correctly"); + c.xAlign("CENTER").yAlign("CENTER"); c._computeLayout(); assertComponentXY(c, 150, 100, "center component aligns correctly"); + c.xAlign("RIGHT").yAlign("BOTTOM"); c._computeLayout(); assertComponentXY(c, 300, 200, "bottom-right component aligns correctly"); svg.remove(); }); + it("components can be offset relative to their alignment, and throw errors if there is insufficient space", function () { fixComponentSize(c, 100, 100); c._anchor(svg); c.xOffset(20).yOffset(20); c._computeLayout(); assertComponentXY(c, 20, 20, "top-left component offsets correctly"); + c.xAlign("CENTER").yAlign("CENTER"); c._computeLayout(); assertComponentXY(c, 170, 120, "center component offsets correctly"); + c.xAlign("RIGHT").yAlign("BOTTOM"); c._computeLayout(); assertComponentXY(c, 320, 220, "bottom-right component offsets correctly"); + c.xOffset(0).yOffset(0); c._computeLayout(); assertComponentXY(c, 300, 200, "bottom-right component offset resets"); + c.xOffset(-20).yOffset(-30); c._computeLayout(); assertComponentXY(c, 280, 170, "negative offsets work properly"); + svg.remove(); }); + it("component defaults are as expected", function () { var layout = c._requestedSpace(1, 1); assert.equal(layout.width, 0, "requested width defaults to 0"); @@ -2700,6 +3354,7 @@ describe("Component behavior", function () { assert.equal(c._yOffset, 0, "yOffset defaults to 0"); svg.remove(); }); + it("clipPath works as expected", function () { assert.isFalse(c.clipPathEnabled, "clipPathEnabled defaults to false"); c.clipPathEnabled = true; @@ -2708,13 +3363,18 @@ describe("Component behavior", function () { c._computeLayout(0, 0, 100, 100); c._render(); var expectedClipPathURL = "url(#clipPath" + expectedClipPathID + ")"; - var normalizeClipPath = function (s) { return s.replace(/"/g, ""); }; + + // IE 9 has clipPath like 'url("#clipPath")', must accomodate + var normalizeClipPath = function (s) { + return s.replace(/"/g, ""); + }; assert.isTrue(normalizeClipPath(c.element.attr("clip-path")) === expectedClipPathURL, "the element has clip-path url attached"); var clipRect = c.boxContainer.select(".clip-rect"); assert.equal(clipRect.attr("width"), 100, "the clipRect has an appropriate width"); assert.equal(clipRect.attr("height"), 100, "the clipRect has an appropriate height"); svg.remove(); }); + it("componentID works as expected", function () { var expectedID = Plottable.Abstract.PlottableObject.nextID; var c1 = new Plottable.Abstract.Component(); @@ -2723,13 +3383,17 @@ describe("Component behavior", function () { 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"); }, Error, "Adding boxes before anchoring is currently disallowed"); + assert.throws(function () { + return c.addBox("pre-anchor"); + }, Error, "Adding boxes before anchoring is currently disallowed"); c.renderTo(svg); c.addBox("post-anchor"); var e = c.element; var boxContainer = e.select(".box-container"); var boxStrings = [".bounding-box", ".post-anchor"]; + boxStrings.forEach(function (s) { var box = boxContainer.select(s); assert.isNotNull(box.node(), s + " box was created and placed inside boxContainer"); @@ -2739,6 +3403,7 @@ describe("Component behavior", function () { }); svg.remove(); }); + it("hitboxes are created iff there are registered interactions", function () { function verifyHitbox(component) { var hitBox = component.hitBox; @@ -2748,27 +3413,35 @@ describe("Component behavior", function () { assert.isTrue(hitBoxFilled, hitBoxFill + " <- this should be filled, so the hitbox will detect events"); assert.equal(hitBox.style("opacity"), "0", "the hitBox is transparent, otherwise it would look weird"); } + c._anchor(svg); assert.isUndefined(c.hitBox, "no hitBox was created when there were no registered interactions"); svg.remove(); svg = generateSVG(); + c = new Plottable.Abstract.Component(); var i = new Plottable.Abstract.Interaction(c).registerWithComponent(); c._anchor(svg); verifyHitbox(c); svg.remove(); svg = generateSVG(); + c = new Plottable.Abstract.Component(); c._anchor(svg); i = new Plottable.Abstract.Interaction(c).registerWithComponent(); verifyHitbox(c); svg.remove(); }); + it("interaction registration works properly", function () { var hitBox1 = null; var hitBox2 = null; - var interaction1 = { _anchor: function (hb) { return hitBox1 = hb.node(); } }; - var interaction2 = { _anchor: function (hb) { return hitBox2 = hb.node(); } }; + var interaction1 = { _anchor: function (hb) { + return hitBox1 = hb.node(); + } }; + var interaction2 = { _anchor: function (hb) { + return hitBox2 = hb.node(); + } }; c.registerInteraction(interaction1); c.renderTo(svg); c.registerInteraction(interaction2); @@ -2777,11 +3450,17 @@ describe("Component behavior", function () { assert.equal(hitBox2, hitNode, "hitBox2 was registerd"); svg.remove(); }); + it("errors are thrown on bad alignments", function () { - assert.throws(function () { return c.xAlign("foo"); }, Error, "Unsupported alignment"); - assert.throws(function () { return c.yAlign("foo"); }, Error, "Unsupported alignment"); + assert.throws(function () { + return c.xAlign("foo"); + }, Error, "Unsupported alignment"); + assert.throws(function () { + return c.yAlign("foo"); + }, Error, "Unsupported alignment"); svg.remove(); }); + it("css classing works as expected", function () { assert.isFalse(c.classed("CSS-PREANCHOR-KEEP")); c.classed("CSS-PREANCHOR-KEEP", true); @@ -2790,6 +3469,7 @@ describe("Component behavior", function () { assert.isTrue(c.classed("CSS-PREANCHOR-REMOVE")); c.classed("CSS-PREANCHOR-REMOVE", false); assert.isFalse(c.classed("CSS-PREANCHOR-REMOVE")); + c._anchor(svg); assert.isTrue(c.classed("CSS-PREANCHOR-KEEP")); assert.isFalse(c.classed("CSS-PREANCHOR-REMOVE")); @@ -2802,29 +3482,42 @@ describe("Component behavior", function () { assert.equal(c.classed(undefined, true), c, "returns this when classed called w/ undefined and true"); svg.remove(); }); + it("detach() works as expected", function () { var cbCalled = 0; - var cb = function (b) { return cbCalled++; }; + var cb = function (b) { + return cbCalled++; + }; var b = new Plottable.Core.Broadcaster(null); + var c1 = new Plottable.Abstract.Component(); + b.registerListener(c1, cb); + c1.renderTo(svg); b.broadcast(); assert.equal(cbCalled, 1, "the callback was called"); assert.isTrue(svg.node().hasChildNodes(), "the svg has children"); c1.detach(); + b.broadcast(); assert.equal(cbCalled, 2, "the callback is still attached to the component"); assert.isFalse(svg.node().hasChildNodes(), "the svg has no children"); + svg.remove(); }); + it("can't reuse component if it's been remove()-ed", function () { var c1 = new Plottable.Abstract.Component(); c1.renderTo(svg); c1.remove(); - assert.throws(function () { return c1.renderTo(svg); }, "reuse"); + + assert.throws(function () { + return c1.renderTo(svg); + }, "reuse"); svg.remove(); }); + it("_invalidateLayout works as expected", function () { var cg = new Plottable.Component.Group(); var c = makeFixedSizeComponent(10, 10); @@ -2838,18 +3531,23 @@ describe("Component behavior", function () { assert.equal(cg.availableWidth, 50, "invalidateLayout propagated to parent and caused resized width"); svg.remove(); }); + it("components can be detached even if not anchored", function () { var c = new Plottable.Abstract.Component(); - c.detach(); + c.detach(); // no error thrown svg.remove(); }); }); +/// 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 (listenable) { assert.equal(listenable, ds, "Callback received the DataSource as the first argument"); @@ -2857,12 +3555,16 @@ describe("DataSource", function () { callbackCalled = true; }; ds.broadcaster.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 (listenable) { assert.equal(listenable, ds, "Callback received the DataSource as the first argument"); @@ -2870,28 +3572,42 @@ describe("DataSource", function () { callbackCalled = true; }; ds.broadcaster.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 plot = new Plottable.Abstract.Plot(dataSource); - var apply = function (a) { return Plottable.Util.Methods._applyAccessor(a, plot); }; - var a1 = function (d, i, m) { return d + i - 2; }; + var apply = function (a) { + return Plottable.Util.Methods._applyAccessor(a, plot); + }; + var a1 = function (d, i, m) { + return d + i - 2; + }; assert.deepEqual(dataSource._getExtent(apply(a1)), [-1, 5], "extent for numerical data works properly"); - var a2 = function (d, i, m) { return d + m.foo; }; + var a2 = function (d, i, m) { + return d + m.foo; + }; assert.deepEqual(dataSource._getExtent(apply(a2)), [12, 15], "extent uses metadata appropriately"); dataSource.metadata({ foo: -1 }); assert.deepEqual(dataSource._getExtent(apply(a2)), [0, 3], "metadata change is reflected in extent results"); - var a3 = function (d, i, m) { return "_" + d; }; + var a3 = function (d, i, m) { + return "_" + d; + }; assert.deepEqual(dataSource._getExtent(apply(a3)), ["_1", "_2", "_3", "_4"], "extent works properly on string domains (no repeats)"); }); }); +/// var assert = chai.assert; + function generateBasicTable(nRows, nCols) { + // makes a table with exactly nRows * nCols children in a regular grid, with each + // child being a basic component var table = new Plottable.Component.Table(); var rows = []; var components = []; @@ -2904,11 +3620,13 @@ function generateBasicTable(nRows, nCols) { } return { "table": table, "components": components }; } + describe("Tables", function () { it("tables are classed properly", function () { var table = new Plottable.Component.Table(); assert.isTrue(table.classed("table")); }); + it("padTableToSize works properly", function () { var t = new Plottable.Component.Table(); assert.deepEqual(t.rows, [], "the table rows is an empty list"); @@ -2919,11 +3637,15 @@ describe("Tables", function () { assert.lengthOf(rows, 1, "there is one row"); assert.lengthOf(row, 1, "the row has one element"); assert.isNull(firstComponent, "the row only has a null component"); + t.padTableToSize(5, 2); assert.lengthOf(rows, 5, "there are five rows"); - rows.forEach(function (r) { return assert.lengthOf(r, 2, "there are two columsn per row"); }); + rows.forEach(function (r) { + return assert.lengthOf(r, 2, "there are two columsn per row"); + }); assert.equal(rows[0][0], firstComponent, "the first component is unchanged"); }); + it("table constructor can take a list of lists of components", function () { var c0 = new Plottable.Abstract.Component(); var row1 = [null, c0]; @@ -2934,6 +3656,7 @@ describe("Tables", function () { table.addComponent(2, 2, c1); assert.equal(table.rows[2][2], c1, "the inserted component went to the right spot"); }); + it("tables can be constructed by adding components in matrix style", function () { var table = new Plottable.Component.Table(); var c1 = new Plottable.Abstract.Component(); @@ -2949,6 +3672,7 @@ describe("Tables", function () { assert.isNull(rows[0][1], "component at (0, 1) is null"); assert.isNull(rows[1][0], "component at (1, 0) is null"); }); + it("can't add a component where one already exists", function () { var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); @@ -2956,45 +3680,67 @@ describe("Tables", function () { var t = new Plottable.Component.Table(); t.addComponent(0, 2, c1); t.addComponent(0, 0, c2); - assert.throws(function () { return t.addComponent(0, 2, c3); }, Error, "component already exists"); + assert.throws(function () { + return t.addComponent(0, 2, c3); + }, Error, "component already exists"); }); + it("addComponent works even if a component is added with a high column and low row index", function () { + // Solves #180, a weird bug var t = new Plottable.Component.Table(); var svg = generateSVG(); t.addComponent(1, 0, new Plottable.Abstract.Component()); t.addComponent(0, 2, new Plottable.Abstract.Component()); - t.renderTo(svg); + t.renderTo(svg); //would throw an error without the fix (tested); svg.remove(); }); + it("basic table with 2 rows 2 cols lays out properly", function () { var tableAndcomponents = generateBasicTable(2, 2); var table = tableAndcomponents.table; var components = tableAndcomponents.components; + var svg = generateSVG(); table.renderTo(svg); - var elements = components.map(function (r) { return r.element; }); - var translates = elements.map(function (e) { return getTranslate(e); }); + + var elements = components.map(function (r) { + return r.element; + }); + var translates = elements.map(function (e) { + return getTranslate(e); + }); assert.deepEqual(translates[0], [0, 0], "first element is centered at origin"); assert.deepEqual(translates[1], [200, 0], "second element is located properly"); assert.deepEqual(translates[2], [0, 200], "third element is located properly"); assert.deepEqual(translates[3], [200, 200], "fourth element is located properly"); - var bboxes = elements.map(function (e) { return Plottable.Util.DOM.getBBox(e); }); + var bboxes = elements.map(function (e) { + return Plottable.Util.DOM.getBBox(e); + }); bboxes.forEach(function (b) { assert.equal(b.width, 200, "bbox is 200 pixels wide"); assert.equal(b.height, 200, "bbox is 200 pixels tall"); }); svg.remove(); }); + it("table with 2 rows 2 cols and margin/padding lays out properly", function () { var tableAndcomponents = generateBasicTable(2, 2); var table = tableAndcomponents.table; var components = tableAndcomponents.components; table.padding(5, 5); + var svg = generateSVG(415, 415); table.renderTo(svg); - var elements = components.map(function (r) { return r.element; }); - var translates = elements.map(function (e) { return getTranslate(e); }); - var bboxes = elements.map(function (e) { return Plottable.Util.DOM.getBBox(e); }); + + var elements = components.map(function (r) { + return r.element; + }); + var translates = elements.map(function (e) { + return getTranslate(e); + }); + var bboxes = elements.map(function (e) { + return Plottable.Util.DOM.getBBox(e); + }); assert.deepEqual(translates[0], [0, 0], "first element is centered properly"); assert.deepEqual(translates[1], [210, 0], "second element is located properly"); assert.deepEqual(translates[2], [0, 210], "third element is located properly"); @@ -3005,24 +3751,46 @@ describe("Tables", function () { }); svg.remove(); }); + it("table with fixed-size objects on every side lays out properly", function () { var svg = generateSVG(); var c4 = new Plottable.Abstract.Component(); + + // [0 1 2] \\ + // [3 4 5] \\ + // [6 7 8] \\ + // give the axis-like objects a minimum var c1 = makeFixedSizeComponent(null, 30); var c7 = makeFixedSizeComponent(null, 30); var c3 = makeFixedSizeComponent(50, null); var c5 = makeFixedSizeComponent(50, null); - var table = new Plottable.Component.Table([[null, c1, null], [c3, c4, c5], [null, c7, null]]); + var table = new Plottable.Component.Table([ + [null, c1, null], + [c3, c4, c5], + [null, c7, null]]); + var components = [c1, c3, c4, c5, c7]; + table.renderTo(svg); - var elements = components.map(function (r) { return r.element; }); - var translates = elements.map(function (e) { return getTranslate(e); }); - var bboxes = elements.map(function (e) { return Plottable.Util.DOM.getBBox(e); }); + + var elements = components.map(function (r) { + return r.element; + }); + var translates = elements.map(function (e) { + return getTranslate(e); + }); + var bboxes = elements.map(function (e) { + return Plottable.Util.DOM.getBBox(e); + }); + + // test the translates assert.deepEqual(translates[0], [50, 0], "top axis translate"); assert.deepEqual(translates[4], [50, 370], "bottom axis translate"); assert.deepEqual(translates[1], [0, 30], "left axis translate"); assert.deepEqual(translates[3], [350, 30], "right axis translate"); assert.deepEqual(translates[2], [50, 30], "plot translate"); + + // test the bboxes assertBBoxEquivalence(bboxes[0], [300, 30], "top axis bbox"); assertBBoxEquivalence(bboxes[4], [300, 30], "bottom axis bbox"); assertBBoxEquivalence(bboxes[1], [50, 340], "left axis bbox"); @@ -3030,11 +3798,14 @@ describe("Tables", function () { assertBBoxEquivalence(bboxes[2], [300, 340], "plot bbox"); svg.remove(); }); + it("table space fixity calculates properly", function () { var tableAndcomponents = generateBasicTable(3, 3); var table = tableAndcomponents.table; var components = tableAndcomponents.components; - components.forEach(function (c) { return fixComponentSize(c, 10, 10); }); + components.forEach(function (c) { + return fixComponentSize(c, 10, 10); + }); assert.isTrue(table._isFixedWidth(), "fixed width when all subcomponents fixed width"); assert.isTrue(table._isFixedHeight(), "fixedHeight when all subcomponents fixed height"); fixComponentSize(components[0], null, 10); @@ -3045,22 +3816,32 @@ describe("Tables", function () { assert.isTrue(table._isFixedWidth(), "width fixed again once no subcomponent width not fixed"); assert.isFalse(table._isFixedHeight(), "height unfixed now that a subcomponent has unfixed height"); }); + it.skip("table._requestedSpace works properly", function () { + // [0 1] + // [2 3] var c0 = new Plottable.Abstract.Component(); var c1 = makeFixedSizeComponent(50, 50); var c2 = makeFixedSizeComponent(20, 50); var c3 = makeFixedSizeComponent(20, 20); + var table = new Plottable.Component.Table([[c0, c1], [c2, c3]]); + var spaceRequest = table._requestedSpace(30, 30); verifySpaceRequest(spaceRequest, 30, 30, true, true, "1"); + spaceRequest = table._requestedSpace(50, 50); verifySpaceRequest(spaceRequest, 50, 50, true, true, "2"); + spaceRequest = table._requestedSpace(90, 90); verifySpaceRequest(spaceRequest, 70, 90, false, true, "3"); + spaceRequest = table._requestedSpace(200, 200); verifySpaceRequest(spaceRequest, 70, 100, false, false, "4"); }); + describe("table.iterateLayout works properly", function () { + // This test battery would have caught #405 function verifyLayoutResult(result, cPS, rPS, gW, gH, wW, wH, id) { assert.deepEqual(result.colProportionalSpace, cPS, "colProportionalSpace:" + id); assert.deepEqual(result.rowProportionalSpace, rPS, "rowProportionalSpace:" + id); @@ -3069,25 +3850,28 @@ describe("Tables", function () { assert.deepEqual(result.wantsWidth, wW, "wantsWidth:" + id); assert.deepEqual(result.wantsHeight, wH, "wantsHeight:" + id); } + var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); var c3 = new Plottable.Abstract.Component(); var c4 = new Plottable.Abstract.Component(); var table = new Plottable.Component.Table([ [c1, c2], - [c3, c4] - ]); + [c3, c4]]); + it("iterateLayout works in the easy case where there is plenty of space and everything is satisfied on first go", function () { fixComponentSize(c1, 50, 50); fixComponentSize(c4, 20, 10); var result = table.iterateLayout(500, 500); verifyLayoutResult(result, [215, 215], [220, 220], [50, 20], [50, 10], false, false, ""); }); + it.skip("iterateLayout works in the difficult case where there is a shortage of space and layout requires iterations", function () { fixComponentSize(c1, 490, 50); var result = table.iterateLayout(500, 500); verifyLayoutResult(result, [0, 0], [220, 220], [480, 20], [50, 10], true, false, ""); }); + it("iterateLayout works in the case where all components are fixed-size", function () { fixComponentSize(c1, 50, 50); fixComponentSize(c2, 50, 50); @@ -3095,11 +3879,16 @@ describe("Tables", function () { fixComponentSize(c4, 50, 50); var result = table.iterateLayout(100, 100); verifyLayoutResult(result, [0, 0], [0, 0], [50, 50], [50, 50], false, false, "..when there's exactly enough space"); + result = table.iterateLayout(80, 80); verifyLayoutResult(result, [0, 0], [0, 0], [40, 40], [40, 40], true, true, "..when there's not enough space"); + result = table.iterateLayout(120, 120); + + // If there is extra space in a fixed-size table, the extra space should not be allocated to proportional space verifyLayoutResult(result, [0, 0], [0, 0], [50, 50], [50, 50], false, false, "..when there's extra space"); }); + it.skip("iterateLayout works in the tricky case when components can be unsatisfied but request little space", function () { table = new Plottable.Component.Table([[c1, c2]]); fixComponentSize(c1, null, null); @@ -3117,6 +3906,7 @@ describe("Tables", function () { verifyLayoutResult(result, [150, 0], [0], [0, 0], [200], true, false, "when there's insufficient space"); }); }); + describe("table._removeComponent works properly", function () { var c1 = new Plottable.Abstract.Component(); var c2 = new Plottable.Abstract.Component(); @@ -3130,27 +3920,36 @@ describe("Tables", function () { table._removeComponent(c4); assert.deepEqual(table.rows, [[c1, c2], [c3, null], [c5, c6]], "remove one element"); }); + it("table._removeComponent does nothing when component is not found", function () { table = new Plottable.Component.Table([[c1, c2], [c3, c4]]); table._removeComponent(c5); + assert.deepEqual(table.rows, [[c1, c2], [c3, c4]], "remove nonexistent component"); }); + it("table._removeComponent removing component twice should have same effect as removing it once", function () { table = new Plottable.Component.Table([[c1, c2, c3], [c4, c5, c6]]); + table._removeComponent(c1); assert.deepEqual(table.rows, [[null, c2, c3], [c4, c5, c6]], "item twice"); + table._removeComponent(c1); assert.deepEqual(table.rows, [[null, c2, c3], [c4, c5, c6]], "item twice"); }); + it("table._removeComponent doesn't do anything weird when called with null", function () { table = new Plottable.Component.Table([[c1, null], [c2, c3]]); + table._removeComponent(null); assert.deepEqual(table.rows, [[c1, null], [c2, c3]]); }); }); }); +/// var assert = chai.assert; + describe("Domainer", function () { var scale; var domainer; @@ -3158,11 +3957,13 @@ describe("Domainer", function () { scale = new Plottable.Scale.Linear(); domainer = new Plottable.Domainer(); }); + it("pad() works in general case", function () { scale.updateExtent("1", "x", [100, 200]); scale.domainer(new Plottable.Domainer().pad(0.2)); assert.deepEqual(scale.domain(), [90, 210]); }); + it("pad() works for date scales", function () { var timeScale = new Plottable.Scale.Time(); var f = d3.time.format("%x"); @@ -3179,6 +3980,7 @@ describe("Domainer", function () { assert.equal(dd1.valueOf(), dd1.valueOf(), "date1 is not NaN"); assert.equal(dd2.valueOf(), dd2.valueOf(), "date2 is not NaN"); }); + it("pad() works on log scales", function () { var logScale = new Plottable.Scale.Log(); logScale.updateExtent("1", "x", [10, 100]); @@ -3195,30 +3997,39 @@ describe("Domainer", function () { assert.closeTo(logScale.domain()[0], 1, 0.001); assert.closeTo(logScale.domain()[1], 1000, 0.001); }); + it("pad() defaults to [v-1, v+1] if there's only one numeric value", function () { domainer.pad(); var domain = domainer.computeDomain([[5, 5]], scale); assert.deepEqual(domain, [4, 6]); }); + it("pad() defaults to [v-1 day, v+1 day] if there's only one date value", function () { var d = new Date(2000, 5, 5); var dayBefore = new Date(2000, 5, 4); var dayAfter = new Date(2000, 5, 6); var timeScale = new Plottable.Scale.Time(); + + // the result of computeDomain() will be number[], but when it + // gets fed back into timeScale, it will be adjusted back to a Date. + // That's why I'm using updateExtent() instead of domainer.computeDomain() timeScale.updateExtent("1", "x", [d, d]); timeScale.domainer(new Plottable.Domainer().pad()); assert.deepEqual(timeScale.domain(), [dayBefore, dayAfter]); }); + it("pad() only takes the last value", function () { domainer.pad(1000).pad(4).pad(0.1); var domain = domainer.computeDomain([[100, 200]], scale); assert.deepEqual(domain, [95, 205]); }); + it("pad() will pad beyond 0 by default", function () { domainer.pad(0.1); var domain = domainer.computeDomain([[0, 100]], scale); assert.deepEqual(domain, [-5, 105]); }); + it("pad() works with scales that have 0-size domain", function () { scale.domain([5, 5]); var domain = domainer.computeDomain([[0, 100]], scale); @@ -3227,6 +4038,7 @@ describe("Domainer", function () { domain = domainer.computeDomain([[0, 100]], scale); assert.deepEqual(domain, [0, 100]); }); + it("paddingException(n) will not pad beyond n", function () { domainer.pad(0.1).addPaddingException(0, "key").addPaddingException(200); var domain = domainer.computeDomain([[0, 100]], scale); @@ -3242,6 +4054,7 @@ describe("Domainer", function () { domain = domainer.computeDomain([[0, 200]], scale); assert.notEqual(domain[1], 200, "unregistered paddingExceptions can be removed using boolean argument"); }); + it("paddingException(n) works on dates", function () { var a = new Date(2000, 5, 5); var b = new Date(2003, 0, 1); @@ -3253,6 +4066,7 @@ describe("Domainer", function () { assert.deepEqual(domain[0], a); assert.isTrue(b < domain[1]); }); + it("include(n) works an expected", function () { domainer.addIncludedValue(5); var domain = domainer.computeDomain([[0, 10]], scale); @@ -3261,16 +4075,19 @@ describe("Domainer", function () { assert.deepEqual(domain, [0, 5]); domain = domainer.computeDomain([[100, 200]], scale); assert.deepEqual(domain, [5, 200]); + domainer.addIncludedValue(-3).addIncludedValue(0).addIncludedValue(10, "key"); domain = domainer.computeDomain([[100, 200]], scale); assert.deepEqual(domain, [-3, 200]); domain = domainer.computeDomain([[0, 0]], scale); assert.deepEqual(domain, [-3, 10]); + domainer.removeIncludedValue("key"); domain = domainer.computeDomain([[100, 200]], scale); assert.deepEqual(domain, [-3, 200]); domain = domainer.computeDomain([[-100, -50]], scale); assert.deepEqual(domain, [-100, 5]); + domainer.addIncludedValue(10); domain = domainer.computeDomain([[-100, -50]], scale); assert.deepEqual(domain, [-100, 10], "unregistered includedValues can be added"); @@ -3278,6 +4095,7 @@ describe("Domainer", function () { domain = domainer.computeDomain([[-100, -50]], scale); assert.deepEqual(domain, [-100, 5], "unregistered includedValues can be removed with addOrRemove argument"); }); + it("include(n) works on dates", function () { var a = new Date(2000, 5, 4); var b = new Date(2000, 5, 5); @@ -3289,6 +4107,7 @@ describe("Domainer", function () { timeScale.domainer(domainer); assert.deepEqual(timeScale.domain(), [b, d]); }); + it("exceptions are setup properly on an area plot", function () { var xScale = new Plottable.Scale.Linear(); var yScale = new Plottable.Scale.Linear(); @@ -3299,6 +4118,7 @@ describe("Domainer", function () { r.project("x", "x", xScale); r.project("y", "y", yScale); r.renderTo(svg); + function getExceptions() { yScale.autoDomain(); var yDomain = yScale.domain(); @@ -3311,12 +4131,17 @@ describe("Domainer", function () { } return exceptions; } + assert.deepEqual(getExceptions(), [0], "initializing the plot adds a padding exception at 0"); + + // assert.deepEqual(getExceptions(), [], "Initially there are no padding exceptions"); r.project("y0", "y0", yScale); assert.deepEqual(getExceptions(), [], "projecting a non-constant y0 removes the padding exception"); r.project("y0", 0, yScale); assert.deepEqual(getExceptions(), [0], "projecting constant y0 adds the exception back"); - r.project("y0", function () { return 5; }, yScale); + r.project("y0", function () { + return 5; + }, yScale); assert.deepEqual(getExceptions(), [5], "projecting a different constant y0 removed the old exception and added a new one"); r.project("y0", "y0", yScale); assert.deepEqual(getExceptions(), [], "projecting a non-constant y0 removes the padding exception"); @@ -3326,7 +4151,9 @@ describe("Domainer", function () { }); }); +/// var assert = chai.assert; + describe("Coordinators", function () { describe("ScaleDomainCoordinator", function () { it("domains are coordinated", function () { @@ -3338,6 +4165,7 @@ describe("Coordinators", function () { assert.deepEqual(s1.domain(), [0, 100]); assert.deepEqual(s1.domain(), s2.domain()); assert.deepEqual(s1.domain(), s3.domain()); + s1.domain([-100, 5000]); assert.deepEqual(s1.domain(), [-100, 5000]); assert.deepEqual(s1.domain(), s2.domain()); @@ -3346,7 +4174,9 @@ describe("Coordinators", function () { }); }); +/// var assert = chai.assert; + describe("Scales", function () { it("Scale's copy() works correctly", function () { var testCallback = function (broadcaster) { @@ -3359,6 +4189,7 @@ describe("Scales", function () { assert.deepEqual(scale.range(), scaleCopy.range(), "Copied scale has the same range as the original."); assert.notDeepEqual(scale.broadcaster, scaleCopy.broadcaster, "Broadcasters are not copied over"); }); + it("Scale alerts listeners when its domain is updated", function () { var scale = new Plottable.Scale.Linear(); var callbackWasCalled = false; @@ -3369,15 +4200,18 @@ describe("Scales", function () { scale.broadcaster.registerListener(null, testCallback); scale.domain([0, 10]); assert.isTrue(callbackWasCalled, "The registered callback was called"); + scale.autoDomainAutomatically = true; scale.updateExtent("1", "x", [0.08, 9.92]); callbackWasCalled = false; scale.domainer(new Plottable.Domainer().nice()); assert.isTrue(callbackWasCalled, "The registered callback was called when nice() is used to set the domain"); + callbackWasCalled = false; scale.domainer(new Plottable.Domainer().pad()); assert.isTrue(callbackWasCalled, "The registered callback was called when padDomain() is used to set the domain"); }); + describe("autoranging behavior", function () { var data; var dataSource; @@ -3387,13 +4221,17 @@ describe("Scales", function () { dataSource = new Plottable.DataSource(data); scale = new Plottable.Scale.Linear(); }); + it("scale autoDomain flag is not overwritten without explicitly setting the domain", function () { - scale.updateExtent("1", "x", d3.extent(data, function (e) { return e.foo; })); + scale.updateExtent("1", "x", d3.extent(data, function (e) { + return e.foo; + })); scale.domainer(new Plottable.Domainer().pad().nice()); assert.isTrue(scale.autoDomainAutomatically, "the autoDomain flag is still set after autoranginging and padding and nice-ing"); scale.domain([0, 5]); assert.isFalse(scale.autoDomainAutomatically, "the autoDomain flag is false after domain explicitly set"); }); + it("scale autorange works as expected with single dataSource", function () { var svg = generateSVG(100, 100); var renderer = new Plottable.Abstract.Plot().dataSource(dataSource).project("x", "foo", scale).renderTo(svg); @@ -3403,6 +4241,7 @@ describe("Scales", function () { assert.deepEqual(scale.domain(), [0, 100], "scale domain was autoranged properly"); svg.remove(); }); + it("scale reference counting works as expected", function () { var svg1 = generateSVG(100, 100); var svg2 = generateSVG(100, 100); @@ -3415,25 +4254,35 @@ describe("Scales", function () { dataSource.data([{ foo: 10 }, { foo: 11 }]); assert.deepEqual(scale.domain(), [10, 11], "scale was still listening to dataSource after one perspective deregistered"); renderer2.project("x", "foo", otherScale); + + // "scale not listening to the dataSource after all perspectives removed" dataSource.data([{ foo: 99 }, { foo: 100 }]); assert.deepEqual(scale.domain(), [0, 1], "scale shows default values when all perspectives removed"); svg1.remove(); svg2.remove(); }); + it("scale perspectives can be removed appropriately", function () { assert.isTrue(scale.autoDomainAutomatically, "autoDomain enabled1"); - scale.updateExtent("1", "x", d3.extent(data, function (e) { return e.foo; })); - scale.updateExtent("2", "x", d3.extent(data, function (e) { return e.bar; })); + scale.updateExtent("1", "x", d3.extent(data, function (e) { + return e.foo; + })); + scale.updateExtent("2", "x", d3.extent(data, function (e) { + return e.bar; + })); assert.isTrue(scale.autoDomainAutomatically, "autoDomain enabled2"); assert.deepEqual(scale.domain(), [-20, 5], "scale domain includes both perspectives"); assert.isTrue(scale.autoDomainAutomatically, "autoDomain enabled3"); scale.removeExtent("1", "x"); assert.isTrue(scale.autoDomainAutomatically, "autoDomain enabled4"); assert.deepEqual(scale.domain(), [-20, 1], "only the bar accessor is active"); - scale.updateExtent("2", "x", d3.extent(data, function (e) { return e.foo; })); + scale.updateExtent("2", "x", d3.extent(data, function (e) { + return e.foo; + })); assert.isTrue(scale.autoDomainAutomatically, "autoDomain enabled5"); assert.deepEqual(scale.domain(), [0, 5], "the bar accessor was overwritten"); }); + it("should resize when a plot is removed", function () { var svg = generateSVG(400, 400); var ds1 = [{ x: 0, y: 0 }, { x: 1, y: 1 }]; @@ -3455,6 +4304,7 @@ describe("Scales", function () { svg.remove(); }); }); + describe("Quantitative Scales", function () { it("autorange defaults to [0, 1] if no perspectives set", function () { var scale = new Plottable.Scale.Linear(); @@ -3463,11 +4313,13 @@ describe("Scales", function () { assert.equal(d[0], 0); assert.equal(d[1], 1); }); + it("autorange defaults to [1, 10] on log scale", function () { var scale = new Plottable.Scale.Log(); scale.autoDomain(); assert.deepEqual(scale.domain(), [1, 10]); }); + it("domain can't include NaN or Infinity", function () { var scale = new Plottable.Scale.Linear(); scale.domain([0, 1]); @@ -3481,26 +4333,32 @@ describe("Scales", function () { assert.deepEqual(scale.domain(), [-1, 5], "Regular domains still accepted"); }); }); + describe("Ordinal Scales", function () { it("defaults to \"bands\" range type", function () { var scale = new Plottable.Scale.Ordinal(); assert.deepEqual(scale.rangeType(), "bands"); }); + it("rangeBand returns 0 when in \"points\" mode", function () { var scale = new Plottable.Scale.Ordinal().rangeType("points"); assert.deepEqual(scale.rangeType(), "points"); assert.deepEqual(scale.rangeBand(), 0); }); + it("rangeBand is updated when domain changes in \"bands\" mode", function () { var scale = new Plottable.Scale.Ordinal(); 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); }); + it("rangeType triggers broadcast", function () { var scale = new Plottable.Scale.Ordinal(); var callbackWasCalled = false; @@ -3513,7 +4371,9 @@ describe("Scales", function () { assert.isTrue(callbackWasCalled, "The registered callback was called"); }); }); + it("OrdinalScale + BarPlot combo works as expected when the data is swapped", function () { + // This unit test taken from SLATE, see SLATE-163 a fix for SLATE-102 var xScale = new Plottable.Scale.Ordinal(); var yScale = new Plottable.Scale.Linear(); var dA = { x: "A", y: 2 }; @@ -3524,23 +4384,27 @@ describe("Scales", function () { assert.deepEqual(xScale.domain(), [], "before anchoring, the bar plot doesn't proxy data to the scale"); barPlot.renderTo(svg); assert.deepEqual(xScale.domain(), ["A", "B"], "after anchoring, the bar plot's data is on the scale"); + function iterateDataChanges() { var dataChanges = []; - for (var _i = 0; _i < arguments.length; _i++) { - dataChanges[_i - 0] = arguments[_i]; + for (var _i = 0; _i < (arguments.length - 0); _i++) { + dataChanges[_i] = arguments[_i + 0]; } dataChanges.forEach(function (dataChange) { barPlot.dataSource().data(dataChange); }); } + iterateDataChanges([], [dA, dB, dC], []); assert.lengthOf(xScale.domain(), 0); + iterateDataChanges([dA], [dB]); assert.lengthOf(xScale.domain(), 1); iterateDataChanges([], [dA, dB, dC]); assert.lengthOf(xScale.domain(), 3); svg.remove(); }); + describe("Color Scales", function () { it("accepts categorical string types and ordinal domain", function () { var scale = new Plottable.Scale.Color("10"); @@ -3550,6 +4414,7 @@ describe("Scales", function () { assert.equal("#2ca02c", scale.scale("maybe")); }); }); + describe("Interpolated Color Scales", function () { it("default scale uses reds and a linear scale type", function () { var scale = new Plottable.Scale.InterpolatedColor(); @@ -3558,12 +4423,14 @@ describe("Scales", function () { assert.equal("#feb24c", scale.scale(8)); assert.equal("#b10026", scale.scale(16)); }); + it("linearly interpolates colors in L*a*b color space", function () { var scale = new Plottable.Scale.InterpolatedColor("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.Scale.InterpolatedColor(["#000", "#FFF"]); scale.domain([0, 16]); @@ -3571,6 +4438,7 @@ describe("Scales", function () { assert.equal("#ffffff", scale.scale(16)); assert.equal("#777777", scale.scale(8)); }); + it("accepts array types with color names", function () { var scale = new Plottable.Scale.InterpolatedColor(["black", "white"]); scale.domain([0, 16]); @@ -3578,6 +4446,7 @@ describe("Scales", function () { assert.equal("#ffffff", scale.scale(16)); assert.equal("#777777", scale.scale(8)); }); + it("overflow scale values clamp to range", function () { var scale = new Plottable.Scale.InterpolatedColor(["black", "white"]); scale.domain([0, 16]); @@ -3586,6 +4455,7 @@ describe("Scales", function () { assert.equal("#000000", scale.scale(-100)); assert.equal("#ffffff", scale.scale(100)); }); + it("can be converted to a different range", function () { var scale = new Plottable.Scale.InterpolatedColor(["black", "white"]); scale.domain([0, 16]); @@ -3594,17 +4464,20 @@ describe("Scales", function () { scale.colorRange("reds"); assert.equal("#b10026", scale.scale(16)); }); + it("can be converted to a different scale type", function () { var scale = new Plottable.Scale.InterpolatedColor(["black", "white"]); scale.domain([0, 16]); assert.equal("#000000", scale.scale(0)); assert.equal("#ffffff", scale.scale(16)); assert.equal("#777777", scale.scale(8)); + scale.scaleType("log"); assert.equal("#000000", scale.scale(0)); assert.equal("#ffffff", scale.scale(16)); assert.equal("#e3e3e3", scale.scale(8)); }); + it("doesn't use a domainer", function () { var scale = new Plottable.Scale.InterpolatedColor(["black", "white"]); var startDomain = scale.domain(); @@ -3620,74 +4493,109 @@ describe("Scales", function () { beforeEach(function () { scale = new Plottable.Scale.ModifiedLog(base); }); + it("is an increasing, continuous function that can go negative", function () { d3.range(-base * 2, base * 2, base / 20).forEach(function (x) { + // increasing assert.operator(scale.scale(x - epsilon), "<", scale.scale(x)); assert.operator(scale.scale(x), "<", scale.scale(x + epsilon)); + + // continuous assert.closeTo(scale.scale(x - epsilon), scale.scale(x), epsilon); assert.closeTo(scale.scale(x), scale.scale(x + epsilon), epsilon); }); assert.closeTo(scale.scale(0), 0, epsilon); }); + it("is close to log() for large values", function () { [10, 100, 23103.4, 5].forEach(function (x) { assert.closeTo(scale.scale(x), Math.log(x) / Math.log(10), 0.1); }); }); + it("x = invert(scale(x))", function () { [0, 1, base, 100, 0.001, -1, -0.3, -base, base - 0.001].forEach(function (x) { assert.closeTo(x, scale.invert(scale.scale(x)), epsilon); assert.closeTo(x, scale.scale(scale.invert(x)), epsilon); }); }); + it("domain defaults to [0, 1]", function () { scale = new Plottable.Scale.ModifiedLog(base); assert.deepEqual(scale.domain(), [0, 1]); }); + it("works with a domainer", function () { scale.updateExtent("1", "x", [0, base * 2]); var domain = scale.domain(); scale.domainer(new Plottable.Domainer().pad(0.1)); assert.operator(scale.domain()[0], "<", domain[0]); assert.operator(domain[1], "<", scale.domain()[1]); + scale.domainer(new Plottable.Domainer().nice()); assert.operator(scale.domain()[0], "<=", domain[0]); assert.operator(domain[1], "<=", scale.domain()[1]); + scale = new Plottable.Scale.ModifiedLog(base); scale.domainer(new Plottable.Domainer()); assert.deepEqual(scale.domain(), [0, 1]); }); + it("gives reasonable values for ticks()", function () { scale.updateExtent("1", "x", [0, base / 2]); var ticks = scale.ticks(); assert.operator(ticks.length, ">", 0); + scale.updateExtent("1", "x", [-base * 2, base * 2]); ticks = scale.ticks(); - var beforePivot = ticks.filter(function (x) { return x <= -base; }); - var afterPivot = ticks.filter(function (x) { return base <= x; }); - var betweenPivots = ticks.filter(function (x) { return -base < x && x < base; }); + var beforePivot = ticks.filter(function (x) { + return x <= -base; + }); + var afterPivot = ticks.filter(function (x) { + return base <= x; + }); + var betweenPivots = ticks.filter(function (x) { + return -base < x && x < base; + }); assert.operator(beforePivot.length, ">", 0, "should be ticks before -base"); assert.operator(afterPivot.length, ">", 0, "should be ticks after base"); assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base"); }); + it("works on inverted domain", function () { scale.updateExtent("1", "x", [200, -100]); var range = scale.range(); assert.closeTo(scale.scale(-100), range[1], epsilon); assert.closeTo(scale.scale(200), range[0], epsilon); var a = [-100, -10, -3, 0, 1, 3.64, 50, 60, 200]; - var b = a.map(function (x) { return scale.scale(x); }); - assert.deepEqual(b.slice().reverse(), b.slice().sort(function (x, y) { return x - y; })); + var b = a.map(function (x) { + return scale.scale(x); + }); + + // should be decreasing function; reverse is sorted + assert.deepEqual(b.slice().reverse(), b.slice().sort(function (x, y) { + return x - y; + })); + var ticks = scale.ticks(); - assert.deepEqual(ticks, ticks.slice().sort(function (x, y) { return x - y; }), "ticks should be sorted"); + assert.deepEqual(ticks, ticks.slice().sort(function (x, y) { + return x - y; + }), "ticks should be sorted"); assert.deepEqual(ticks, Plottable.Util.Methods.uniqNumbers(ticks), "ticks should not be repeated"); - var beforePivot = ticks.filter(function (x) { return x <= -base; }); - var afterPivot = ticks.filter(function (x) { return base <= x; }); - var betweenPivots = ticks.filter(function (x) { return -base < x && x < base; }); + var beforePivot = ticks.filter(function (x) { + return x <= -base; + }); + var afterPivot = ticks.filter(function (x) { + return base <= x; + }); + var betweenPivots = ticks.filter(function (x) { + return -base < x && x < base; + }); assert.operator(beforePivot.length, ">", 0, "should be ticks before -base"); assert.operator(afterPivot.length, ">", 0, "should be ticks after base"); assert.operator(betweenPivots.length, ">", 0, "should be ticks between -base and base"); }); + it("ticks() is always non-empty", function () { [[2, 9], [0, 1], [1, 2], [0.001, 0.01], [-0.1, 0.1], [-3, -2]].forEach(function (domain) { scale.updateExtent("1", "x", domain); @@ -3698,12 +4606,15 @@ describe("Scales", function () { }); }); +/// var assert = chai.assert; + describe("TimeScale tests", function () { it("parses reasonable formats for dates", function () { var scale = new Plottable.Scale.Time(); var firstDate = new Date(2014, 9, 1, 0, 0, 0, 0).valueOf(); var secondDate = new Date(2014, 10, 1, 0, 0, 0).valueOf(); + function checkDomain(domain) { scale.domain(domain); var time1 = scale.domain()[0].valueOf(); @@ -3715,34 +4626,49 @@ describe("TimeScale tests", function () { checkDomain(["October 1, 2014", "November 1, 2014"]); checkDomain(["Oct 1, 2014", "Nov 1, 2014"]); }); + it("tickInterval produces correct number of ticks", function () { var scale = new Plottable.Scale.Time(); + + // 100 year span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2100, 0, 1, 0, 0, 0, 0)]); var ticks = scale.tickInterval(d3.time.year); assert.equal(ticks.length, 101, "generated correct number of ticks"); + + // 1 year span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 11, 31, 0, 0, 0, 0)]); ticks = scale.tickInterval(d3.time.month); assert.equal(ticks.length, 12, "generated correct number of ticks"); ticks = scale.tickInterval(d3.time.month, 3); assert.equal(ticks.length, 4, "generated correct number of ticks"); + + // 1 month span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 1, 1, 0, 0, 0, 0)]); ticks = scale.tickInterval(d3.time.day); assert.equal(ticks.length, 32, "generated correct number of ticks"); + + // 1 day span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 23, 0, 0, 0)]); ticks = scale.tickInterval(d3.time.hour); assert.equal(ticks.length, 24, "generated correct number of ticks"); + + // 1 hour span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 1, 0, 0, 0)]); ticks = scale.tickInterval(d3.time.minute); assert.equal(ticks.length, 61, "generated correct number of ticks"); ticks = scale.tickInterval(d3.time.minute, 10); assert.equal(ticks.length, 7, "generated correct number of ticks"); + + // 1 minute span scale.domain([new Date(2000, 0, 1, 0, 0, 0, 0), new Date(2000, 0, 1, 0, 1, 0, 0)]); ticks = scale.tickInterval(d3.time.second); assert.equal(ticks.length, 61, "generated correct number of ticks"); }); }); +/// var assert = chai.assert; + describe("Util.DOM", function () { it("getBBox works properly", function () { var svg = generateSVG(); @@ -3752,47 +4678,58 @@ describe("Util.DOM", function () { assert.deepEqual(bb1, bb2); svg.remove(); }); + describe("getElementWidth, getElementHeight", function () { 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.Util.DOM.getElementWidth(parentElem); assert.equal(width, 300, "measured width matches set width"); var height = Plottable.Util.DOM.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.Util.DOM.getElementWidth(svgElem); assert.equal(width, 450, "measured width matches set width"); var height = Plottable.Util.DOM.getElementHeight(svgElem); assert.equal(height, 120, "measured height matches set height"); svg.remove(); }); + it("can accept multiple units and convert to pixels", function () { var parent = getSVGParent(); var parentElem = parent[0][0]; var child = parent.append("div"); var childElem = child[0][0]; + parent.style("width", "200px"); parent.style("height", "50px"); assert.equal(Plottable.Util.DOM.getElementWidth(parentElem), 200, "width is correct"); assert.equal(Plottable.Util.DOM.getElementHeight(parentElem), 50, "height is correct"); + child.style("width", "20px"); child.style("height", "10px"); assert.equal(Plottable.Util.DOM.getElementWidth(childElem), 20, "width is correct"); assert.equal(Plottable.Util.DOM.getElementHeight(childElem), 10, "height is correct"); + child.style("width", "100%"); child.style("height", "100%"); assert.equal(Plottable.Util.DOM.getElementWidth(childElem), 200, "width is correct"); assert.equal(Plottable.Util.DOM.getElementHeight(childElem), 50, "height is correct"); + child.style("width", "50%"); child.style("height", "50%"); assert.equal(Plottable.Util.DOM.getElementWidth(childElem), 100, "width is correct"); assert.equal(Plottable.Util.DOM.getElementHeight(childElem), 25, "height is correct"); + + // reset test page DOM parent.style("width", "auto"); parent.style("height", "auto"); child.remove(); @@ -3800,7 +4737,9 @@ describe("Util.DOM", function () { }); }); +/// var assert = chai.assert; + describe("Formatters", function () { describe("fixed", function () { it("shows exactly [precision] digits", function () { @@ -3812,17 +4751,20 @@ describe("Formatters", function () { result = fixed3(1.2345); assert.strictEqual(result, "", "changed values are not shown (get turned into empty strings)"); }); + it("precision can be changed", function () { var fixed2 = Plottable.Formatters.fixed(2); var result = fixed2(1); assert.strictEqual(result, "1.00", "formatter was changed to show only two decimal places"); }); + it("can be set to show rounded values", function () { var fixed3 = Plottable.Formatters.fixed(3, false); var result = fixed3(1.2349); assert.strictEqual(result, "1.235", "long values are rounded correctly"); }); }); + describe("general", function () { it("formats number to show at most [precision] digits", function () { var general = Plottable.Formatters.general(); @@ -3833,6 +4775,7 @@ describe("Formatters", function () { result = general(1.2345); assert.strictEqual(result, "", "(changed) values with more than three decimal places are not shown"); }); + it("stringifies non-number values", function () { var general = Plottable.Formatters.general(); var result = general("blargh"); @@ -3840,6 +4783,7 @@ describe("Formatters", function () { result = general(null); assert.strictEqual(result, "null", "non-number inputs are stringified"); }); + it("throws an error on strange precision", function () { assert.throws(function () { var general = Plottable.Formatters.general(-1); @@ -3851,6 +4795,7 @@ describe("Formatters", function () { }); }); }); + describe("identity", function () { it("stringifies inputs", function () { var identity = Plottable.Formatters.identity(); @@ -3864,6 +4809,7 @@ describe("Formatters", function () { assert.strictEqual(result, "undefined", "formats undefined"); }); }); + describe("currency", function () { it("uses reasonable defaults", function () { var currencyFormatter = Plottable.Formatters.currency(); @@ -3871,19 +4817,24 @@ describe("Formatters", function () { assert.strictEqual(result.charAt(0), "$", "defaults to $ for currency symbol"); var decimals = result.substring(result.indexOf(".") + 1, result.length); assert.strictEqual(decimals.length, 2, "defaults to 2 decimal places"); + result = currencyFormatter(-1); assert.strictEqual(result.charAt(0), "-", "prefixes negative values with \"-\""); assert.strictEqual(result.charAt(1), "$", "places the currency symbol after the negative sign"); }); + it("can change the type and position of the currency symbol", function () { var centsFormatter = Plottable.Formatters.currency(0, "c", false); var result = centsFormatter(1); assert.strictEqual(result.charAt(result.length - 1), "c", "The specified currency symbol was appended"); }); }); + describe("time", function () { it("uses reasonable defaults", function () { var timeFormatter = Plottable.Formatters.time(); + + // year, month, day, hours, minutes, seconds, milliseconds var result = timeFormatter(new Date(2000, 0, 1, 0, 0, 0, 0)); assert.strictEqual(result, "2000", "only the year was displayed"); result = timeFormatter(new Date(2000, 2, 1, 0, 0, 0, 0)); @@ -3900,12 +4851,14 @@ describe("Formatters", function () { assert.strictEqual(result, ".950", "milliseconds was displayed"); }); }); + describe("percentage", function () { it("uses reasonable defaults", function () { var percentFormatter = Plottable.Formatters.percentage(); var result = percentFormatter(1); assert.strictEqual(result, "100%", "the value was multiplied by 100, a percent sign was appended, and no decimal places are shown by default"); }); + it("can handle float imprecision", function () { var percentFormatter = Plottable.Formatters.percentage(); var result = percentFormatter(0.07); @@ -3915,9 +4868,12 @@ describe("Formatters", function () { assert.strictEqual(result2, "0.35%", "works even if multiplying by 100 does not make it an integer"); }); }); + describe("time", function () { it("uses reasonable defaults", function () { var timeFormatter = Plottable.Formatters.time(); + + // year, month, day, hours, minutes, seconds, milliseconds var result = timeFormatter(new Date(2000, 0, 1, 0, 0, 0, 0)); assert.strictEqual(result, "2000", "only the year was displayed"); result = timeFormatter(new Date(2000, 2, 1, 0, 0, 0, 0)); @@ -3934,6 +4890,7 @@ describe("Formatters", function () { assert.strictEqual(result, ".950", "milliseconds was displayed"); }); }); + describe("SISuffix", function () { it("shortens long numbers", function () { var lnFormatter = Plottable.Formatters.siSuffix(); @@ -3945,12 +4902,14 @@ describe("Formatters", function () { assert.operator(result.length, "<=", 5, "small number was formatted to a short string"); }); }); + describe("relativeDate", function () { it("uses reasonable defaults", function () { var relativeDateFormatter = Plottable.Formatters.relativeDate(); var result = relativeDateFormatter(7 * Plottable.MILLISECONDS_IN_ONE_DAY); assert.strictEqual(result, "7", "7 day difference from epoch, incremented by days, no suffix"); }); + it("resulting value is difference from base value", function () { var relativeDateFormatter = Plottable.Formatters.relativeDate(5 * Plottable.MILLISECONDS_IN_ONE_DAY); var result = relativeDateFormatter(9 * Plottable.MILLISECONDS_IN_ONE_DAY); @@ -3958,14 +4917,17 @@ describe("Formatters", function () { var result = relativeDateFormatter(Plottable.MILLISECONDS_IN_ONE_DAY); assert.strictEqual(result, "-4", "4 days less from base value"); }); + it("can increment by different time types (hours, minutes)", function () { var hoursRelativeDateFormatter = Plottable.Formatters.relativeDate(0, Plottable.MILLISECONDS_IN_ONE_DAY / 24); var result = hoursRelativeDateFormatter(3 * Plottable.MILLISECONDS_IN_ONE_DAY); assert.strictEqual(result, "72", "72 hour difference from epoch"); + var minutesRelativeDateFormatter = Plottable.Formatters.relativeDate(0, Plottable.MILLISECONDS_IN_ONE_DAY / (24 * 60)); var result = minutesRelativeDateFormatter(3 * Plottable.MILLISECONDS_IN_ONE_DAY); assert.strictEqual(result, "4320", "4320 minute difference from epoch"); }); + it("can append a suffix", function () { var relativeDateFormatter = Plottable.Formatters.relativeDate(0, Plottable.MILLISECONDS_IN_ONE_DAY, "days"); var result = relativeDateFormatter(7 * Plottable.MILLISECONDS_IN_ONE_DAY); @@ -3974,7 +4936,9 @@ describe("Formatters", function () { }); }); +/// var assert = chai.assert; + describe("IDCounter", function () { it("IDCounter works as expected", function () { var i = new Plottable.Util.IDCounter(); @@ -3989,7 +4953,9 @@ describe("IDCounter", function () { }); }); +/// var assert = chai.assert; + describe("StrictEqualityAssociativeArray", function () { it("StrictEqualityAssociativeArray works as expected", function () { var s = new Plottable.Util.StrictEqualityAssociativeArray(); @@ -4014,6 +4980,7 @@ describe("StrictEqualityAssociativeArray", function () { assert.equal(s.get(o2), "baz"); assert.equal(s.get("3"), "ball"); }); + it("Array-level operations (retrieve keys, vals, and map)", function () { var s = new Plottable.Util.StrictEqualityAssociativeArray(); s.set(2, "foo"); @@ -4021,28 +4988,36 @@ describe("StrictEqualityAssociativeArray", function () { s.set(4, "baz"); assert.deepEqual(s.values(), ["foo", "bar", "baz"]); assert.deepEqual(s.keys(), [2, 3, 4]); - assert.deepEqual(s.map(function (k, v, i) { return [k, v, i]; }), [[2, "foo", 0], [3, "bar", 1], [4, "baz", 2]]); + assert.deepEqual(s.map(function (k, v, i) { + return [k, v, i]; + }), [[2, "foo", 0], [3, "bar", 1], [4, "baz", 2]]); }); }); +/// var assert = chai.assert; + describe("CachingCharacterMeasurer", function () { var g; var measurer; var svg; + beforeEach(function () { svg = generateSVG(100, 100); g = svg.append("g"); measurer = new Plottable.Util.Text.CachingCharacterMeasurer(g.append("text")); }); + afterEach(function () { svg.remove(); }); + it("empty string has non-zero size", function () { var a = measurer.measure("x x").width; var b = measurer.measure("xx").width; assert.operator(a, ">", b, "'x x' is longer than 'xx'"); }); + it("should repopulate cache if it changes size and clear() is called", function () { var a = measurer.measure("x").width; g.style("font-size", "40px"); @@ -4052,6 +5027,7 @@ describe("CachingCharacterMeasurer", function () { var c = measurer.measure("x").width; assert.operator(a, "<", c, "cache reset after font size changed"); }); + it("multiple spaces take up same area as one space", function () { var a = measurer.measure("x x").width; var b = measurer.measure("x \t \n x").width; @@ -4059,7 +5035,9 @@ describe("CachingCharacterMeasurer", function () { }); }); +/// var assert = chai.assert; + describe("Cache", function () { var callbackCalled = false; var f = function (s) { @@ -4067,10 +5045,12 @@ describe("Cache", function () { return s + s; }; var cache; + beforeEach(function () { callbackCalled = false; cache = new Plottable.Util.Cache(f); }); + it("Doesn't call its function if it already called", function () { assert.equal(cache.get("hello"), "hellohello"); assert.isTrue(callbackCalled); @@ -4078,6 +5058,7 @@ describe("Cache", function () { assert.equal(cache.get("hello"), "hellohello"); assert.isFalse(callbackCalled); }); + it("Clears its cache when .clear() is called", function () { var prefix = "hello"; cache = new Plottable.Util.Cache(function (s) { @@ -4094,6 +5075,7 @@ describe("Cache", function () { assert.equal(cache.get("world"), "holaworld"); assert.isTrue(callbackCalled); }); + it("Doesn't clear the cache when canonicalKey doesn't change", function () { cache = new Plottable.Util.Cache(f, "x"); assert.equal(cache.get("hello"), "hellohello"); @@ -4103,6 +5085,7 @@ describe("Cache", function () { assert.equal(cache.get("hello"), "hellohello"); assert.isFalse(callbackCalled); }); + it("Clears the cache when canonicalKey changes", function () { var prefix = "hello"; cache = new Plottable.Util.Cache(function (s) { @@ -4117,9 +5100,12 @@ describe("Cache", function () { cache.get("world"); assert.isTrue(callbackCalled); }); + it("uses valueEq to check if it should clear", function () { var decider = true; - cache = new Plottable.Util.Cache(f, "x", function (a, b) { return decider; }); + cache = new Plottable.Util.Cache(f, "x", function (a, b) { + return decider; + }); cache.get("hello"); assert.isTrue(callbackCalled); cache.clear(); @@ -4133,6 +5119,7 @@ describe("Cache", function () { }); }); +/// var assert = chai.assert; describe("Util.Text", function () { it("getTruncatedText works properly", function () { @@ -4146,46 +5133,58 @@ describe("Util.Text", function () { assert.equal(partialText, "hello...", "text truncated"); var tinyText = Plottable.Util.Text.getTruncatedText("hellom world!", 5, measure); assert.equal(tinyText, "", "empty string for tiny text"); + svg.remove(); }); + describe("_addEllipsesToLine", function () { var svg; var measure; var e; var textSelection; + before(function () { svg = generateSVG(); textSelection = svg.append("text"); measure = Plottable.Util.Text.getTextMeasurer(textSelection); - e = function (text, width) { return Plottable.Util.Text._addEllipsesToLine(text, width, measure); }; + e = function (text, width) { + return Plottable.Util.Text._addEllipsesToLine(text, width, measure); + }; }); it("works on an empty string", function () { assert.equal(e("", 200), "...", "produced \"...\" with plenty of space"); }); + it("works as expected when given no width", function () { assert.equal(e("this wont fit", 0), "", "returned empty string when width is 0"); }); + it("works as expected when given only one periods worth of space", function () { var w = measure(".").width; assert.equal(e("this won't fit", w), ".", "returned a single period"); }); + it("works as expected with plenty of space", function () { assert.equal(e("this will fit", 400), "this will fit..."); }); + it("works as expected with insufficient space", function () { var w = measure("this won't fit").width; assert.equal(e("this won't fit", w), "this won't..."); }); + it("handles spaces intelligently", function () { var spacey = "this xx"; var w = measure(spacey).width - 1; assert.equal(e(spacey, w), "this..."); }); + after(function () { assert.lengthOf(svg.node().childNodes, 0, "this was all without side-effects"); svg.remove(); }); }); + describe("writeText", function () { it("behaves appropriately when there is too little height and width to fit any text", function () { var svg = generateSVG(); @@ -4197,6 +5196,7 @@ describe("Util.Text", function () { assert.isFalse(results.textFits, "measurement mode: text doesn't fit"); assert.equal(0, results.usedWidth, "measurement mode: no width used"); assert.equal(0, results.usedHeight, "measurement mode: no height used"); + var writeOptions = { g: svg, xAlign: "center", yAlign: "center" }; results = Plottable.Util.Text.writeText("hello world", width, height, measure, true, writeOptions); assert.isFalse(results.textFits, "write mode: text doesn't fit"); @@ -4206,6 +5206,7 @@ describe("Util.Text", function () { assert.lengthOf(svg.selectAll("text")[0], 0, "no text was written"); svg.remove(); }); + it("behaves appropriately when there is plenty of width but too little height to fit text", function () { var svg = generateSVG(); var width = 500; @@ -4216,6 +5217,7 @@ describe("Util.Text", function () { assert.isFalse(results.textFits, "measurement mode: text doesn't fit"); assert.equal(0, results.usedWidth, "measurement mode: no width used"); assert.equal(0, results.usedHeight, "measurement mode: no height used"); + var writeOptions = { g: svg, xAlign: "center", yAlign: "center" }; results = Plottable.Util.Text.writeText("hello world", width, height, measure, true, writeOptions); assert.isFalse(results.textFits, "write mode: text doesn't fit"); @@ -4226,11 +5228,13 @@ describe("Util.Text", function () { svg.remove(); }); }); + describe("getTextMeasurer", function () { var svg; var measurer; var canonicalBB; var canonicalResult; + before(function () { svg = generateSVG(200, 200); var t = svg.append("text"); @@ -4240,6 +5244,7 @@ describe("Util.Text", function () { t.text("bla bla bla"); measurer = Plottable.Util.Text.getTextMeasurer(t); }); + it("works on empty string", function () { var result = measurer(""); assert.deepEqual(result, { width: 0, height: 0 }, "empty string has 0 width and height"); @@ -4252,11 +5257,13 @@ describe("Util.Text", function () { svg.remove(); }); }); + describe("writeLine", function () { var svg; var g; var text = "hello world ARE YOU THERE?"; var hideResults = true; + describe("writeLineHorizontally", function () { it("writes no text if there is insufficient space", function () { svg = generateSVG(20, 20); @@ -4268,6 +5275,7 @@ describe("Util.Text", function () { assert.equal(g.text(), "", "no text written"); svg.remove(); }); + it("performs basic functionality and defaults to left, top", function () { svg = generateSVG(400, 400); g = svg.append("g"); @@ -4290,6 +5298,7 @@ describe("Util.Text", function () { var bb = Plottable.Util.DOM.getBBox(textEl); var x = bb.x + Plottable.Util.DOM.translate(g.select("g"))[0] + bb.width / 2; var y = bb.y + Plottable.Util.DOM.translate(g.select("g"))[1] + bb.height / 2; + if (hideResults) { svg.remove(); } @@ -4303,11 +5312,13 @@ describe("Util.Text", function () { var bb = Plottable.Util.DOM.getBBox(textEl); var x = bb.x + Plottable.Util.DOM.translate(g.select("g"))[0] + bb.width; var y = bb.y + Plottable.Util.DOM.translate(g.select("g"))[1] + bb.height; + if (hideResults) { svg.remove(); } ; }); + it("throws an error if there's too little space", function () { svg = generateSVG(20, 20); g = svg.append("g"); @@ -4317,12 +5328,14 @@ describe("Util.Text", function () { ; }); }); + describe("writeLineVertically", function () { it("performs basic functionality and defaults to right, left, top", function () { svg = generateSVG(60, 400); g = svg.append("g"); var wh = Plottable.Util.Text.writeLineVertically(text, g, 60, 400); var bb = Plottable.Util.DOM.getBBox(g.select("g")); + if (hideResults) { svg.remove(); } @@ -4358,6 +5371,7 @@ describe("Util.Text", function () { } ; }); + it("left, center, center", function () { svg = generateSVG(60, 400); g = svg.append("g"); @@ -4367,6 +5381,7 @@ describe("Util.Text", function () { } ; }); + it("left, right, bottom", function () { svg = generateSVG(60, 400); g = svg.append("g"); @@ -4380,13 +5395,16 @@ describe("Util.Text", function () { }); }); +/// var assert = chai.assert; + describe("Util.s", function () { it("inRange works correct", function () { assert.isTrue(Plottable.Util.Methods.inRange(0, -1, 1), "basic functionality works"); assert.isTrue(Plottable.Util.Methods.inRange(0, 0, 1), "it is a closed interval"); assert.isTrue(!Plottable.Util.Methods.inRange(0, 1, 2), "returns false when false"); }); + it("sortedIndex works properly", function () { var a = [1, 2, 3, 4, 5]; var si = Plottable.Util.OpenSource.sortedIndex; @@ -4394,24 +5412,34 @@ describe("Util.s", function () { assert.equal(si(6, a), a.length, "returns a.length when val >= arr[arr.length-1]"); assert.equal(si(1.5, a), 1, "returns 1 when val is between the first and second elements"); }); + it("accessorize works properly", function () { var datum = { "foo": 2, "bar": 3, "key": 4 }; - var f = function (d, i, m) { return d + i; }; + + var f = function (d, i, m) { + return d + i; + }; var a1 = Plottable.Util.Methods._accessorize(f); assert.equal(f, a1, "function passes through accessorize unchanged"); + var a2 = Plottable.Util.Methods._accessorize("key"); assert.equal(a2(datum, 0, null), 4, "key accessor works appropriately"); + var a3 = Plottable.Util.Methods._accessorize("#aaaa"); assert.equal(a3(datum, 0, null), "#aaaa", "strings beginning with # are returned as final value"); + var a4 = Plottable.Util.Methods._accessorize(33); assert.equal(a4(datum, 0, null), 33, "numbers are return as final value"); + var a5 = Plottable.Util.Methods._accessorize(datum); assert.equal(a5(datum, 0, null), datum, "objects are return as final value"); }); + it("uniq works as expected", function () { var strings = ["foo", "bar", "foo", "foo", "baz", "bam"]; assert.deepEqual(Plottable.Util.Methods.uniq(strings), ["foo", "bar", "baz", "bam"]); }); + it("objEq works as expected", function () { assert.isTrue(Plottable.Util.Methods.objEq({}, {})); assert.isTrue(Plottable.Util.Methods.objEq({ a: 5 }, { a: 5 })); @@ -4422,7 +5450,9 @@ describe("Util.s", function () { }); }); +/// var assert = chai.assert; + function makeFakeEvent(x, y) { return { dx: 0, @@ -4438,6 +5468,7 @@ function makeFakeEvent(x, y) { altKey: false }; } + function fakeDragSequence(anyedInteraction, startX, startY, endX, endY) { anyedInteraction._dragstart(); d3.event = makeFakeEvent(startX, startY); @@ -4447,19 +5478,26 @@ function fakeDragSequence(anyedInteraction, startX, startY, endX, endY) { anyedInteraction._dragend(); d3.event = null; } + describe("Interactions", function () { describe("PanZoomInteraction", 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.Scale.Linear().domain([0, 11]); var yScale = new Plottable.Scale.Linear().domain([11, 0]); + var svg = generateSVG(); var dataset = makeLinearSeries(11); var renderer = new Plottable.Plot.Scatter(dataset, xScale, yScale); renderer.renderTo(svg); + var xDomainBefore = xScale.domain(); var yDomainBefore = yScale.domain(); + var interaction = new Plottable.Interaction.PanZoom(renderer, xScale, yScale); interaction.registerWithComponent(); + var hb = renderer.element.select(".hit-box").node(); var dragDistancePixelX = 10; var dragDistancePixelY = 20; @@ -4467,23 +5505,30 @@ describe("Interactions", function () { dx: dragDistancePixelX, dy: dragDistancePixelY }); + var xDomainAfter = xScale.domain(); var yDomainAfter = yScale.domain(); + assert.notDeepEqual(xDomainAfter, xDomainBefore, "x domain was changed by panning"); assert.notDeepEqual(yDomainAfter, yDomainBefore, "y domain was changed by panning"); + function getSlope(scale) { var range = scale.range(); var domain = scale.domain(); return (domain[1] - domain[0]) / (range[1] - range[0]); } ; + var expectedXDragChange = -dragDistancePixelX * getSlope(xScale); var expectedYDragChange = -dragDistancePixelY * getSlope(yScale); + 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(); }); }); + describe("XYDragBoxInteraction", function () { var svgWidth = 400; var svgHeight = 400; @@ -4493,10 +5538,12 @@ describe("Interactions", function () { var yScale; var renderer; var interaction; + var dragstartX = 20; var dragstartY = svgHeight - 100; var dragendX = 100; var dragendY = svgHeight - 20; + before(function () { svg = generateSVG(svgWidth, svgHeight); dataset = new Plottable.DataSource(makeLinearSeries(10)); @@ -4507,31 +5554,41 @@ describe("Interactions", function () { interaction = new Plottable.Interaction.XYDragBox(renderer); interaction.registerWithComponent(); }); + afterEach(function () { - interaction.callback(); + interaction.dragstart(null); + interaction.drag(null); + interaction.dragend(null); interaction.clearBox(); }); - it("All callbacks are notified with appropriate data when a drag finishes", function () { + + it("All callbacks are notified with appropriate data on drag", function () { var timesCalled = 0; - var areaCallback = function (a) { + interaction.dragstart(function (a) { timesCalled++; - if (timesCalled === 1) { - assert.deepEqual(a, null, "areaCallback called with null arg on dragstart"); - } - if (timesCalled === 2) { - var expectedPixelArea = { - xMin: dragstartX, - xMax: dragendX, - yMin: dragstartY, - yMax: dragendY - }; - assert.deepEqual(a, expectedPixelArea, "areaCallback was passed the correct pixel area"); - } - }; - interaction.callback(areaCallback); + var expectedStartLocation = { x: dragstartX, y: dragstartY }; + assert.deepEqual(a, expectedStartLocation, "areaCallback called with null arg on dragstart"); + }); + interaction.dragend(function (a, b) { + timesCalled++; + var expectedStart = { + x: dragstartX, + y: dragstartY + }; + var expectedEnd = { + x: dragendX, + y: dragendY + }; + assert.deepEqual(a, expectedStart, "areaCallback was passed the correct starting point"); + assert.deepEqual(b, expectedEnd, "areaCallback was passed the correct ending point"); + }); + + // fake a drag event fakeDragSequence(interaction, dragstartX, dragstartY, dragendX, dragendY); - assert.equal(timesCalled, 2, "areaCallback was called twice"); + + assert.equal(timesCalled, 2, "drag callbacks are called twice"); }); + it("Highlights and un-highlights areas appropriately", function () { fakeDragSequence(interaction, dragstartX, dragstartY, dragendX, dragendY); var dragBoxClass = "." + Plottable.Interaction.XYDragBox.CLASS_DRAG_BOX; @@ -4542,14 +5599,17 @@ describe("Interactions", function () { assert.deepEqual(actualStartPosition, expectedStartPosition, "highlighted box is positioned correctly"); assert.equal(parseFloat(dragBox.attr("width")), Math.abs(dragstartX - dragendX), "highlighted box has correct width"); assert.equal(parseFloat(dragBox.attr("height")), Math.abs(dragstartY - dragendY), "highlighted box has correct height"); + interaction.clearBox(); var boxGone = dragBox.attr("width") === "0" && dragBox.attr("height") === "0"; assert.isTrue(boxGone, "highlighted box disappears when clearBox is called"); }); + after(function () { svg.remove(); }); }); + describe("YDragBoxInteraction", function () { var svgWidth = 400; var svgHeight = 400; @@ -4559,10 +5619,12 @@ describe("Interactions", function () { var yScale; var renderer; var interaction; + var dragstartX = 20; var dragstartY = svgHeight - 100; var dragendX = 100; var dragendY = svgHeight - 20; + before(function () { svg = generateSVG(svgWidth, svgHeight); dataset = new Plottable.DataSource(makeLinearSeries(10)); @@ -4573,29 +5635,35 @@ describe("Interactions", function () { interaction = new Plottable.Interaction.YDragBox(renderer); interaction.registerWithComponent(); }); + afterEach(function () { - interaction.callback(); + interaction.dragstart(null); + interaction.drag(null); + interaction.dragend(null); interaction.clearBox(); }); + it("All callbacks are notified with appropriate data when a drag finishes", function () { var timesCalled = 0; - var areaCallback = function (a) { + interaction.dragstart(function (a) { timesCalled++; - if (timesCalled === 1) { - assert.deepEqual(a, null, "areaCallback called with null arg on dragstart"); - } - if (timesCalled === 2) { - var expectedPixelArea = { - yMin: dragstartY, - yMax: dragendY - }; - assert.deepEqual(a, expectedPixelArea, "areaCallback was passed the correct pixel area"); - } - }; - interaction.callback(areaCallback); + var expectedY = dragstartY; + assert.deepEqual(a.y, expectedY, "areaCallback called with null arg on dragstart"); + }); + interaction.dragend(function (a, b) { + timesCalled++; + var expectedStartY = dragstartY; + var expectedEndY = dragendY; + assert.deepEqual(a.y, expectedStartY); + assert.deepEqual(b.y, expectedEndY); + }); + + // fake a drag event fakeDragSequence(interaction, dragstartX, dragstartY, dragendX, dragendY); - assert.equal(timesCalled, 2, "areaCallback was called twice"); + + assert.equal(timesCalled, 2, "drag callbacks area called twice"); }); + it("Highlights and un-highlights areas appropriately", function () { fakeDragSequence(interaction, dragstartX, dragstartY, dragendX, dragendY); var dragBoxClass = "." + Plottable.Interaction.XYDragBox.CLASS_DRAG_BOX; @@ -4606,46 +5674,64 @@ describe("Interactions", function () { assert.deepEqual(actualStartPosition, expectedStartPosition, "highlighted box is positioned correctly"); assert.equal(parseFloat(dragBox.attr("width")), svgWidth, "highlighted box has correct width"); assert.equal(parseFloat(dragBox.attr("height")), Math.abs(dragstartY - dragendY), "highlighted box has correct height"); + interaction.clearBox(); var boxGone = dragBox.attr("width") === "0" && dragBox.attr("height") === "0"; assert.isTrue(boxGone, "highlighted box disappears when clearBox is called"); }); + after(function () { 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); + + // svg.attr("id", "key-interaction-test"); var component = new Plottable.Abstract.Component(); component.renderTo(svg); + var code = 65; var ki = new Plottable.Interaction.Key(component, code); + var callbackCalled = false; var callback = function () { callbackCalled = true; }; + ki.callback(callback); ki.registerWithComponent(); + var $hitbox = $(component.hitBox.node()); + $hitbox.simulate("keydown", { keyCode: code }); assert.isFalse(callbackCalled, "callback is not called if component does not have mouse focus (before mouseover)"); + $hitbox.simulate("mouseover"); + $hitbox.simulate("keydown", { keyCode: code }); assert.isTrue(callbackCalled, "callback gets called if the appropriate key is pressed while the component has mouse focus"); + callbackCalled = false; $hitbox.simulate("keydown", { keyCode: (code + 1) }); assert.isFalse(callbackCalled, "callback is not called if the wrong key is pressed"); + $hitbox.simulate("mouseout"); + $hitbox.simulate("keydown", { keyCode: code }); assert.isFalse(callbackCalled, "callback is not called if component does not have mouse focus (after mouseout)"); + svg.remove(); }); }); + describe("BarHover", function () { var dataset; var ordinalScale; var linearScale; + before(function () { dataset = [ { name: "A", value: 3 }, @@ -4654,170 +5740,231 @@ describe("Interactions", function () { ordinalScale = new Plottable.Scale.Ordinal(); linearScale = new Plottable.Scale.Linear(); }); + it("hoverMode()", function () { var barPlot = new Plottable.Plot.VerticalBar(dataset, ordinalScale, linearScale); var bhi = new Plottable.Interaction.BarHover(barPlot); + bhi.hoverMode("line"); bhi.hoverMode("POINT"); - assert.throws(function () { return bhi.hoverMode("derp"); }, "not a valid"); + + assert.throws(function () { + return bhi.hoverMode("derp"); + }, "not a valid"); }); + it("correctly triggers callbacks (vertical)", function () { var svg = generateSVG(400, 400); var barPlot = new Plottable.Plot.VerticalBar(dataset, ordinalScale, linearScale); barPlot.project("x", "name", ordinalScale).project("y", "value", linearScale); var bhi = new Plottable.Interaction.BarHover(barPlot); + var barDatum = null; bhi.onHover(function (datum, bar) { barDatum = datum; }); + var unhoverCalled = false; bhi.onUnhover(function (datum, bar) { barDatum = datum; unhoverCalled = true; }); + barPlot.renderTo(svg); bhi.registerWithComponent(); + var hitbox = barPlot.element.select(".hit-box"); + triggerFakeMouseEvent("mousemove", hitbox, 100, 200); assert.deepEqual(barDatum, dataset[0], "the first bar was selected (point mode)"); barDatum = null; triggerFakeMouseEvent("mousemove", hitbox, 100, 201); assert.isNull(barDatum, "hover callback isn't called if the hovered bar didn't change"); + barDatum = null; triggerFakeMouseEvent("mousemove", hitbox, 10, 10); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing away from a bar"); assert.deepEqual(barDatum, dataset[0], "the unhover callback was passed the last-hovered bar"); + unhoverCalled = false; triggerFakeMouseEvent("mousemove", hitbox, 11, 11); assert.isFalse(unhoverCalled, "unhover callback isn't triggered multiple times in succession"); + triggerFakeMouseEvent("mousemove", hitbox, 100, 200); triggerFakeMouseEvent("mouseout", hitbox, 100, 9999); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing out of the chart"); + triggerFakeMouseEvent("mousemove", hitbox, 100, 200); unhoverCalled = false; triggerFakeMouseEvent("mousemove", hitbox, 250, 200); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing from one bar to another"); + bhi.hoverMode("line"); barDatum = null; triggerFakeMouseEvent("mousemove", hitbox, 100, 1); assert.deepEqual(barDatum, dataset[0], "the first bar was selected (line mode)"); + svg.remove(); }); + it("correctly triggers callbacks (hoizontal)", function () { var svg = generateSVG(400, 400); var barPlot = new Plottable.Plot.HorizontalBar(dataset, linearScale, ordinalScale); barPlot.project("y", "name", ordinalScale).project("x", "value", linearScale); var bhi = new Plottable.Interaction.BarHover(barPlot); + var barDatum = null; bhi.onHover(function (datum, bar) { barDatum = datum; }); + var unhoverCalled = false; bhi.onUnhover(function () { unhoverCalled = true; }); + barPlot.renderTo(svg); bhi.registerWithComponent(); + var hitbox = barPlot.element.select(".hit-box"); + triggerFakeMouseEvent("mousemove", hitbox, 200, 250); assert.deepEqual(barDatum, dataset[0], "the first bar was selected (point mode)"); barDatum = null; triggerFakeMouseEvent("mousemove", hitbox, 201, 250); assert.isNull(barDatum, "hover callback isn't called if the hovered bar didn't change"); + triggerFakeMouseEvent("mousemove", hitbox, 10, 10); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing away from a bar"); unhoverCalled = false; triggerFakeMouseEvent("mousemove", hitbox, 11, 11); assert.isFalse(unhoverCalled, "unhover callback isn't triggered multiple times in succession"); + triggerFakeMouseEvent("mousemove", hitbox, 200, 250); triggerFakeMouseEvent("mouseout", hitbox, -999, 250); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing out of the chart"); + triggerFakeMouseEvent("mousemove", hitbox, 200, 250); unhoverCalled = false; triggerFakeMouseEvent("mousemove", hitbox, 200, 100); assert.isTrue(unhoverCalled, "unhover callback is triggered on mousing from one bar to another"); + bhi.hoverMode("line"); triggerFakeMouseEvent("mousemove", hitbox, 399, 250); assert.deepEqual(barDatum, dataset[0], "the first bar was selected (line mode)"); + svg.remove(); }); }); }); +/// var assert = chai.assert; + describe("Dispatchers", function () { it("correctly registers for and deregisters from events", function () { var target = generateSVG(); + var dispatcher = new Plottable.Abstract.Dispatcher(target); var callbackWasCalled = false; dispatcher._event2Callback["click"] = function () { callbackWasCalled = true; }; + triggerFakeUIEvent("click", target); assert.isFalse(callbackWasCalled, "The callback is not called before the dispatcher connect()s"); + dispatcher.connect(); triggerFakeUIEvent("click", target); assert.isTrue(callbackWasCalled, "The dispatcher called its callback"); + callbackWasCalled = false; dispatcher.disconnect(); triggerFakeUIEvent("click", target); assert.isFalse(callbackWasCalled, "The callback is not called after the dispatcher disconnect()s"); + target.remove(); }); + it("target can be changed", function () { var target1 = generateSVG(); var target2 = generateSVG(); + var dispatcher = new Plottable.Abstract.Dispatcher(target1); var callbackWasCalled = false; - dispatcher._event2Callback["click"] = function () { return callbackWasCalled = true; }; + dispatcher._event2Callback["click"] = function () { + return callbackWasCalled = true; + }; + dispatcher.connect(); triggerFakeUIEvent("click", target1); assert.isTrue(callbackWasCalled, "The dispatcher received the event on the target"); + dispatcher.target(target2); callbackWasCalled = false; + triggerFakeUIEvent("click", target1); assert.isFalse(callbackWasCalled, "The dispatcher did not receive the event on the old target"); triggerFakeUIEvent("click", target2); assert.isTrue(callbackWasCalled, "The dispatcher received the event on the new target"); + target1.remove(); target2.remove(); }); + it("multiple dispatchers can be attached to the same target", function () { var target = generateSVG(); + var dispatcher1 = new Plottable.Abstract.Dispatcher(target); var called1 = false; - dispatcher1._event2Callback["click"] = function () { return called1 = true; }; + dispatcher1._event2Callback["click"] = function () { + return called1 = true; + }; dispatcher1.connect(); + var dispatcher2 = new Plottable.Abstract.Dispatcher(target); var called2 = false; - dispatcher2._event2Callback["click"] = function () { return called2 = true; }; + dispatcher2._event2Callback["click"] = function () { + return called2 = true; + }; dispatcher2.connect(); + triggerFakeUIEvent("click", target); assert.isTrue(called1, "The first dispatcher called its callback"); assert.isTrue(called2, "The second dispatcher also called its callback"); + target.remove(); }); + it("can't double-connect", function () { var target = generateSVG(); + var dispatcher = new Plottable.Abstract.Dispatcher(target); dispatcher.connect(); - assert.throws(function () { return dispatcher.connect(); }, "connect"); + assert.throws(function () { + return dispatcher.connect(); + }, "connect"); + target.remove(); }); + describe("Mouse Dispatcher", function () { it("passes event position to mouseover, mousemove, and mouseout callbacks", function () { var target = generateSVG(); + var targetX = 17; var targetY = 76; var expectedPoint = { x: targetX, y: targetY }; + function assertPointsClose(actual, expected, epsilon, message) { assert.closeTo(actual.x, expected.x, epsilon, message + " (x)"); assert.closeTo(actual.y, expected.y, epsilon, message + " (y)"); } ; + var md = new Plottable.Dispatcher.Mouse(target); var mouseoverCalled = false; md.mouseover(function (p) { @@ -4834,6 +5981,7 @@ describe("Dispatchers", function () { mouseoutCalled = true; assertPointsClose(p, expectedPoint, 0.5, "the mouse position was passed to the callback"); }); + md.connect(); triggerFakeMouseEvent("mouseover", target, targetX, targetY); assert.isTrue(mouseoverCalled, "mouseover callback was called"); @@ -4841,6 +5989,7 @@ describe("Dispatchers", function () { assert.isTrue(mousemoveCalled, "mousemove callback was called"); triggerFakeMouseEvent("mouseout", target, targetX, targetY); assert.isTrue(mouseoutCalled, "mouseout callback was called"); + target.remove(); }); });