From aeedd839c8a698eb206c3f38c35ddbde1fbeb2aa Mon Sep 17 00:00:00 2001 From: Mihai Budiu Date: Thu, 13 Aug 2020 16:29:09 -0700 Subject: [PATCH] Bookmark reconstruction (#666) --- .gitignore | 1 + bookmark/README.md | 1 + uiconfig.json | 3 +- web/src/main/webapp/dataViews/axisData.ts | 36 ++++-- web/src/main/webapp/dataViews/chartView.ts | 31 ++--- .../webapp/dataViews/dataRangesReceiver.ts | 111 +++++++++--------- web/src/main/webapp/dataViews/heatmapView.ts | 35 +++--- .../main/webapp/dataViews/histogram2DView.ts | 14 ++- .../main/webapp/dataViews/histogramView.ts | 11 +- .../dataViews/quartilesHistogramView.ts | 24 +++- web/src/main/webapp/dataViews/tableView.ts | 37 +++--- .../webapp/dataViews/trellisHeatmapView.ts | 17 ++- .../dataViews/trellisHistogram2DView.ts | 17 ++- .../trellisHistogramQuartilesView.ts | 16 ++- .../webapp/dataViews/trellisHistogramView.ts | 18 +-- web/src/main/webapp/dataViews/tsViewBase.ts | 4 +- web/src/main/webapp/datasetView.ts | 63 ++++++---- web/src/main/webapp/index.html | 2 +- web/src/main/webapp/initialObject.ts | 2 +- web/src/main/webapp/javaBridge.ts | 1 + web/src/main/webapp/loadMenu.ts | 10 +- web/src/main/webapp/schemaClass.ts | 16 +-- web/src/main/webapp/tableTarget.ts | 59 ++++++---- web/src/main/webapp/ui/dialog.ts | 2 +- web/src/main/webapp/ui/fullPage.ts | 13 +- web/src/main/webapp/ui/heatmapLegendPlot.ts | 4 +- web/src/main/webapp/ui/heatmapPlot.ts | 12 +- web/src/main/webapp/ui/histogramLegendPlot.ts | 4 +- web/src/main/webapp/ui/menu.ts | 37 +++--- web/src/main/webapp/ui/plottingSurface.ts | 10 +- web/src/main/webapp/util.ts | 63 +++++++--- 31 files changed, 404 insertions(+), 270 deletions(-) create mode 100644 bookmark/README.md diff --git a/.gitignore b/.gitignore index 4269a93a5..ed1721cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__ *.ear target/ +bookmark/*.json # Idea files # see https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems diff --git a/bookmark/README.md b/bookmark/README.md new file mode 100644 index 000000000..7789fef88 --- /dev/null +++ b/bookmark/README.md @@ -0,0 +1 @@ +This directory will contain views which are bookmarked when running on the local machine. diff --git a/uiconfig.json b/uiconfig.json index d7168363b..11ac3c445 100644 --- a/uiconfig.json +++ b/uiconfig.json @@ -3,5 +3,6 @@ "localDbMenu": true, "showTestMenu": true, "enableManagement": true, - "privateIsCsv": true + "privateIsCsv": true, + "hideSuggestions": true } diff --git a/web/src/main/webapp/dataViews/axisData.ts b/web/src/main/webapp/dataViews/axisData.ts index 6f5a5e6f1..554be76ca 100644 --- a/web/src/main/webapp/dataViews/axisData.ts +++ b/web/src/main/webapp/dataViews/axisData.ts @@ -26,7 +26,7 @@ import { RowValue } from "../javaBridge"; import { - assert, + assert, assertNever, binarySearch, Converters, formatDate, @@ -157,15 +157,16 @@ export class AxisDescription { } // This value indicates that some data does not fall within a bucket. -export const NoBucketIndex: number = null; +export const NoBucketIndex: number | null = null; /** * Contains all information required to build an axis and a d3 scale associated to it. */ export class AxisData { - public scale: AnyScale; - public axis: AxisDescription; - public displayRange: BucketsInfo; // the range used to draw the data; may be adjusted from this.dataRange + // The following are set only when the resolution is set. + public scale: AnyScale | null; + public axis: AxisDescription | null; + public displayRange: BucketsInfo | null; // the range used to draw the data; may be adjusted from this.dataRange public constructor(public description: IColumnDescription | null, // may be null for e.g., the Y col in a histogram // dataRange is the original range of the data @@ -174,10 +175,10 @@ export class AxisData { this.displayRange = dataRange; const kind = description == null ? null : description.kind; if (dataRange != null) { - if (kindIsString(kind)) { + if (kindIsString(kind!)) { this.displayRange = { min: -.5, - max: dataRange.stringQuantiles.length - .5, + max: dataRange.stringQuantiles!.length - .5, presentCount: dataRange.presentCount, missingCount: dataRange.missingCount, allStringsKnown: dataRange.allStringsKnown, @@ -186,8 +187,8 @@ export class AxisData { }; } else if (kind === "Integer") { this.displayRange = { - min: dataRange.min - .5, - max: dataRange.max + .5, + min: dataRange.min! - .5, + max: dataRange.max! + .5, presentCount: dataRange.presentCount, missingCount: dataRange.missingCount }; @@ -198,7 +199,9 @@ export class AxisData { this.axis = null; } - public getDisplayNameString(schema: SchemaClass): string { + public getDisplayNameString(schema: SchemaClass): string | null { + if (this.description == null) + return null; return schema.displayName(this.description.name).displayName; } @@ -333,10 +336,12 @@ export class AxisData { this.axis = new AxisDescription(axisCreator(this.scale), 1, false, null); break; } - default: { - console.log("Unexpected data kind for axis" + this.description.kind); + case "Duration": { + // TODO break; } + default: + assertNever(this.description.kind); } } @@ -461,13 +466,18 @@ export class AxisData { case "Double": case "Date": case "Time": + case "Duration": case "LocalDate": return new BucketBoundaries( new BucketBoundary(start, valueKind, true), new BucketBoundary(end, valueKind, inclusive) ); + case "String": + case "Json": + // handled above + return null; default: - assert(false, "Unhandled data type " + valueKind); + assertNever(valueKind); } } diff --git a/web/src/main/webapp/dataViews/chartView.ts b/web/src/main/webapp/dataViews/chartView.ts index dae3a6270..a02678dfd 100644 --- a/web/src/main/webapp/dataViews/chartView.ts +++ b/web/src/main/webapp/dataViews/chartView.ts @@ -28,6 +28,7 @@ import {event as d3event, mouse as d3mouse} from "d3-selection"; import {AxisData} from "./axisData"; import {Dialog} from "../ui/dialog"; import {NextKReceiver, TableView} from "../modules"; +import { assert } from "../util"; /** * A ChartView is a common base class for many views that @@ -45,7 +46,7 @@ export abstract class ChartView extends BigTableView { /** * Coordinates of mouse within canvas. */ - protected selectionOrigin: Point; + protected selectionOrigin: Point | null; /** * Rectangle in canvas used to display the current selection. */ @@ -53,12 +54,12 @@ export abstract class ChartView extends BigTableView { /** * Describes the data currently pointed by the mouse. */ - protected pointDescription: TextOverlay; + protected pointDescription: TextOverlay | null; /** * The main surface on top of which the image is drawn. * There may exist other surfaces as well besides this one. */ - protected surface: PlottingSurface; + protected surface: PlottingSurface | null; /** * Top-level menu. */ @@ -97,7 +98,7 @@ export abstract class ChartView extends BigTableView { const order = new RecordOrder( columns.map(c => { return { columnDescription: c, isAscending: true }})); - const rr = table.createNextKRequest(order, null, Resolution.tableRowsOnScreen); + const rr = table.createNextKRequest(order, null, Resolution.tableRowsOnScreen, null, null); rr.invoke(new NextKReceiver(newPage, table, rr, false, order, null)); } @@ -109,7 +110,7 @@ export abstract class ChartView extends BigTableView { .on("drag", () => this.dragMove()) .on("end", () => this.dragEnd()); - const canvas = this.surface.getCanvas(); + const canvas = this.surface!.getCanvas(); canvas.call(drag) .on("mousemove", () => this.onMouseMove()) .on("mouseenter", () => this.onMouseEnter()) @@ -149,7 +150,7 @@ export abstract class ChartView extends BigTableView { * Converts a point coordinate in canvas to a point coordinate in the chart surface. */ public canvasToChart(point: Point): Point { - return { x: point.x - this.surface.leftMargin, y: point.y - this.surface.topMargin }; + return { x: point.x - this.surface!.leftMargin, y: point.y - this.surface!.topMargin }; } protected abstract onMouseMove(): void; @@ -162,7 +163,7 @@ export abstract class ChartView extends BigTableView { protected dragStartRectangle(): void { this.dragging = true; this.moved = false; - const position = d3mouse(this.surface.getCanvas().node()); + const position = d3mouse(this.surface!.getCanvas().node()); this.selectionOrigin = { x: position[0], y: position[1] }; @@ -250,9 +251,9 @@ export abstract class ChartView extends BigTableView { return false; } this.moved = true; - let ox = this.selectionOrigin.x; - let oy = this.selectionOrigin.y; - const position = d3mouse(this.surface.getCanvas().node()); + let ox = this.selectionOrigin!.x; + let oy = this.selectionOrigin!.y; + const position = d3mouse(this.surface!.getCanvas().node()); const x = position[0]; const y = position[1]; let width = x - ox; @@ -308,16 +309,16 @@ export abstract class ChartView extends BigTableView { */ protected filterSelectionRectangle(xl: number, xr: number, yl: number, yr: number, xAxisData: AxisData, yAxisData: AxisData): - RangeFilterArrayDescription { + RangeFilterArrayDescription | null{ if (xAxisData.axis == null || yAxisData.axis == null) { return null; } - xl -= this.surface.leftMargin; - xr -= this.surface.leftMargin; - yl -= this.surface.topMargin; - yr -= this.surface.topMargin; + xl -= this.surface!.leftMargin; + xr -= this.surface!.leftMargin; + yl -= this.surface!.topMargin; + yr -= this.surface!.topMargin; const xRange = xAxisData.getFilter(xl, xr); const yRange = yAxisData.getFilter(yl, yr); return { diff --git a/web/src/main/webapp/dataViews/dataRangesReceiver.ts b/web/src/main/webapp/dataViews/dataRangesReceiver.ts index 7627ddf1b..c83d3aeca 100644 --- a/web/src/main/webapp/dataViews/dataRangesReceiver.ts +++ b/web/src/main/webapp/dataViews/dataRangesReceiver.ts @@ -26,7 +26,7 @@ import { } from "../javaBridge"; import {BaseReceiver, TableTargetAPI} from "../modules"; import {FullPage, PageTitle} from "../ui/fullPage"; -import {ICancellable, periodicSamples, Seed, zip} from "../util"; +import {assertNever, ICancellable, periodicSamples, Seed, zip, assert} from "../util"; import {SchemaClass} from "../schemaClass"; import {ChartOptions, Resolution, Size} from "../ui/ui"; import {PlottingSurface} from "../ui/plottingSurface"; @@ -182,12 +182,12 @@ export class DataRangesReceiver extends OnCompleteReceiver { constructor( protected originator: TableTargetAPI, page: FullPage, - operation: ICancellable, + operation: ICancellable | null, protected schema: SchemaClass, protected bucketCounts: number[], // if 0 we get to choose protected cds: IColumnDescription[], - protected title: PageTitle, // If null we generate a title - protected provenance: string, // Only used if the title is null + protected title: PageTitle | null, // If null we generate a title + protected provenance: string | null, // Only used if the title is null protected options: ChartOptions) { super(page, operation, "histogram"); console.assert(title != null || provenance != null); @@ -223,6 +223,7 @@ export class DataRangesReceiver extends OnCompleteReceiver { exact: boolean, chartSize: Size): HistogramParameters { if (kindIsString(cd.kind)) { + assert(range.stringQuantiles != null); const cdfBucketCount = range.stringQuantiles.length; let samplingRate = DataRangesReceiver.samplingRate( cdfBucketCount, range.presentCount, range.missingCount, chartSize); @@ -230,7 +231,7 @@ export class DataRangesReceiver extends OnCompleteReceiver { samplingRate = 1.0; let bounds = range.stringQuantiles; if (bucketCount !== 0) - bounds = periodicSamples(range.stringQuantiles, bucketCount); + bounds = periodicSamples(range.stringQuantiles, bucketCount)!; return { cd: cd, samplingRate: samplingRate, @@ -244,8 +245,8 @@ export class DataRangesReceiver extends OnCompleteReceiver { let adjust = 0; if (cd.kind === "Integer") { - if (cdfCount > range.max - range.min) - cdfCount = range.max - range.min + 1; + if (cdfCount > range.max! - range.min!) + cdfCount = range.max! - range.min! + 1; adjust = .5; } @@ -256,8 +257,8 @@ export class DataRangesReceiver extends OnCompleteReceiver { // noinspection UnnecessaryLocalVariableJS const args: HistogramParameters = { cd: cd, - min: range.min - adjust, - max: range.max + adjust, + min: range.min! - adjust, + max: range.max! + adjust, samplingRate: samplingRate, bucketCount: cdfCount, }; @@ -286,23 +287,19 @@ export class DataRangesReceiver extends OnCompleteReceiver { } public run(ranges: BucketsInfo[]): void { - for (const range of ranges) { - if (range == null) { - console.log("Null range received"); - return; - } - if (range.presentCount === 0) { - this.page.reportError("No non-missing data"); - return; - } + if (ranges.length == 0) + return; + if (ranges[0].presentCount === 0) { + this.page.reportError("No non-missing data"); + return; } this.rowCount = ranges[0].presentCount + ranges[0].missingCount; const chartSize = PlottingSurface.getDefaultChartSize(this.page.getWidthInPixels()); const exact = this.options.exact || this.isPrivate(); // variables when drawing Trellis plots - let trellisShape: TrellisShape = null; - let windows: number = null; // number of Trellis windows + let trellisShape: TrellisShape | null = null; + let windows: number | null = null; // number of Trellis windows if (this.options.chartKind === "TrellisHeatmap" || this.options.chartKind === "TrellisHistogram" || this.options.chartKind === "Trellis2DHistogram" || @@ -316,10 +313,10 @@ export class DataRangesReceiver extends OnCompleteReceiver { Math.floor(chartSize.width / Resolution.minTrellisWindowSize) * Math.floor(chartSize.height / Resolution.minTrellisWindowSize); if (kindIsString(this.cds[groupByIndex].kind)) - windows = Math.min(maxWindows, ranges[groupByIndex].stringQuantiles.length); + windows = Math.min(maxWindows, ranges[groupByIndex].stringQuantiles!.length); else if (this.cds[groupByIndex].kind === "Integer") windows = Math.min(maxWindows, - ranges[groupByIndex].max - ranges[groupByIndex].min + 1); + ranges[groupByIndex].max! - ranges[groupByIndex].min! + 1); else windows = maxWindows; } @@ -330,7 +327,7 @@ export class DataRangesReceiver extends OnCompleteReceiver { case "CorrelationHeatmaps": { if (this.title == null) this.title = new PageTitle( - "Pairwise correlations between " + ranges.length + " columns", this.provenance); + "Pairwise correlations between " + ranges.length + " columns", this.provenance!); const colCount = ranges.length; if (colCount < 1) { this.page.reportError("Not enough columns"); @@ -365,8 +362,8 @@ export class DataRangesReceiver extends OnCompleteReceiver { } if (this.title == null) this.title = new PageTitle( - "Quartiles of " + this.schema.displayName(this.cds[1].name).toString() + - " bucketed by " + this.schema.displayName(this.cds[0].name).toString(), this.provenance); + "Quartiles of " + this.schema.displayName(this.cds[1].name)!.toString() + + " bucketed by " + this.schema.displayName(this.cds[0].name)!.toString(), this.provenance!); const histoArg = DataRangesReceiver.computeHistogramArgs( this.cds[0], ranges[0], maxXBucketCount, @@ -411,7 +408,7 @@ export class DataRangesReceiver extends OnCompleteReceiver { const axisData = new AxisData(this.cds[0], ranges[0], histoArg.bucketCount); if (this.title == null) this.title = new PageTitle( - "Histogram of " + this.schema.displayName(this.cds[0].name).toString(), this.provenance); + "Histogram of " + this.schema.displayName(this.cds[0].name)!.toString(), this.provenance!); const renderer = new HistogramReceiver(this.title, this.page, this.originator.remoteObjectId, this.rowCount, this.schema, axisData, rr, cdfArg.samplingRate, this.options.pieChart, this.options.reusePage); // TODO sampling rate? @@ -423,11 +420,11 @@ export class DataRangesReceiver extends OnCompleteReceiver { // noinspection JSObjectNullOrUndefined const xArg = DataRangesReceiver.computeHistogramArgs( this.cds[0], ranges[0], this.bucketCounts[0], - exact, trellisShape.size); + exact, trellisShape!.size); let groups = this.bucketCounts[1] === 0 ? - groupByBuckets(trellisShape) : this.bucketCounts[1]; + groupByBuckets(trellisShape!) : this.bucketCounts[1]; const wArg = DataRangesReceiver.computeHistogramArgs( - this.cds[1], ranges[1], groups, exact, trellisShape.size); + this.cds[1], ranges[1], groups, exact, trellisShape!.size); // Window argument comes first const args = createRequestArgs([wArg, xArg], true); const rr = this.originator.createHistogram2DRequest(args); @@ -435,12 +432,12 @@ export class DataRangesReceiver extends OnCompleteReceiver { const groupByAxis = new AxisData(this.cds[1], ranges[1], wArg.bucketCount); if (this.title == null) this.title = new PageTitle( - "Histograms of " + this.schema.displayName(this.cds[0].name).toString() + - " grouped by " + this.schema.displayName(this.cds[1].name).toString(), this.provenance); + "Histograms of " + this.schema.displayName(this.cds[0].name)!.toString() + + " grouped by " + this.schema.displayName(this.cds[1].name)!.toString(), this.provenance!); const renderer = new TrellisHistogramReceiver(this.title, this.page, this.originator, this.rowCount, this.schema, [xAxisData, groupByAxis], this.bucketCounts[0], - xArg.samplingRate, trellisShape, rr, this.options.reusePage); + xArg.samplingRate, trellisShape!, rr, this.options.reusePage); rr.chain(this.operation); rr.invoke(renderer); break; @@ -457,18 +454,18 @@ export class DataRangesReceiver extends OnCompleteReceiver { let maxXBucketCount = this.bucketCounts[0]; if (maxXBucketCount === 0) { // noinspection JSObjectNullOrUndefined - maxXBucketCount = Math.floor(trellisShape.size.width / Resolution.minBarWidth); + maxXBucketCount = Math.floor(trellisShape!.size.width / Resolution.minBarWidth); } let maxGBucketCount = this.bucketCounts[2]; if (maxGBucketCount === 0) // noinspection JSObjectNullOrUndefined - maxGBucketCount = trellisShape.windowCount; + maxGBucketCount = trellisShape!.windowCount; if (this.title == null) this.title = new PageTitle( - "Trellis quartiles of " + this.schema.displayName(this.cds[1].name).toString() + - " bucketed by " + this.schema.displayName(this.cds[0].name).toString() + - " grouped by " + this.schema.displayName(this.cds[2].name).toString(), this.provenance); + "Trellis quartiles of " + this.schema.displayName(this.cds[1].name)!.toString() + + " bucketed by " + this.schema.displayName(this.cds[0].name)!.toString() + + " grouped by " + this.schema.displayName(this.cds[2].name)!.toString(), this.provenance); const histoArg0 = DataRangesReceiver.computeHistogramArgs( this.cds[0], ranges[0], maxXBucketCount, @@ -495,19 +492,19 @@ export class DataRangesReceiver extends OnCompleteReceiver { let maxYBucketCount; if (this.options.chartKind === "Trellis2DHistogram") { // noinspection JSObjectNullOrUndefined - maxXBucketCount = Math.floor(trellisShape.size.width / Resolution.minBarWidth); + maxXBucketCount = Math.floor(trellisShape!.size.width / Resolution.minBarWidth); maxYBucketCount = Resolution.max2DBucketCount; } else { // noinspection JSObjectNullOrUndefined - maxXBucketCount = Math.floor(trellisShape.size.width / Resolution.minDotSize); - maxYBucketCount = Math.floor(trellisShape.size.height / Resolution.minDotSize); + maxXBucketCount = Math.floor(trellisShape!.size.width / Resolution.minDotSize); + maxYBucketCount = Math.floor(trellisShape!.size.height / Resolution.minDotSize); } const xArg = DataRangesReceiver.computeHistogramArgs( - this.cds[0], ranges[0], maxXBucketCount, exact, trellisShape.size); + this.cds[0], ranges[0], maxXBucketCount, exact, trellisShape!.size); const yArg = DataRangesReceiver.computeHistogramArgs( - this.cds[1], ranges[1], maxYBucketCount, exact, trellisShape.size); + this.cds[1], ranges[1], maxYBucketCount, exact, trellisShape!.size); const wArg = DataRangesReceiver.computeHistogramArgs( - this.cds[2], ranges[2], groupByBuckets(trellisShape), exact, chartSize); + this.cds[2], ranges[2], groupByBuckets(trellisShape!), exact, chartSize); // Window argument comes first const args = createRequestArgs([wArg, xArg, yArg], true); const rr = this.originator.createHistogram3DRequest(args); @@ -517,18 +514,18 @@ export class DataRangesReceiver extends OnCompleteReceiver { if (this.title == null) this.title = new PageTitle( this.options.chartKind.toString().replace("Trellis", "") + - " (" + this.schema.displayName(this.cds[0].name).displayName + - ", " + this.schema.displayName(this.cds[1].name).displayName + - ") grouped by " + this.schema.displayName(this.cds[2].name).displayName, this.provenance); + " (" + this.schema.displayName(this.cds[0].name)!.displayName + + ", " + this.schema.displayName(this.cds[1].name)!.displayName + + ") grouped by " + this.schema.displayName(this.cds[2].name)!.displayName, this.provenance); let renderer; if (this.options.chartKind === "Trellis2DHistogram") renderer = new TrellisHistogram2DReceiver(this.title, this.page, this.originator, this.rowCount, this.schema, - [xAxis, yAxis, groupByAxis], 1.0, trellisShape, rr, this.options); + [xAxis, yAxis, groupByAxis], 1.0, trellisShape!, rr, this.options); else renderer = new TrellisHeatmapReceiver(this.title, this.page, this.originator, this.rowCount, this.schema, - [xAxis, yAxis, groupByAxis], 1.0, trellisShape, rr, this.options.reusePage); + [xAxis, yAxis, groupByAxis], 1.0, trellisShape!, rr, this.options.reusePage); rr.chain(this.operation); rr.invoke(renderer); break; @@ -549,8 +546,8 @@ export class DataRangesReceiver extends OnCompleteReceiver { const yAxis = new AxisData(this.cds[1], ranges[1], yArg.bucketCount); if (this.title == null) this.title = new PageTitle( - "Heatmap (" + this.schema.displayName(this.cds[0].name).displayName + ", " + - this.schema.displayName(this.cds[1].name).displayName + ")", this.provenance); + "Heatmap (" + this.schema.displayName(this.cds[0].name)!.displayName + ", " + + this.schema.displayName(this.cds[1].name)!.displayName + ")", this.provenance!); if (this.isPrivate()) { const rr = this.originator.createHistogram2DRequest(args); const renderer = new HeatmapReceiver(this.title, this.page, @@ -601,8 +598,8 @@ export class DataRangesReceiver extends OnCompleteReceiver { const yData = new AxisData(this.cds[1], ranges[1], yarg.bucketCount); if (this.title == null) this.title = new PageTitle( - "Histogram (" + this.schema.displayName(this.cds[0].name).displayName + ", " + - this.schema.displayName(this.cds[1].name).displayName + ")", this.provenance); + "2DHistogram (" + this.schema.displayName(this.cds[0].name)!.displayName + ", " + + this.schema.displayName(this.cds[1].name)!.displayName + ")", this.provenance!); const renderer = new Histogram2DReceiver(this.title, this.page, this.originator, this.rowCount, this.schema, [xAxis, yData], cdfArg.samplingRate, rr, @@ -611,9 +608,15 @@ export class DataRangesReceiver extends OnCompleteReceiver { rr.invoke(renderer); break; } + case "Table": + case "SVD Spectrum": + case "HeavyHitters": + case "Schema": + case "Load": + case "LogFileView": + throw new Error("Unexpected kind " + this.options.chartKind); default: - console.assert(false); - break; + assertNever(this.options.chartKind); } } } diff --git a/web/src/main/webapp/dataViews/heatmapView.ts b/web/src/main/webapp/dataViews/heatmapView.ts index c8c2d6b48..5b88369b7 100644 --- a/web/src/main/webapp/dataViews/heatmapView.ts +++ b/web/src/main/webapp/dataViews/heatmapView.ts @@ -37,7 +37,7 @@ import {HtmlPlottingSurface, PlottingSurface} from "../ui/plottingSurface"; import {TextOverlay} from "../ui/textOverlay"; import {DragEventKind, Resolution} from "../ui/ui"; import { - assert, + assert, assertNever, Converters, dataRange, Heatmap, @@ -67,11 +67,11 @@ export class HeatmapView extends // with one cell for each column in the detailColumns schema. protected colorLegend: HeatmapLegendPlot; - protected detailLegend: HistogramLegendPlot; + protected detailLegend: HistogramLegendPlot | null; protected plot: HeatmapPlot; protected legendSurface: HtmlPlottingSurface; - protected detailLegendDiv: HTMLDivElement; - protected detailLegendSurface: HtmlPlottingSurface; + protected detailLegendDiv: HTMLDivElement | null; + protected detailLegendSurface: HtmlPlottingSurface | null; protected xPoints: number; protected yPoints: number; protected readonly viewMenu: SubMenu; @@ -116,8 +116,8 @@ export class HeatmapView extends help: "Change the number of buckets used to draw this histogram. ", }, { text: "table", - action: () => this.showTable([this.xAxisData.description, - this.yAxisData.description], this.defaultProvenance), + action: () => this.showTable([this.xAxisData.description!, + this.yAxisData.description!], this.defaultProvenance), help: "View the data underlying this view as a table.", }, { text: "histogram", @@ -171,18 +171,18 @@ export class HeatmapView extends input.required = true; dialog.setAction(() => { const c = dialog.getColumnName("column"); - this.detailIndex = this.detailColumns.columnIndex(this.schema.fromDisplayName(c)); + this.detailIndex = this.detailColumns!.columnIndex(this.schema.fromDisplayName(c)!); this.resize(); }); dialog.show(); } private quartileView(): void { - if (!kindIsNumeric(this.yAxisData.description.kind)) { + if (!kindIsNumeric(this.yAxisData.description!.kind)) { this.page.reportError("Quartiles require a numeric second column"); return; } - const cds = [this.xAxisData.description, this.yAxisData.description]; + const cds = [this.xAxisData.description!, this.yAxisData.description!]; const rr = this.createDataQuantilesRequest(cds, this.page, "QuartileVector"); rr.invoke(new DataRangesReceiver(this, this.page, rr, this.schema, [0, 0], cds, null, this.defaultProvenance,{ @@ -320,6 +320,8 @@ export class HeatmapView extends return this.xAxisData; case "YAxis": return this.yAxisData; + default: + assertNever(event); } } @@ -442,23 +444,26 @@ export class HeatmapView extends columnDescription1: this.yAxisData.description, xBucketCount: this.xAxisData.bucketCount, yBucketCount: this.yAxisData.bucketCount, + xRange: this.xAxisData.dataRange, + yRange: this.yAxisData.dataRange, ...ser, }; return result; } - public static reconstruct(ser: HeatmapSerialization, page: FullPage): IDataView { - if (ser.columnDescription0 == null || ser.columnDescription1 == null || - ser.samplingRate == null || ser.schema == null || - ser.xBucketCount == null || ser.yBucketCount == null) { + public static reconstruct(ser: HeatmapSerialization, page: FullPage): IDataView | null { + if (ser.columnDescription0 === null || ser.columnDescription1 === null || + ser.samplingRate === null || ser.schema === null || + ser.xBucketCount === null || ser.yBucketCount === null || + ser.xRange === null || ser.yRange === null) { return null; } const schema: SchemaClass = new SchemaClass([]).deserialize(ser.schema); const detailed: SchemaClass = new SchemaClass([]).deserialize(ser.detailedColumns); const hv = new HeatmapView(ser.remoteObjectId, ser.rowCount, schema, detailed, ser.samplingRate, page); hv.setAxes( - new AxisData(ser.columnDescription0, null, ser.xBucketCount), - new AxisData(ser.columnDescription1, null, ser.yBucketCount)); + new AxisData(ser.columnDescription0, ser.xRange, ser.xBucketCount), + new AxisData(ser.columnDescription1, ser.yRange, ser.yBucketCount)); return hv; } diff --git a/web/src/main/webapp/dataViews/histogram2DView.ts b/web/src/main/webapp/dataViews/histogram2DView.ts index b2f768974..d92ed7e04 100644 --- a/web/src/main/webapp/dataViews/histogram2DView.ts +++ b/web/src/main/webapp/dataViews/histogram2DView.ts @@ -35,7 +35,7 @@ import {HtmlPlottingSurface} from "../ui/plottingSurface"; import {TextOverlay} from "../ui/textOverlay"; import {ChartOptions, DragEventKind, Resolution} from "../ui/ui"; import { - add, + add, assertNever, Converters, Heatmap, histogram2DAsCsv, ICancellable, @@ -193,6 +193,8 @@ export class Histogram2DView extends HistogramViewBase>>> / missingCount: this.histogram().first.perMissing }; return new AxisData(null, range, 0); + default: + assertNever(event); } } @@ -208,7 +210,8 @@ export class HistogramView extends HistogramViewBase>>> / samplingRate: this.samplingRate, bucketCount: this.bucketCount, columnDescription: this.xAxisData.description, - isPie: this.pie + isPie: this.pie, + range: this.xAxisData.dataRange }; return result; } @@ -217,12 +220,12 @@ export class HistogramView extends HistogramViewBase>>> / const args: CommonArgs = this.validateSerialization(ser); if (args == null || ser.columnDescription == null || ser.samplingRate == null || - ser.bucketCount == null) + ser.bucketCount == null || ser.range == null) return null; const hv = new HistogramView( ser.remoteObjectId, ser.rowCount, args.schema, ser.samplingRate, ser.isPie, page); - hv.setAxis(new AxisData(ser.columnDescription, null, ser.bucketCount)); + hv.setAxis(new AxisData(ser.columnDescription, ser.range, ser.bucketCount)); hv.bucketCount = ser.bucketCount; return hv; } diff --git a/web/src/main/webapp/dataViews/quartilesHistogramView.ts b/web/src/main/webapp/dataViews/quartilesHistogramView.ts index ff7dff9d3..cb73ba4da 100644 --- a/web/src/main/webapp/dataViews/quartilesHistogramView.ts +++ b/web/src/main/webapp/dataViews/quartilesHistogramView.ts @@ -31,6 +31,7 @@ import {HtmlPlottingSurface} from "../ui/plottingSurface"; import {TextOverlay} from "../ui/textOverlay"; import {ChartOptions, DragEventKind, Resolution} from "../ui/ui"; import { + assertNever, Converters, describeQuartiles, ICancellable, @@ -96,7 +97,7 @@ export class QuartilesHistogramView extends HistogramViewBase> public trellis(): void { const columns: DisplayName[] = this.schema.displayNamesExcluding( - [this.xAxisData.description.name, this.yAxisData.description.name]); + [this.xAxisData.description.name, this.qCol.name]); this.chooseTrellis(columns); } @@ -104,7 +105,7 @@ export class QuartilesHistogramView extends HistogramViewBase> const groupBy = this.schema.findByDisplayName(colName); const cds: IColumnDescription[] = [ this.xAxisData.description, - this.yAxisData.description, + this.qCol, groupBy]; const rr = this.createDataQuantilesRequest(cds, this.page, "TrellisQuartiles"); rr.invoke(new DataRangesReceiver(this, this.page, rr, this.schema, @@ -129,6 +130,8 @@ export class QuartilesHistogramView extends HistogramViewBase> return this.yAxisData; case "XAxis": return this.xAxisData; + default: + assertNever(event); } return null; } @@ -157,6 +160,7 @@ export class QuartilesHistogramView extends HistogramViewBase> // noinspection UnnecessaryLocalVariableJS const result: QuantileVectorSerialization = { ...super.serialize(), + xRange: this.xAxisData.dataRange, columnDescription0: this.xAxisData.description, columnDescription1: this.qCol, xBucketCount: this.xAxisData.bucketCount, @@ -170,11 +174,11 @@ export class QuartilesHistogramView extends HistogramViewBase> const xPoints: number = ser.xBucketCount; const schema: SchemaClass = new SchemaClass([]).deserialize(ser.schema); if (cd0 === null || cd1 === null || schema === null || - xPoints === null) + xPoints === null || ser.xRange === null) return null; const hv = new QuartilesHistogramView(ser.remoteObjectId, ser.rowCount, schema, cd1, page); - hv.setAxis(new AxisData(cd0, null, ser.xBucketCount)); + hv.setAxis(new AxisData(cd0, ser.xRange, ser.xBucketCount)); return hv; } @@ -256,7 +260,17 @@ export class QuartilesHistogramView extends HistogramViewBase> } public refresh(): void { - /* TODO */ + const cds = [this.xAxisData.description, this.qCol]; + const ranges = [this.xAxisData.dataRange]; + const collector = new DataRangesReceiver(this, + this.page, null, this.schema, + [this.xAxisData.bucketCount], + cds, this.page.title, null,{ + chartKind: "QuartileVector", exact: true, + reusePage: true + }); + collector.run(ranges); + collector.finished(); } public onMouseEnter(): void { diff --git a/web/src/main/webapp/dataViews/tableView.ts b/web/src/main/webapp/dataViews/tableView.ts index 856458dc8..a37f30f2d 100644 --- a/web/src/main/webapp/dataViews/tableView.ts +++ b/web/src/main/webapp/dataViews/tableView.ts @@ -62,7 +62,7 @@ import { significantDigits, significantDigitsHtml, truncate, - all, percent + all, percent, assertNever } from "../util"; import {SchemaView} from "../modules"; import {SpectrumReceiver} from "./spectrumView"; @@ -90,7 +90,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { protected cellsPerColumn: Map; protected selectedColumns = new SelectionStateMachine(); protected message: HTMLElement; - protected strFilter: StringFilterDescription; + protected strFilter: StringFilterDescription | null; public aggregates: AggregateDescription[] | null; // The following elements are used for Find @@ -414,7 +414,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { } const order = this.order.invert(); const rr = this.createNextKRequest(order, this.nextKList.rows[0].values, - this.tableRowsDesired, this.aggregates); + this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.getPage(), this, rr, true, order, null)); } @@ -426,7 +426,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { return; } const o = this.order.clone(); - const rr = this.createNextKRequest(o, null, this.tableRowsDesired, this.aggregates); + const rr = this.createNextKRequest(o, null, this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.getPage(), this, rr, false, o, null)); } @@ -438,7 +438,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { return; } const order = this.order.invert(); - const rr = this.createNextKRequest(order, null, this.tableRowsDesired, this.aggregates); + const rr = this.createNextKRequest(order, null, this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.getPage(), this, rr, true, order, null)); } @@ -452,7 +452,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { const o = this.order.clone(); const rr = this.createNextKRequest( o, this.nextKList.rows[this.nextKList.rows.length - 1].values, - this.tableRowsDesired, this.aggregates); + this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.getPage(), this, rr, false, o, null)); } @@ -582,7 +582,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { this.nextKList.rows.length > 0) firstRow = this.nextKList.rows[0].values; const rr = this.createNextKRequest(this.order, firstRow, - this.tableRowsDesired, this.aggregates); + this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.page, this, rr, false, this.order, null)); } @@ -968,9 +968,9 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { const cd = this.schema.get(i); cds.push(cd); - const kindString = cd.kind; + const kindString = cd.kind.toString(); const name = this.schema.displayName(cd.name); - let title = name + "\nType is " + kindString + "\n"; + let title = name.displayName + "\nType is " + kindString + "\n"; if (this.isPrivate()) { const pm = this.dataset.privacySchema.quantization.quantization[cd.name]; if (pm != null) { @@ -1127,6 +1127,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { case "Date": case "Duration": case "LocalDate": + case "Time": doubleValue = value as number; break; case "Interval": @@ -1134,6 +1135,8 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { doubleValue = a[0]; intervalEnd = a[1]; break; + default: + assertNever(cd.kind); } const cfd: ComparisonFilterDescription = { column: cd, @@ -1356,7 +1359,7 @@ export class TableView extends TSViewBase implements IScrollTarget, OnNextK { public moveRowToTop(row: RowData): void { const rr = this.createNextKRequest( - this.order, row.values, this.tableRowsDesired, this.aggregates); + this.order, row.values, this.tableRowsDesired, this.aggregates, null); rr.invoke(new NextKReceiver(this.page, this, rr, false, this.order, null)); } @@ -1626,7 +1629,7 @@ export class NextKReceiver extends Receiver { operation: ICancellable, protected reverse: boolean, protected order: RecordOrder, - protected result: FindResult) { + protected result: FindResult | null) { super(page, operation, "Getting table rows"); } @@ -1651,13 +1654,13 @@ export class SchemaReceiver extends OnCompleteReceiver { * @param operation Operation that will bring the results. * @param remoteObject Table object. * @param dataset Dataset that this is a part of. - * @param schema Schema that is used to display the data. + * @param schema Schema that is used to display the data - if not null. * @param viewKind What view to use. If null we get to choose. */ constructor(page: FullPage, operation: ICancellable, protected remoteObject: TableTargetAPI, protected dataset: DatasetView, - protected schema: SchemaClass, + protected schema: SchemaClass | null, protected viewKind: ViewKind | null) { super(page, operation, "Get schema"); } @@ -1709,7 +1712,7 @@ class QuantileReceiver extends OnCompleteReceiver { public run(firstRow: RowValue[]): void { const rr = this.tv.createNextKRequest( - this.order, firstRow, this.tv.tableRowsDesired, this.tv.aggregates); + this.order, firstRow, this.tv.tableRowsDesired, this.tv.aggregates, null); rr.chain(this.operation); rr.invoke(new NextKReceiver(this.page, this.tv, rr, false, this.order, null)); } @@ -1793,7 +1796,7 @@ class PCASchemaReceiver extends OnCompleteReceiver { const table = new TableView( this.remoteObject.remoteObjectId, this.tv.rowCount, schema, this.page); this.page.setDataView(table); - const rr = table.createNextKRequest(o, null, this.tableRowsDesired, null); + const rr = table.createNextKRequest(o, null, this.tableRowsDesired, null, null); rr.chain(this.operation); rr.invoke(new NextKReceiver(this.page, table, rr, false, o, null)); } @@ -1836,7 +1839,7 @@ export class TableOperationCompleted extends BaseReceiver { table.aggregates = this.aggregates; this.page.setDataView(table); const rr = table.createNextKRequest( - this.order, null, this.tableRowsDesired, this.aggregates); + this.order, null, this.tableRowsDesired, this.aggregates, null); rr.chain(this.operation); rr.invoke(new NextKReceiver(this.page, table, rr, false, this.order, null)); } @@ -1861,7 +1864,7 @@ export class FindReceiver extends OnCompleteReceiver { return; } const rr = this.tv.createNextKRequest( - this.order, result.firstMatchingRow, this.tv.tableRowsDesired, this.tv.aggregates); + this.order, result.firstMatchingRow, this.tv.tableRowsDesired, this.tv.aggregates, null); rr.chain(this.operation); rr.invoke(new NextKReceiver(this.page, this.tv, rr, false, this.order, result)); } diff --git a/web/src/main/webapp/dataViews/trellisHeatmapView.ts b/web/src/main/webapp/dataViews/trellisHeatmapView.ts index d4042d6e0..a9d18f872 100644 --- a/web/src/main/webapp/dataViews/trellisHeatmapView.ts +++ b/web/src/main/webapp/dataViews/trellisHeatmapView.ts @@ -35,6 +35,7 @@ import { } from "./dataRangesReceiver"; import {Receiver, RpcRequest} from "../rpc"; import { + assertNever, Converters, GroupsClass, Heatmap, histogram3DAsCsv, @@ -108,8 +109,9 @@ export class TrellisHeatmapView extends TrellisChartView= 1, relative: false, reusePage: true @@ -234,26 +234,28 @@ export class TrellisHistogramView extends TrellisChartView this.runComparisonFilter( diff --git a/web/src/main/webapp/datasetView.ts b/web/src/main/webapp/datasetView.ts index b8784aa3c..c8c0d71a8 100644 --- a/web/src/main/webapp/datasetView.ts +++ b/web/src/main/webapp/datasetView.ts @@ -37,7 +37,7 @@ import {IDataView} from "./ui/dataview"; import {FullPage, PageTitle} from "./ui/fullPage"; import {ContextMenu, MenuItem, SubMenu, TopMenuItem} from "./ui/menu"; import {IHtmlElement, removeAllChildren, ViewKind} from "./ui/ui"; -import {assert, cloneArray, EnumIterators, ICancellable} from "./util"; +import {assertNever, cloneArray, EnumIterators, ICancellable} from "./util"; import {TrellisHeatmapView} from "./dataViews/trellisHeatmapView"; import {TrellisHistogram2DView} from "./dataViews/trellisHistogram2DView"; import {TrellisHistogramView} from "./dataViews/trellisHistogramView"; @@ -47,11 +47,12 @@ import {QuartilesHistogramView} from "./dataViews/quartilesHistogramView"; import {TrellisHistogramQuartilesView} from "./dataViews/trellisHistogramQuartilesView"; import {saveAs} from "./ui/dialog"; import {showBookmarkURL} from "./ui/dialog"; +import {CorrelationHeatmapView} from "./dataViews/correlationHeatmapView"; export interface IViewSerialization { viewKind: ViewKind; pageId: number; - sourcePageId: number; + sourcePageId: number | null; title: string; provenance: string; remoteObjectId: RemoteObjectId; @@ -76,6 +77,7 @@ export interface HistogramSerialization extends IViewSerialization { bucketCount: number; samplingRate: number; isPie: boolean; + range: BucketsInfo; columnDescription: IColumnDescription; } @@ -85,7 +87,8 @@ export interface BaseHeatmapSerialization extends IViewSerialization { columnDescription1: IColumnDescription; xBucketCount: number; yBucketCount: number; - + xRange: BucketsInfo; + yRange: BucketsInfo; } export interface HeatmapSerialization extends BaseHeatmapSerialization { @@ -96,6 +99,7 @@ export interface QuantileVectorSerialization extends IViewSerialization { columnDescription0: IColumnDescription; columnDescription1: IColumnDescription; xBucketCount: number; + xRange: BucketsInfo; } export interface Histogram2DSerialization extends BaseHeatmapSerialization { @@ -112,6 +116,7 @@ export interface TrellisShapeSerialization { yWindows: number; windowCount: number; missingBucket: boolean; + gRange: BucketsInfo; } export interface TrellisHistogramSerialization extends @@ -149,7 +154,7 @@ export class DatasetView implements IHtmlElement { private readonly pageContainer: HTMLElement; private pageCounter: number; public readonly allPages: FullPage[]; - public privacySchema: PrivacySchema = null; + public privacySchema: PrivacySchema | null = null; protected privacyEditor: HTMLElement; private readonly menu: ContextMenu; @@ -225,11 +230,11 @@ export class DatasetView implements IHtmlElement { const copy = cloneArray(columns); copy.sort(); const key = copy.join("+"); - let eps = this.privacySchema.epsilons[key]; + let eps = this.privacySchema!.epsilons[key]; if (eps == null) - eps = this.privacySchema.defaultEpsilons[copy.length.toString()]; + eps = this.privacySchema!.defaultEpsilons[copy.length.toString()]; if (eps == null) - eps = this.privacySchema.defaultEpsilon; + eps = this.privacySchema!.defaultEpsilon; return eps; } @@ -237,7 +242,7 @@ export class DatasetView implements IHtmlElement { const copy = cloneArray(columns); copy.sort(); const key = copy.join("+"); - this.privacySchema.epsilons[key] = epsilon; + this.privacySchema!.epsilons[key] = epsilon; this.uploadPrivacy(JSON.stringify(this.privacySchema), false); } @@ -347,7 +352,7 @@ export class DatasetView implements IHtmlElement { help: "Select the current view; later it can be combined with another view, " + "using one of the operations below.", }); - combineMenu.push({text: "---", action: null, help: null}); + combineMenu.push({text: "---", action: null, help: ""}); EnumIterators.getNamesAndValues(CombineOperators) .forEach((c) => combineMenu.push({ text: c.name, @@ -374,8 +379,7 @@ export class DatasetView implements IHtmlElement { * @param {FullPage} after Page to insert after; if null insertion is done at the end. */ public insertAfter(toInsert: FullPage, after: FullPage | null): void { - assert(toInsert !== null); - const pageRepresentation = toInsert.getHTMLRepresentation(); + const pageRepresentation = toInsert!.getHTMLRepresentation(); if (after == null) { this.pageContainer.appendChild(pageRepresentation); this.allPages.push(toInsert); @@ -476,7 +480,7 @@ export class DatasetView implements IHtmlElement { return false; const page = this.reconstructPage(new PageTitle(vs.title, vs.provenance), vs.pageId, vs.sourcePageId); - let view: IDataView = null; + let view: IDataView | null = null; switch (vs.viewKind) { case "Table": view = TableView.reconstruct(vs as TableSerialization, page); @@ -514,14 +518,22 @@ export class DatasetView implements IHtmlElement { case "SVD Spectrum": view = SpectrumView.reconstruct(vs as SpectrumSerialization, page); break; + case "CorrelationHeatmaps": + view = CorrelationHeatmapView.reconstruct(vs as CorrelationHeatmapSerialization, page); + break; case "Load": - // These do not need to be reconstructed ever. + // These do not need to be reconstructed ever. + break; + case "LogFileView": + // TODO + break; default: + assertNever(vs.viewKind); break; } if (view != null) { - view.refresh(); page.setDataView(view); + view.refresh(); return true; } return false; @@ -529,19 +541,22 @@ export class DatasetView implements IHtmlElement { /** * reconstruct a dataset view from serialized information. - * @param {Object} obj Serialized description of the dataset read back. - * @returns {boolean} True if the reconstruction succeeded. + * @param obj Serialized description of the dataset read back. + * @returns The number of failures. */ - public reconstruct(obj: object): boolean { + public reconstruct(obj: object): number { const dss = obj as IDatasetSerialization; if (dss.views == null) - return false; + return 1; if (!Array.isArray(dss.views)) - return false; - for (const v of dss.views) - if (!this.reconstructView(v)) - return false; - return true; + return 1; + let failures = 0; + for (const v of dss.views) { + if (!this.reconstructView(v)) { + failures++; + } + } + return failures; } public serialize(): IDatasetSerialization { @@ -577,7 +592,7 @@ export class DatasetView implements IHtmlElement { public refresh(): void { for (const page of this.allPages) { - page.getDataView().refresh(); + page.getDataView()!.refresh(); // TODO: refresh will un-minimize the page // but that will only happen when the asynchronous request comes back, // so there is no point minimizing it here. diff --git a/web/src/main/webapp/index.html b/web/src/main/webapp/index.html index 455c176e6..2e096bcea 100644 --- a/web/src/main/webapp/index.html +++ b/web/src/main/webapp/index.html @@ -58,7 +58,7 @@

Suggestions
+ onmouseout="showSuggestions(false)" id="suggestions-anchor" style="display: inline-block">Suggestions
{ }; } - public deserialize(data: SchemaClassSerialization): SchemaClass { + public deserialize(data: SchemaClassSerialization): SchemaClass | null { if (data == null) return null; this.schema = data.schema; @@ -120,7 +120,7 @@ export class SchemaClass implements Serializable { } public allDisplayNames(): DisplayName[] { - return this.columnNames.map((c) => this.displayName(c)); + return this.columnNames.map((c) => this.displayName(c)!); } /** @@ -130,7 +130,7 @@ export class SchemaClass implements Serializable { public displayNamesExcluding(names: string[]): DisplayName[] { return this.columnNames .filter((n) => names.indexOf(n) < 0) - .map((c) => this.displayName(c)); + .map((c) => this.displayName(c)!); } /** @@ -147,7 +147,7 @@ export class SchemaClass implements Serializable { /** * Given a display name get the real column name. */ - public fromDisplayName(displayName: DisplayName): string | null { + public fromDisplayName(displayName: DisplayName | null): string | null { if (displayName == null) return null; if (this.reverseDisplayNameMap.has(displayName.displayName)) @@ -189,7 +189,7 @@ export class SchemaClass implements Serializable { return this.columnMap.get(colName); } - public find(colName: string): IColumnDescription { + public find(colName: string | null): IColumnDescription | null { if (colName == null) return null; const colIndex = this.columnIndex(colName); @@ -198,7 +198,7 @@ export class SchemaClass implements Serializable { return null; } - public findByDisplayName(displayName: DisplayName | null): IColumnDescription { + public findByDisplayName(displayName: DisplayName | null): IColumnDescription | null { const original = this.fromDisplayName(displayName); return this.find(original); } @@ -246,8 +246,8 @@ export class SchemaClass implements Serializable { return result; } - public getDescriptions(columns: string[]): IColumnDescription[] { - const cds: IColumnDescription[] = []; + public getDescriptions(columns: string[]): (IColumnDescription | null)[] { + const cds: (IColumnDescription | null)[] = []; columns.forEach((v) => { const colDesc = this.find(v); cds.push(colDesc); diff --git a/web/src/main/webapp/tableTarget.ts b/web/src/main/webapp/tableTarget.ts index 70e648e37..a3ec62d9d 100644 --- a/web/src/main/webapp/tableTarget.ts +++ b/web/src/main/webapp/tableTarget.ts @@ -55,7 +55,7 @@ import {OnCompleteReceiver, RemoteObject, RpcRequest} from "./rpc"; import {FullPage, PageTitle} from "./ui/fullPage"; import {HtmlString, PointSet, Resolution, SpecialChars, ViewKind} from "./ui/ui"; import { - assert, + assert, assertNever, ICancellable, Pair, Seed, @@ -80,7 +80,7 @@ export interface OnNextK extends CompletedWithTime { updateView(nextKList: NextKList, revert: boolean, order: RecordOrder, - result: FindResult): void; + result: FindResult | null): void; } /** @@ -160,9 +160,17 @@ export class TableTargetAPI extends RemoteObject { return [maxBuckets, maxBuckets, maxWindows]; case "TrellisHistogram": return [width, maxWindows]; + case "Table": + case "Schema": + case "Load": + case "HeavyHitters": + case "SVD Spectrum": + case "LogFileView": + // Shoudld not occur + assert(false); + return []; default: - assert(false, "Unhandled case " + viewKind); - return null; + assertNever(viewKind); } } @@ -229,14 +237,14 @@ export class TableTargetAPI extends RemoteObject { * "minimum possible value" instead of "null". */ public createNextKRequest(order: RecordOrder, firstRow: RowValue[] | null, rowsOnScreen: number, - aggregates?: AggregateDescription[], columnsNoValue?: string[]): + aggregates: AggregateDescription[] | null, columnsNoValue: string[] | null): RpcRequest { const nextKArgs: NextKArgs = { toFind: null, - order: order, - firstRow: firstRow, - rowsOnScreen: rowsOnScreen, - columnsNoValue: columnsNoValue, + order, + firstRow, + rowsOnScreen, + columnsNoValue, aggregates }; return this.createStreamingRpcRequest("getNextK", nextKArgs); @@ -455,7 +463,7 @@ export class SummaryMessage { summary.appendSafeString(k + ": "); if (this.approx.has(k)) summary.appendSafeString(SpecialChars.approx); - summary.append(significantDigitsHtml(v)); + summary.append(significantDigitsHtml(v)!); }); summary.setInnerHtml(this.parent); } @@ -479,11 +487,11 @@ type CommonPlots = "chart" // Contains the chart (or charts for trellis views) export abstract class BigTableView extends TableTargetAPI implements IDataView, CompletedWithTime { protected topLevel: HTMLElement; public readonly dataset: DatasetView; - protected chartDiv: HTMLDivElement; - protected summaryDiv: HTMLDivElement; + protected chartDiv: HTMLDivElement | null; + protected summaryDiv: HTMLDivElement | null; // This may not exist. - protected legendDiv: HTMLDivElement; - protected summary: SummaryMessage; + protected legendDiv: HTMLDivElement | null; + protected summary: SummaryMessage | null; /** * Create a view for a big table. @@ -568,14 +576,14 @@ export abstract class BigTableView extends TableTargetAPI implements IDataView, } protected standardSummary(): void { - this.summary.set("row count", this.rowCount, this.isPrivate()); + this.summary!.set("row count", this.rowCount, this.isPrivate()); } /** * Validate the serialization. Returns null on failure. * @param ser Serialization of a view. */ - public static validateSerialization(ser: IViewSerialization): CommonArgs { + public static validateSerialization(ser: IViewSerialization): CommonArgs | null { if (ser.schema == null || ser.rowCount == null || ser.remoteObjectId == null || ser.provenance == null || ser.title == null || ser.viewKind == null || ser.pageId == null) @@ -615,6 +623,12 @@ export abstract class BigTableView extends TableTargetAPI implements IDataView, } public abstract resize(): void; + + /** + * The refresh method should be able to execute based solely on + * the state serialized by calling "serialize", which is + * reloaded by "reconstruct". + */ public abstract refresh(): void; public getHTMLRepresentation(): HTMLElement { @@ -642,8 +656,13 @@ export abstract class BigTableView extends TableTargetAPI implements IDataView, if (renderer == null) return; - const view = this.dataset.findPage(pageId).dataView; - const rr = this.createSetRequest(view.getRemoteObjectId(), how); + const view = this.dataset.findPage(pageId)?.dataView; + if (view == null) + return; + const rid = view.getRemoteObjectId(); + if (rid === null) + return; + const rr = this.createSetRequest(rid, how); const receiver = renderer(this.getPage(), rr); rr.invoke(receiver); } @@ -671,11 +690,9 @@ export abstract class BaseReceiver extends OnCompleteReceiver { public description: string, protected dataset: DatasetView) { // may be null for the first table super(page, operation, description); - this.remoteObject = null; } public run(value: RemoteObjectId): void { - if (value != null) - this.remoteObject = new TableTargetAPI(value); + this.remoteObject = new TableTargetAPI(value); } } diff --git a/web/src/main/webapp/ui/dialog.ts b/web/src/main/webapp/ui/dialog.ts index 763b9e299..a3873fb59 100644 --- a/web/src/main/webapp/ui/dialog.ts +++ b/web/src/main/webapp/ui/dialog.ts @@ -568,7 +568,7 @@ export class Dialog extends DialogBase { * @return A reference to the select html input field. */ public addColumnSelectField(fieldName: string, labelText: string, - options: DisplayName[], value: DisplayName, + options: DisplayName[], value: DisplayName | null, toolTip: string): HTMLInputElement | HTMLSelectElement { let v = null; if (value !== null) diff --git a/web/src/main/webapp/ui/fullPage.ts b/web/src/main/webapp/ui/fullPage.ts index 8c9f831f1..cddb6b411 100644 --- a/web/src/main/webapp/ui/fullPage.ts +++ b/web/src/main/webapp/ui/fullPage.ts @@ -16,7 +16,7 @@ */ import {DatasetView} from "../datasetView"; -import {makeMissing, makeSpan, openInNewTab, significantDigits,} from "../util"; +import {assertNever, makeMissing, makeSpan, openInNewTab, significantDigits,} from "../util"; import {IDataView} from "./dataview"; import {ErrorDisplay, ErrorReporter} from "./errReporter"; import {TopMenu} from "./menu"; @@ -265,7 +265,7 @@ export class FullPage implements IHtmlElement { const dialog = new Dialog("Change " + SpecialChars.epsilon, "Change the privacy parameter for this view"); dialog.addTextField("epsilon", SpecialChars.epsilon + "=", FieldKind.Double, - this.epsilon.toString(), "Epsilon value"); + this.epsilon!.toString(), "Epsilon value"); dialog.setAction(() => { const eps = dialog.getFieldValueAsNumber("epsilon"); this.dataset.setEpsilon(this.epsilonColumns, eps); @@ -295,6 +295,8 @@ export class FullPage implements IHtmlElement { protected dropped(e: DragEvent): void { e.preventDefault(); + if (e.dataTransfer === null) + return; const payload = e.dataTransfer.getData("text"); const parts = payload.split(":", 2); console.assert(parts.length === 2); @@ -309,7 +311,7 @@ export class FullPage implements IHtmlElement { * @param type Type of payload carried in the drag event. */ public setDragPayload(e: DragEvent, type: DragEventKind): void { - e.dataTransfer.setData("text", type + ":" + this.pageId.toString()); + e.dataTransfer!.setData("text", type + ":" + this.pageId.toString()); } /** @@ -409,7 +411,7 @@ export class FullPage implements IHtmlElement { if (hdv != null) { this.displayHolder.appendChild(hdv.getHTMLRepresentation()); this.setViewKind(hdv.viewKind); - switch (this.dataView.viewKind) { + switch (hdv.viewKind) { case "Histogram": case "2DHistogram": case "Heatmap": @@ -431,7 +433,10 @@ export class FullPage implements IHtmlElement { case "Load": case "SVD Spectrum": case "LogFileView": + case "CorrelationHeatmaps": break; + default: + assertNever(hdv.viewKind); } } } diff --git a/web/src/main/webapp/ui/heatmapLegendPlot.ts b/web/src/main/webapp/ui/heatmapLegendPlot.ts index 5b6bcae46..72f0a0be8 100644 --- a/web/src/main/webapp/ui/heatmapLegendPlot.ts +++ b/web/src/main/webapp/ui/heatmapLegendPlot.ts @@ -22,7 +22,7 @@ import { import {D3Axis, D3Scale, D3SvgElement, Resolution} from "./ui"; import {ContextMenu} from "./menu"; import {HtmlPlottingSurface, PlottingSurface} from "./plottingSurface"; -import {assert, ColorMap, desaturateOutsideRange, Pair} from "../util"; +import {assert, assertNever, ColorMap, desaturateOutsideRange, Pair} from "../util"; import {scaleLinear as d3scaleLinear, scaleLog as d3scaleLog} from "d3-scale"; import {axisBottom as d3axisBottom} from "d3-axis"; import {AxisDescription} from "../dataViews/axisData"; @@ -155,6 +155,8 @@ export class HeatmapLegendPlot extends LegendPlot> { this.colorMap.setMap((x: number) => `rgb( ${Math.round(255 * (1 - x))},${Math.round(255 * (1 - x))},${Math.round(255 * (1 - x))})`); break; + default: + assertNever(kind); } } diff --git a/web/src/main/webapp/ui/heatmapPlot.ts b/web/src/main/webapp/ui/heatmapPlot.ts index b1d59ff21..b2549c1fc 100644 --- a/web/src/main/webapp/ui/heatmapPlot.ts +++ b/web/src/main/webapp/ui/heatmapPlot.ts @@ -17,7 +17,7 @@ import {AxisData, AxisKind} from "../dataViews/axisData"; import {Groups, kindIsString, RowValue} from "../javaBridge"; -import {assert, ColorMap, regression, Triple, valueWithConfidence} from "../util"; +import {ColorMap, regression, Triple, valueWithConfidence} from "../util"; import {Plot} from "./plot"; import {PlottingSurface} from "./plottingSurface"; import {SchemaClass} from "../schemaClass"; @@ -52,7 +52,7 @@ export class HeatmapPlot protected detailIndex: number, protected showAxes: boolean) { super(surface); - this.dots = null; + this.dots = []; this.isPrivate = false; this.showRegression = true; this.regressionLine = null; @@ -76,8 +76,6 @@ export class HeatmapPlot const canvas = this.plottingSurface.getCanvas(); if (this.showAxes) { - assert(this.yAxisData != null); - assert(this.xAxisData != null); canvas.append("text") .text(this.yAxisData.getDisplayNameString(this.schema)) .attr("dominant-baseline", "text-before-edge"); @@ -94,7 +92,7 @@ export class HeatmapPlot htmlCanvas.height = this.getChartHeight(); htmlCanvas.width = this.getChartWidth(); // draw the image onto the canvas. - const ctx: CanvasRenderingContext2D = htmlCanvas.getContext("2d"); + const ctx: CanvasRenderingContext2D = htmlCanvas.getContext("2d")!; for (const dot of this.dots) { ctx.beginPath(); if (dot.confident) { @@ -119,8 +117,8 @@ export class HeatmapPlot if (this.xAxisData != null && this.yAxisData != null && - !kindIsString(this.yAxisData.description.kind) && - !kindIsString(this.xAxisData.description.kind) && + !kindIsString(this.yAxisData.description!.kind) && + !kindIsString(this.xAxisData.description!.kind) && !this.isPrivate && this.showRegression) { // It makes no sense to do regressions for string values. diff --git a/web/src/main/webapp/ui/histogramLegendPlot.ts b/web/src/main/webapp/ui/histogramLegendPlot.ts index faf39588f..d72fb4f21 100644 --- a/web/src/main/webapp/ui/histogramLegendPlot.ts +++ b/web/src/main/webapp/ui/histogramLegendPlot.ts @@ -21,7 +21,7 @@ import {Resolution} from "./ui"; import {SchemaClass} from "../schemaClass"; import {LegendPlot} from "./legendPlot"; import {HtmlPlottingSurface} from "./plottingSurface"; -import {ColorMap, desaturateOutsideRange} from "../util"; +import {assertNever, ColorMap, desaturateOutsideRange} from "../util"; import {kindIsString} from "../javaBridge"; import {interpolateCool as d3interpolateCool, interpolateWarm as d3interpolateWarm} from "d3-scale-chromatic"; import {ColorMapKind} from "./heatmapLegendPlot"; @@ -125,6 +125,8 @@ export class HistogramLegendPlot extends LegendPlot { this.colorMap = (x: number) => `rgb( ${Math.round(255 * (1 - x))},${Math.round(255 * (1 - x))},${Math.round(255 * (1 - x))})`; break; + default: + assertNever(kind); } } diff --git a/web/src/main/webapp/ui/menu.ts b/web/src/main/webapp/ui/menu.ts index b87a3cf5f..c6451097e 100644 --- a/web/src/main/webapp/ui/menu.ts +++ b/web/src/main/webapp/ui/menu.ts @@ -37,17 +37,17 @@ export interface MenuItem extends BaseMenuItem { /** * Action that is executed when the item is selected by the user. */ - readonly action: () => void; + readonly action: (() => void) | null; } -interface subMenuCells { - dummySubMenu: HTMLTableDataCellElement; +interface SubMenuCells { + dummySubMenu: HTMLTableDataCellElement | null; cells: HTMLTableDataCellElement[]; } -function initSubMenu(): subMenuCells { +function initSubMenu(): SubMenuCells { return { - dummySubMenu: undefined, + dummySubMenu: null, cells: [] } } @@ -61,7 +61,7 @@ abstract class BaseMenu implements IHtmlElement { public outer: HTMLTableElement; public tableBody: HTMLTableSectionElement; public cells: HTMLTableDataCellElement[]; - public subMenuCells: subMenuCells[]; + public subMenuCells: SubMenuCells[]; public rows: HTMLTableRowElement[]; public selectedIndex: number; // -1 if no item is selected public selectedParentMenu: number; // -1 if no item is selected @@ -128,7 +128,9 @@ abstract class BaseMenu implements IHtmlElement { return this.cells[index]; } - public expandMenu(parentMenu: string) { + public expandMenu(parentMenu: string | null) { + if (parentMenu === null) + return; const index = this.find(parentMenu); this.selectedParentMenu = index; if (index < 0) throw new Error("Cannot find menu item " + parentMenu); @@ -182,7 +184,7 @@ abstract class BaseMenu implements IHtmlElement { this.hideAllSubMenu(); // Show subMenu when parent menu is active if (cell.classList.contains("expandableMenu")) - this.expandMenu(cell.firstChild.textContent); + this.expandMenu(cell.firstChild!.textContent); cell.classList.add("selected"); this.outer.focus(); } else { @@ -288,7 +290,8 @@ export class ContextMenu extends BaseMenu implements IHtmlElement { */ constructor(parent: Element, mis?: MenuItem[]) { super(); - this.addItems(mis); + if (mis != null) + this.addItems(mis); this.outer.classList.add("dropdown"); this.outer.classList.add("menu"); this.outer.onmouseleave = () => { this.hide(); }; @@ -337,10 +340,10 @@ export class ContextMenu extends BaseMenu implements IHtmlElement { if (cell.classList.contains("expandableMenu")) { // no operation because expandableMenu only expand its submenus } else { - cell.onclick = () => { - this.hide(); - mi.action(); - }; + cell.onclick = () => { + this.hide(); + mi.action!(); + }; } } else { cell.onclick = () => this.hide(); @@ -394,7 +397,7 @@ export class ContextMenu extends BaseMenu implements IHtmlElement { if (mi.action != null && enabled) { cell.onclick = () => { this.hide(); - mi.action(); + mi.action!(); }; } else { cell.onclick = () => this.hide(); @@ -424,7 +427,7 @@ export class SubMenu extends BaseMenu implements IHtmlElement { public setAction(mi: MenuItem, enabled: boolean): void { const cell = this.getCell(mi); if (mi.action != null && enabled) - cell.onclick = (e: MouseEvent) => { e.stopPropagation(); this.hide(); mi.action(); }; + cell.onclick = (e: MouseEvent) => { e.stopPropagation(); this.hide(); mi.action!(); }; else cell.onclick = (e: MouseEvent) => { e.stopPropagation(); this.hide(); }; } @@ -459,7 +462,7 @@ export class TopMenu extends BaseMenu { } public addItem(mi: TopMenuItem, enabled: boolean): HTMLTableDataCellElement { - const cell = this.tableBody.rows.item(0).insertCell(); + const cell = this.tableBody.rows.item(0)!.insertCell(); cell.id = makeId(mi.text); // for testing cell.textContent = mi.text; cell.appendChild(mi.subMenu.getHTMLRepresentation()); @@ -495,7 +498,7 @@ export class TopMenu extends BaseMenu { * Find the position of an item * @param text Text string in the item. */ - public getSubmenu(text: string): SubMenu { + public getSubmenu(text: string): SubMenu | null { for (const item of this.items) if (item.text === text) return item.subMenu; diff --git a/web/src/main/webapp/ui/plottingSurface.ts b/web/src/main/webapp/ui/plottingSurface.ts index 6c394b71d..c139aa03a 100644 --- a/web/src/main/webapp/ui/plottingSurface.ts +++ b/web/src/main/webapp/ui/plottingSurface.ts @@ -18,7 +18,6 @@ import {select as d3select} from "d3-selection"; import {FullPage} from "./fullPage"; import {D3SvgElement, IHtmlElement, Size} from "./ui"; -import {assert} from "../util"; /** * An interface that can be used to specify various dimensions. @@ -127,16 +126,14 @@ export abstract class PlottingSurface { } public getChart(): D3SvgElement { - console.assert(this.chartArea != null); - return this.chartArea; + return this.chartArea!; } /** * Get the canvas of the plotting area. Must be called after create. */ public getCanvas(): D3SvgElement { - console.assert(this.svgCanvas != null); - return this.svgCanvas; + return this.svgCanvas!; } /** @@ -199,8 +196,7 @@ export class HtmlPlottingSurface extends PlottingSurface implements IHtmlElement constructor(parent: HTMLDivElement, public readonly page: FullPage, sb: SizeAndBorders) { super(page, sb); - assert(parent != null); - this.topLevel = parent; + this.topLevel = parent!; this.createObjects(d3select(this.topLevel)); } diff --git a/web/src/main/webapp/util.ts b/web/src/main/webapp/util.ts index b0fc28915..e0c705333 100644 --- a/web/src/main/webapp/util.ts +++ b/web/src/main/webapp/util.ts @@ -64,7 +64,8 @@ export function zip(a: T[], b: S[], f: (ae: T, be: S) => R): R[] { export function histogramAsCsv(data: Groups, schema: SchemaClass, axis: AxisData): string[] { const lines: string[] = []; - let line = JSON.stringify(schema.displayName(axis.description.name).displayName) + ",count"; + const displayName = schema.displayName(axis.description!.name); + let line = JSON.stringify(displayName!.displayName) + ",count"; lines.push(line); for (let x = 0; x < data.perBucket.length; x++) { const bx = axis.bucketDescription(x, 0); @@ -133,7 +134,7 @@ export function dataRange(data: RowValue[], cd: IColumnDescription): BucketsInfo b.allStringsKnown = true; } else { for (let i = 0; i < Resolution.max2DBucketCount; i++) - b.stringQuantiles.push(unique[Math.round(i * unique.length / Resolution.max2DBucketCount)]); + b.stringQuantiles!.push(unique[Math.round(i * unique.length / Resolution.max2DBucketCount)]); b.allStringsKnown = false; } } @@ -144,8 +145,8 @@ export function histogram2DAsCsv( data: Groups>, schema: SchemaClass, axis: AxisData[]): string[] { const lines: string[] = []; - const yAxis = schema.displayName(axis[1].description.name); - const xAxis = schema.displayName(axis[0].description.name); + const yAxis = schema.displayName(axis[1].description!.name); + const xAxis = schema.displayName(axis[0].description!.name); let line = ""; for (let y = 0; y < axis[1].bucketCount; y++) { const by = axis[1].bucketDescription(y, 0); @@ -173,7 +174,7 @@ export function histogram2DAsCsv( export function histogram3DAsCsv( data: Groups>>, schema: SchemaClass, axis: AxisData[]): string[] { let lines: string[] = []; - const gAxis = schema.displayName(axis[2].description.name); + const gAxis = schema.displayName(axis[2].description!.name); for (let g = 0; g < axis[2].bucketCount; g++) { const gl = histogram2DAsCsv(data.perBucket[g], schema, axis); const first = gl[0]; @@ -194,7 +195,7 @@ export function histogram3DAsCsv( export function quartileAsCsv(g: Groups, schema: SchemaClass, axis: AxisData): string[] { const lines: string[] = []; let line = ""; - const axisName = schema.displayName(axis.description.name); + const axisName = schema.displayName(axis.description!.name); for (let x = 0; x < axis.bucketCount; x++) { const bx = axis.bucketDescription(x, 0); const l = JSON.stringify( axisName + " " + bx); @@ -274,12 +275,14 @@ export class Converters { return new Date(value + offset * 60 * 1000); } - public static timeFromDouble(value: number): Date | null { + public static timeFromDouble(value: number | null): Date | null { return Converters.localDateFromDouble(value); } public static doubleFromDate(value: Date | null): number | null { - return value?.getTime(); + if (value === null) + return null; + return value.getTime(); } public static doubleFromLocalDate(value: Date | null): number | null { @@ -354,7 +357,8 @@ export class Converters { } else if (kind === "Date") { return formatDate(Converters.dateFromDouble(val as number)); } else if (kind === "Time" || kind === "Duration") { - return formatTime(Converters.timeFromDouble(val as number), true); + const time = Converters.timeFromDouble(val as number); + return formatTime(time, true); } else if (kindIsString(kind)) { return val as string; } else if (kind == "Interval") { @@ -432,6 +436,8 @@ export class Converters { case "GAxis": result += " Grouping " break; + default: + assertNever(event); } return result + " from " + pageReferenceFormat(pageId); } @@ -455,6 +461,8 @@ export class Converters { case "Interval": str = this.valueToString([filter.doubleValue, filter.intervalEnd], kind, true); break; + default: + assertNever(kind); } return filter.column.name + " " + filter.comparison + " " + str; } @@ -537,7 +545,7 @@ export interface Serializable { * Initialize the current object from the specified object. * @returns The same object if deserialization is successful, null otherwise. */ - deserialize(data: object): T; + deserialize(data: object): T | null; } /** @@ -630,7 +638,7 @@ function zeroPad(num: number, length: number): string { * The suffix may be omitted if it is zero. * This should match the algorithm in the Java Converters.toString(Instant) method. */ -export function formatDate(d: Date): string { +export function formatDate(d: Date | null): string { if (d == null) return "missing"; const year = d.getFullYear(); @@ -643,13 +651,29 @@ export function formatDate(d: Date): string { return df; } +export function assertNever(x: never): never { + throw new Error("Unexpected object: " + x); +} + +export function disableSuggestions(visible: boolean): void { + const vis = visible ? "block" : "none"; + const suggestions = document.getElementById("suggestions"); + if (suggestions !== null) + suggestions.style.display = vis; + const anchor = document.getElementById("suggestions-anchor"); + if (anchor !== null) + anchor.style.display = vis; +} + /** * This is a time encoded as a date. Ignore the date part and just * return the time. * @param d date that only has a time component. * @param nonEmpty if true return 00:00 when the result is empty. */ -export function formatTime(d: Date, nonEmpty: boolean): string { +export function formatTime(d: Date | null, nonEmpty: boolean): string { + if (d === null) + return "missing"; const hour = d.getHours(); const minutes = d.getMinutes(); const seconds = d.getSeconds(); @@ -691,7 +715,7 @@ export function makeId(text: string): string { * Convert a number to an html string by keeping only the most significant digits * and adding a suffix. */ -export function significantDigitsHtml(n: number): HtmlString { +export function significantDigitsHtml(n: number | null): HtmlString | null { if (n === null) return null; let suffix = ""; @@ -956,7 +980,8 @@ export function browserWindowSize(): Size { export function openInNewTab(url: string): void { const win = window.open(url, "_blank"); - win.focus(); + if (win != null) + win.focus(); } /** @@ -1095,7 +1120,7 @@ export class Color { /** * Parse a color in the format rgb(r, g, b). */ - public static parse(s: string): Color { + public static parse(s: string): Color | null { const m = Color.colorReg.exec(s); if (m == null) return null; @@ -1116,7 +1141,7 @@ export class Color { * @param count Number of strings to return. * @returns At most count strings equi-spaced. */ -export function periodicSamples(data: string[], count: number): string[] { +export function periodicSamples(data: string[], count: number): string[] | null { if (data == null) return null; @@ -1140,8 +1165,10 @@ export function desaturateOutsideRange(c: ColorMap, x0: number, x1: number): Col const color = c(value); if (value < min || value > max) { const cValue = Color.parse(color); - const b = cValue.brighten(4); - return b.toString(); + if (cValue != null) { + const b = cValue.brighten(4); + return b.toString(); + } } return color; }