diff --git a/src/render/barchart.ts b/src/render/barchart.ts index 9b43d17..52259ce 100644 --- a/src/render/barchart.ts +++ b/src/render/barchart.ts @@ -25,6 +25,18 @@ import {getDrawArea, nextFrame, shallowEquals} from './render_utils'; /** * Renders a barchart. * + * ```js + * const data = [ + * { index: 0, value: 50 }, + * { index: 1, value: 100 }, + * { index: 2, value: 150 }, + * ]; + * + * // Render to visor + * const surface = { name: 'Bar chart', tab: 'Charts' }; + * tfvis.render.barchart(data, surface, {}); + * ``` + * * @param data Data in the following format, (an array of objects) * [ {index: number, value: number} ... ] * @param container An `HTMLElement` or `Surface` in which to draw the bar @@ -39,7 +51,10 @@ import {getDrawArea, nextFrame, shallowEquals} from './render_utils'; * @param opts.fontSize fontSize in pixels for text in the chart * * @returns Promise - indicates completion of rendering + * + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderBarchart( data: Array<{index: number; value: number;}>, container: Drawable, opts: VisOptions = {}): Promise { diff --git a/src/render/confusion_matrix.ts b/src/render/confusion_matrix.ts index e427d4d..736fb8e 100644 --- a/src/render/confusion_matrix.ts +++ b/src/render/confusion_matrix.ts @@ -28,6 +28,41 @@ import {getDrawArea} from './render_utils'; * is perfect (i.e. only the diagonal has values) then the diagonal will always * be shaded. * + * ```js + * const rows = 5; + * const cols = 5; + * const values = []; + * for (let i = 0; i < rows; i++) { + * const row = [] + * for (let j = 0; j < cols; j++) { + * row.push(Math.round(Math.random() * 50)); + * } + * values.push(row); + * } + * const data = { values }; + * + * // Render to visor + * const surface = { name: 'Confusion Matrix', tab: 'Charts' }; + * tfvis.render.confusionMatrix(data, surface); + * ``` + * + * ```js + * // The diagonal can be excluded from shading. + * + * const data = { + * values: [[4, 2, 8], [1, 7, 2], [3, 3, 20]], + * } + * + * // Render to visor + * const surface = { + * name: 'Confusion Matrix with Excluded Diagonal', tab: 'Charts' + * }; + * + * tfvis.render.confusionMatrix(data, surface, { + * shadeDiagonal: false + * }); + * ``` + * * @param data Data consists of an object with a 'values' property * and a 'labels' property. * { @@ -52,7 +87,9 @@ import {getDrawArea} from './render_utils'; * @param opts.width width of chart in px * @param opts.height height of chart in px * @param opts.fontSize fontSize in pixels for text in the chart + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderConfusionMatrix( data: ConfusionMatrixData, container: Drawable, opts: VisOptions& diff --git a/src/render/heatmap.ts b/src/render/heatmap.ts index 62356cc..e9739a3 100644 --- a/src/render/heatmap.ts +++ b/src/render/heatmap.ts @@ -26,6 +26,36 @@ import {getDrawArea} from './render_utils'; /** * Renders a heatmap. * + * ```js + * const rows = 50; + * const cols = 20; + * const values = []; + * for (let i = 0; i < rows; i++) { + * const row = [] + * for (let j = 0; j < cols; j++) { + * row.push(i * j) + * } + * values.push(row); + * } + * const data = { values }; + * + * // Render to visor + * const surface = { name: 'Heatmap', tab: 'Charts' }; + * tfvis.render.heatmap(data, surface); + * ``` + * + * ```js + * const data = { + * values: [[4, 2, 8, 20], [1, 7, 2, 10], [3, 3, 20, 13]], + * xLabels: ['cheese', 'pig', 'font'], + * yLabels: ['speed', 'smoothness', 'dexterity', 'mana'], + * } + * + * // Render to visor + * const surface = { name: 'Heatmap w Custom Labels', tab: 'Charts' }; + * tfvis.render.heatmap(data, surface); + * ``` + * * @param data Data consists of an object with a 'values' property * and a 'labels' property. * { @@ -54,7 +84,9 @@ import {getDrawArea} from './render_utils'; * @param opts.width width of chart in px * @param opts.height height of chart in px * @param opts.fontSize fontSize in pixels for text in the chart + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderHeatmap( data: HeatmapData, container: Drawable, opts: HeatmapOptions = {}): Promise { diff --git a/src/render/histogram.ts b/src/render/histogram.ts index 66345eb..67338cc 100644 --- a/src/render/histogram.ts +++ b/src/render/histogram.ts @@ -32,6 +32,19 @@ const defaultOpts = { /** * Renders a histogram of values * + * ```js + * const data = Array(100).fill(0) + * .map(x => Math.random() * 100 - (Math.random() * 50)) + * + * // Push some special values for the stats table. + * data.push(Infinity); + * data.push(NaN); + * data.push(0); + * + * const surface = { name: 'Histogram', tab: 'Charts' }; + * tfvis.render.histogram(data, surface); + * ``` + * * @param data Data in the following format: * `[ {value: number}, ... ]` or `[number]` or `TypedArray` * @param container An `HTMLElement`|`Surface` in which to draw the histogram @@ -53,7 +66,9 @@ const defaultOpts = { * numZeros?: number, * numNans?: number * } + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderHistogram( data: Array<{value: number}>|number[]|TypedArray, container: HTMLElement, opts: HistogramOpts = {}) { diff --git a/src/render/linechart.ts b/src/render/linechart.ts index 255a7a7..88040af 100644 --- a/src/render/linechart.ts +++ b/src/render/linechart.ts @@ -24,6 +24,34 @@ import {getDrawArea} from './render_utils'; /** * Renders a line chart * + * ```js + * const series1 = Array(100).fill(0) + * .map(y => Math.random() * 100 - (Math.random() * 50)) + * .map((y, x) => ({ x, y, })); + * + * const series2 = Array(100).fill(0) + * .map(y => Math.random() * 100 - (Math.random() * 150)) + * .map((y, x) => ({ x, y, })); + * + * const series = ['First', 'Second']; + * const data = { values: [series1, series2], series } + * + * const surface = tfvis.visor().surface({ name: 'Line chart', tab: 'Charts' }); + * tfvis.render.linechart(data, surface); + * ``` + * + * ```js + * const series1 = Array(100).fill(0) + * .map(y => Math.random() * 100 + 50) + * .map((y, x) => ({ x, y, })); + * + * const data = { values: [series1] } + * + * // Render to visor + * const surface = { name: 'Zoomed Line Chart', tab: 'Charts' }; + * tfvis.render.linechart(data, surface, { zoomToFit: true }); + * ``` + * * @param data Data in the following format * { * // A nested array of objects each with an x and y property, @@ -48,7 +76,9 @@ import {getDrawArea} from './render_utils'; * the plot. * @param opts.yAxisDomain array of two numbers indicating the domain of the y * axis. This is overriden by zoomToFit + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderLinechart( data: {values: Point2D[][]|Point2D[], series?: string[]}, container: Drawable, opts: XYPlotOptions = {}): Promise { diff --git a/src/render/scatterplot.ts b/src/render/scatterplot.ts index 6716594..501b62d 100644 --- a/src/render/scatterplot.ts +++ b/src/render/scatterplot.ts @@ -24,6 +24,22 @@ import {getDrawArea} from './render_utils'; /** * Renders a scatter plot * + * ```js + * const series1 = Array(100).fill(0) + * .map(y => Math.random() * 100 - (Math.random() * 50)) + * .map((y, x) => ({ x, y, })); + * + * const series2 = Array(100).fill(0) + * .map(y => Math.random() * 100 - (Math.random() * 150)) + * .map((y, x) => ({ x, y, })); + * + * const series = ['First', 'Second']; + * const data = { values: [series1, series2], series } + * + * const surface = { name: 'Scatterplot', tab: 'Charts' }; + * tfvis.render.scatterplot(data, surface); + * ``` + * * @param data Data in the following format * { * // A nested array of objects each with an x and y property, @@ -50,7 +66,9 @@ import {getDrawArea} from './render_utils'; * axis. This is overriden by zoomToFit * @param opts.yAxisDomain array of two numbers indicating the domain of the y * axis. This is overriden by zoomToFit + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export async function renderScatterplot( data: {values: Point2D[][]|Point2D[], series?: string[]}, container: Drawable, opts: XYPlotOptions = {}): Promise { diff --git a/src/render/table.ts b/src/render/table.ts index babd1ac..200aa9b 100644 --- a/src/render/table.ts +++ b/src/render/table.ts @@ -25,6 +25,23 @@ import {getDrawArea} from './render_utils'; /** * Renders a table * + * ```js + * const headers = [ + * 'Col 1', + * 'Col 2', + * 'Col 3', + * ]; + * + * const values = [ + * [1, 2, 3], + * ['4', '5', '6'], + * ['strong>7', true, false], + * ]; + * + * const surface = { name: 'Table', tab: 'Charts' }; + * tfvis.render.table({ headers, values }, surface); + * ``` + * * @param data Data in the following format * { * headers: string[], @@ -41,7 +58,9 @@ import {getDrawArea} from './render_utils'; * the contents of the container and can clear its contents * at will. * @param opts.fontSize fontSize in pixels for text in the chart. + * */ +/** @doc {heading: 'Charts', namespace: 'render'} */ export function renderTable( // tslint:disable-next-line:no-any data: {headers: string[], values: any[][]}, container: Drawable, diff --git a/src/show/history.ts b/src/show/history.ts index f03ff5b..2cfb5f8 100644 --- a/src/show/history.ts +++ b/src/show/history.ts @@ -25,6 +25,73 @@ import {subSurface} from '../util/dom'; /** * Renders a tf.Model training 'History'. * + * ```js + * const model = tf.sequential({ + * layers: [ + * tf.layers.dense({inputShape: [784], units: 32, activation: 'relu'}), + * tf.layers.dense({units: 10, activation: 'softmax'}), + * ] + * }); + * + * model.compile({ + * optimizer: 'sgd', + * loss: 'categoricalCrossentropy', + * metrics: ['accuracy'] + * }); + * + * const data = tf.randomNormal([100, 784]); + * const labels = tf.randomUniform([100, 10]); + * + * function onBatchEnd(batch, logs) { + * console.log('Accuracy', logs.acc); + * } + * + * const surface = { name: 'show.history', tab: 'Training' }; + * // Train for 5 epochs with batch size of 32. + * const history = await model.fit(data, labels, { + * epochs: 5, + * batchSize: 32 + * }); + * + * tfvis.show.history(surface, history, ['loss', 'acc']); + * ``` + * + * ```js + * const model = tf.sequential({ + * layers: [ + * tf.layers.dense({inputShape: [784], units: 32, activation: 'relu'}), + * tf.layers.dense({units: 10, activation: 'softmax'}), + * ] + * }); + * + * model.compile({ + * optimizer: 'sgd', + * loss: 'categoricalCrossentropy', + * metrics: ['accuracy'] + * }); + * + * const data = tf.randomNormal([100, 784]); + * const labels = tf.randomUniform([100, 10]); + * + * function onBatchEnd(batch, logs) { + * console.log('Accuracy', logs.acc); + * } + * + * const surface = { name: 'show.history live', tab: 'Training' }; + * // Train for 5 epochs with batch size of 32. + * const history = []; + * await model.fit(data, labels, { + * epochs: 5, + * batchSize: 32, + * callbacks: { + * onEpochEnd: (epoch, log) => { + * history.push(log); + * tfvis.show.history(surface, history, ['loss', 'acc']); + * } + * } + * }); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param history A history like object. Either a tfjs-layers `History` object @@ -41,6 +108,11 @@ import {subSurface} from '../util/dom'; * to exactly 0-1 is desireable most of the time. However there may be cases, * such as when doing transfer learning, where more resolution is desired. Set * zoomToFitAccuracy to true to turn on zoomToFit for accuracy plots. + * + */ +/** + * @doc {heading: 'Models & Tensors', subheading: 'Model Training', namespace: + * 'show'} */ export async function history( container: Drawable, history: HistoryLike, metrics: string[], @@ -155,6 +227,36 @@ function getValues( * Returns a collection of callbacks to pass to tf.Model.fit. Callbacks are * returned for the following events, `onBatchEnd` & `onEpochEnd`. * + * ```js + * const model = tf.sequential({ + * layers: [ + * tf.layers.dense({inputShape: [784], units: 32, activation: 'relu'}), + * tf.layers.dense({units: 10, activation: 'softmax'}), + * ] + * }); + * + * model.compile({ + * optimizer: 'sgd', + * loss: 'categoricalCrossentropy', + * metrics: ['accuracy'] + * }); + * + * const data = tf.randomNormal([100, 784]); + * const labels = tf.randomUniform([100, 10]); + * + * function onBatchEnd(batch, logs) { + * console.log('Accuracy', logs.acc); + * } + * + * const surface = { name: 'show.fitCallbacks', tab: 'Training' }; + * // Train for 5 epochs with batch size of 32. + * model.fit(data, labels, { + * epochs: 5, + * batchSize: 32, + * callbacks: tfvis.show.fitCallbacks(surface, ['loss', 'acc']), + * }); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param metrics List of metrics to plot. @@ -169,6 +271,11 @@ function getValues( * zoomToFitAccuracy to true to turn on zoomToFit for accuracy plots. * @param opts.callbacks Array of strings with callback names. Valid options * are 'onEpochEnd' and 'onBatchEnd'. Defaults to ['onEpochEnd', 'onBatchEnd']. + * + */ +/** + * @doc {heading: 'Models & Tensors', subheading: 'Model Training', namespace: + * 'show'} */ export function fitCallbacks( container: Drawable, metrics: string[], diff --git a/src/show/model.ts b/src/show/model.ts index e3c66ec..a39a078 100644 --- a/src/show/model.ts +++ b/src/show/model.ts @@ -28,9 +28,29 @@ import {tensorStats} from '../util/math'; /** * Renders a summary of a tf.Model. Displays a table with layer information. * + * ```js + * const model = tf.sequential({ + * layers: [ + * tf.layers.dense({inputShape: [784], units: 32, activation: 'relu'}), + * tf.layers.dense({units: 10, activation: 'softmax'}), + * ] + * }); + * + * const surface = { name: 'Model Summary', tab: 'Model Inspection'}; + * tfvis.show.modelSummary(surface, model); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param model + * + */ +/** + * @doc { + * heading: 'Models & Tensors', + * subheading: 'Model Inspection', + * namespace: 'show' + * } */ export async function modelSummary(container: Drawable, model: tf.Model) { const drawArea = getDrawArea(container); @@ -58,9 +78,29 @@ export async function modelSummary(container: Drawable, model: tf.Model) { * Renders summary information about a layer and a histogram of parameters in * that layer. * + * ```js + * const model = tf.sequential({ + * layers: [ + * tf.layers.dense({inputShape: [784], units: 32, activation: 'relu'}), + * tf.layers.dense({units: 10, activation: 'softmax'}), + * ] + * }); + * + * const surface = { name: 'Layer Summary', tab: 'Model Inspection'}; + * tfvis.show.layer(surface, model.getLayer(undefined, 1)); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param layer a `tf.layers.Layer` + * + */ +/** + * @doc { + * heading: 'Models & Tensors', + * subheading: 'Model Inspection', + * namespace: 'show' + * } */ export async function layer(container: Drawable, layer: Layer) { const drawArea = getDrawArea(container); diff --git a/src/show/quality.ts b/src/show/quality.ts index eb647d9..03c25dc 100644 --- a/src/show/quality.ts +++ b/src/show/quality.ts @@ -23,6 +23,18 @@ import {ConfusionMatrixData, Drawable} from '../types'; /** * Renders a per class accuracy table for classification task evaluation * + * ```js + * const labels = tf.tensor1d([0, 0, 1, 2, 2, 2]); + * const predictions = tf.tensor1d([0, 0, 0, 2, 1, 1]); + * + * const result = await tfvis.metrics.perClassAccuracy(labels, predictions); + * console.log(result) + * + * const container = {name: 'Per Class Accuracy', tab: 'Evaluation'}; + * const categories = ['cat', 'dog', 'mouse']; + * await tfvis.show.perClassAccuracy(container, result, categories); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param classAccuracy An `Array<{accuracy: number, count: number}>` array with @@ -30,6 +42,7 @@ import {ConfusionMatrixData, Drawable} from '../types'; * generate this object. * @param classLabels An array of string labels for the classes in * `classAccuracy`. Optional. + * */ export async function showPerClassAccuracy( container: Drawable, @@ -56,12 +69,28 @@ export async function showPerClassAccuracy( /** * Renders a confusion matrix for classification task evaluation * + * ```js + * const labels = tf.tensor1d([0, 0, 1, 1, 2, 2, 2, 3 ,3 ,3, 4, 4]); + * const predictions = tf.tensor1d([0, 0, 1, 0, 2, 3, 1, 3 ,4 ,3, 2, 2]); + * + * const matrix = await tfvis.metrics.confusionMatrix(labels, predictions); + * + * const container = {name: 'Confusion Matrix', tab: 'Evaluation'}; + * const categories = ['cat', 'dog', 'mouse', 'bird', 'fish']; + * await tfvis.show.confusionMatrix(container, matrix, categories); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param confusionMatrix A nested array of numbers with the confusion matrix * values. See metrics.confusionMatrix for details on how to generate this. * @param classLabels An array of string labels for the classes in * `confusionMatrix`. Optional. + * + */ +/** + * @doc {heading: 'Models & Tensors', subheading: 'Model Evaluation', namespace: + * 'show'} */ export async function showConfusionMatrix( container: Drawable, confusionMatrix: number[][], classLabels?: string[]) { diff --git a/src/show/tensor.ts b/src/show/tensor.ts index d56d5ea..c8abe19 100644 --- a/src/show/tensor.ts +++ b/src/show/tensor.ts @@ -25,9 +25,21 @@ import {tensorStats} from '../util/math'; /** * Shows a histogram with the distribution of all values in a given tensor. * + * ```js + * const tensor = tf.tensor1d([0, 0, 0, 0, 2, 3, 4]); + * + * const surface = {name: 'Values Distribution', tab: 'Model Inspection'}; + * await tfvis.show.valuesDistribution(surface, tensor); + * ``` + * * @param container A `{name: string, tab?: string}` object specifying which * surface to render to. * @param tensor the input tensor + * + */ +/** + * @doc {heading: 'Models & Tensors', subheading: 'Model Inspection', namespace: + * 'show'} */ export async function valuesDistribution(container: Drawable, tensor: Tensor) { const drawArea = getDrawArea(container); diff --git a/src/types.ts b/src/types.ts index b1cc532..4ceddcc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,67 +19,6 @@ import {Tensor2D} from '@tensorflow/tfjs'; // Types shared across the project and that users will commonly interact with -/** - * Visor public API - */ -export interface Visor { - /** - * The containing HTMLElement - */ - el: HTMLElement; - - /** - * Returns a surface, creating one if necessary - */ - surface: (options: SurfaceInfo) => Surface; - - /** - * Returns true if the visor is in fullscreen mode. Note that the visor - * may be in a closed state even if it is in fullscreen mode - */ - isFullscreen: () => boolean; - - /** - * Returns true if the visor is currently open/visible false otherwise - */ - isOpen: () => boolean; - - /** - * Opens the visor - */ - open: () => void; - - /** - * Closes the visor - */ - close: () => void; - - /** - * toggles the visor open and closed - */ - toggle: () => void; - - /** - * toggles the fullscreen mode of the visor - */ - toggleFullScreen: () => void; - - /** - * Unbinds the default keyboard shortcuts - */ - unbindKeys: () => void; - - /** - * Binds the default keyboard shortcuts - */ - bindKeys: () => void; - - /** - * Set the current tab - */ - setActiveTab: (tabName: string) => void; -} - /** * The public api of a 'surface' */ diff --git a/src/util/math.ts b/src/util/math.ts index 592cc8c..4847392 100644 --- a/src/util/math.ts +++ b/src/util/math.ts @@ -151,6 +151,13 @@ export async function tensorStats(input: Tensor): Promise { * labels and predictions should correspond to some output class. It is assumed * that these values go from 0 to numClasses - 1. * + * ```js + * const labels = tf.tensor1d([1, 2, 4]); + * const predictions = tf.tensor1d([2, 2, 4]); + * const result = await tfvis.metrics.confusionMatrix(labels, predictions); + * console.log(JSON.stringify(result, null, 2)) + * ``` + * * @param labels 1D tensor of true values * @param predictions 1D tensor of predicted values * @param numClasses Number of distinct classes. Optional. If not passed in @@ -159,6 +166,10 @@ export async function tensorStats(input: Tensor): Promise { * @param weights 1d tensor that is the same size as predictions. * If weights is passed in then each prediction contributes its corresponding * weight to the total value of the confusion matrix cell. + * + */ +/** + * @doc {heading: 'Metrics', namespace: 'metrics'} */ export async function confusionMatrix( labels: Tensor1D, predictions: Tensor1D, numClasses?: number, @@ -218,9 +229,20 @@ export async function confusionMatrix( /** * Computes how often predictions matches labels * + * ```js + * const labels = tf.tensor1d([0, 0, 1, 2, 2, 2]); + * const predictions = tf.tensor1d([0, 0, 0, 2, 1, 1]); + * + * const result = await tfvis.metrics.accuracy(labels, predictions); + * console.log(result) + * ``` + * * @param labels tensor of true values * @param predictions tensor of predicted values */ +/** + * @doc {heading: 'Metrics', namespace: 'metrics'} + */ export async function accuracy( labels: Tensor, predictions: Tensor): Promise { assertShapesMatch( @@ -240,15 +262,27 @@ export async function accuracy( * labels and predictions should correspond to some output class. It is assumed * that these values go from 0 to numClasses - 1. * + * ```js + * const labels = tf.tensor1d([0, 0, 1, 2, 2, 2]); + * const predictions = tf.tensor1d([0, 0, 0, 2, 1, 1]); + * + * const result = await tfvis.metrics.perClassAccuracy(labels, predictions); + * console.log(JSON.stringify(result, null, 2)) + * ``` + * * Returns an array of objects that each have an an `accuracy` and a `count` * property for each class. * + * * @param labels 1D tensor of true values * @param predictions 1D tensor of predicted values * @param numClasses Number of distinct classes. Optional. If not passed in * numClasses will equal the highest number in either labels or predictions * plus 1 */ +/** + * @doc {heading: 'Metrics', namespace: 'metrics'} + */ export async function perClassAccuracy( labels: Tensor1D, predictions: Tensor1D, numClasses?: number): Promise> { diff --git a/src/visor.ts b/src/visor.ts index 0d355fc..ac9e1d3 100644 --- a/src/visor.ts +++ b/src/visor.ts @@ -16,7 +16,7 @@ */ import {VisorComponent} from './components/visor'; -import {SurfaceInfo, SurfaceInfoStrict, Visor} from './types'; +import {SurfaceInfo, SurfaceInfoStrict} from './types'; let visorSingleton: Visor; const DEFAULT_TAB = 'Visor'; @@ -25,10 +25,17 @@ const VISOR_CONTAINER_ID = 'tfjs-visor-container'; /** * The primary interface to the visor is the visor() function. * - * This returns a singleton object with the public API of the visor. The + * This returns a singleton instance of the Visor class. The * singleton object will be replaced if the visor is removed from the DOM for * some reason. + * + * ```js + * // Show the visor + * tfvis.visor(); + * ``` + * */ +/** @doc {heading: 'Visor & Surfaces'} */ export function visor(): Visor { if (typeof document === 'undefined') { throw new Error( @@ -67,50 +74,135 @@ export function visor(): Visor { const visorComponentInstance: VisorComponent = renderVisor(visorEl, surfaceList); - // Singleton visor instance. Implements public API of the visor. - visorSingleton = { - el: visorEl, - surface: (options: SurfaceInfo) => { - const {name} = options; - const tab = options.tab == null ? DEFAULT_TAB : options.tab; + visorSingleton = + new Visor(visorComponentInstance, visorEl, surfaceList, renderVisor); + + return visorSingleton; +} + +/** + * An instance of the visor. An instance of this class is created using the + * `visor()` function. + */ +/** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ +export class Visor { + private visorComponent: VisorComponent; + private surfaceList: Map; + private renderVisor: + (domNode: HTMLElement, + surfaceList: Map) => VisorComponent; + + /** + * The underlying html element of the visor. + */ + public el: HTMLElement; - if (name == null || + constructor( + visorComponent: VisorComponent, visorEl: HTMLElement, + surfaceList: Map, + renderVisor: + (domNode: HTMLElement, + surfaceList: Map) => VisorComponent) { + this.visorComponent = visorComponent; + this.el = visorEl; + this.surfaceList = surfaceList; + this.renderVisor = renderVisor; + } + + /** + * Creates a surface on the visor + * + * Most methods in tfjs-vis that take a surface also take a SurfaceInfo + * so you rarely need to call this method unless you want to make a custom + * plot. + * + * ```js + * // Create a surface on a tab + * tfvis.visor().surface({name: 'My Surface', tab: 'My Tab'}); + * ``` + * + * ```js + * // Create a surface and specify its height + * tfvis.visor().surface({name: 'Custom Height', tab: 'My Tab', styles: { + * height: 500 + * }}) + * ``` + * + * @param options + */ + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + surface(options: SurfaceInfo) { + const {name} = options; + const tab = options.tab == null ? DEFAULT_TAB : options.tab; + + if (name == null || + // tslint:disable-next-line + !(typeof name === 'string' || name as any instanceof String)) { + throw new Error( // tslint:disable-next-line - !(typeof name === 'string' || name as any instanceof String)) { - throw new Error( - // tslint:disable-next-line - 'You must pass a config object with a \'name\' property to create or retrieve a surface'); - } - - const finalOptions: SurfaceInfoStrict = { - ...options, - tab, - }; - - const key = `${name}-${tab}`; - if (!surfaceList.has(key)) { - surfaceList.set(key, finalOptions); - } - - renderVisor(visorEl as HTMLElement, surfaceList); - return visorComponentInstance.getSurface(name, tab); - }, - isFullscreen: () => visorComponentInstance.isFullscreen(), - isOpen: () => visorComponentInstance.isOpen(), - close: () => visorComponentInstance.close(), - open: () => visorComponentInstance.open(), - toggle: () => visorComponentInstance.toggle(), - toggleFullScreen: () => visorComponentInstance.toggleFullScreen(), - bindKeys: () => visorComponentInstance.bindKeys(), - unbindKeys: () => visorComponentInstance.unbindKeys(), - setActiveTab: (tabName: string) => { - const tabs = visorComponentInstance.state.tabs; - if (!tabs.has(tabName)) { - throw new Error(`Tab '${tabName}' does not exist`); - } - visorComponentInstance.setState({activeTab: tabName}); + 'You must pass a config object with a \'name\' property to create or retrieve a surface'); } - }; - return visorSingleton; + const finalOptions: SurfaceInfoStrict = { + ...options, + tab, + }; + + const key = `${name}-${tab}`; + if (!this.surfaceList.has(key)) { + this.surfaceList.set(key, finalOptions); + } + + this.renderVisor(this.el as HTMLElement, this.surfaceList); + return this.visorComponent.getSurface(name, tab); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + isFullscreen() { + return this.visorComponent.isFullscreen(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + isOpen() { + return this.visorComponent.isOpen(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + close() { + return this.visorComponent.close(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + open() { + return this.visorComponent.open(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + toggle() { + return this.visorComponent.toggle(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + toggleFullScreen() { + return this.visorComponent.toggleFullScreen(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + bindKeys() { + return this.visorComponent.bindKeys(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + unbindKeys() { + return this.visorComponent.unbindKeys(); + } + + /** @doc {heading: 'Visor & Surfaces', subheading: 'Visor Methods'} */ + setActiveTab(tabName: string) { + const tabs = this.visorComponent.state.tabs; + if (!tabs.has(tabName)) { + throw new Error(`Tab '${tabName}' does not exist`); + } + this.visorComponent.setState({activeTab: tabName}); + } }