From 947618f9b6c5250d6157b0e38e0461dbca83aedc Mon Sep 17 00:00:00 2001 From: Ben Schmidt Date: Wed, 3 Apr 2024 18:58:16 -0400 Subject: [PATCH] fix: misc typing improvements with strict --- .eslintrc | 1 + src/aesthetics/Aesthetic.ts | 39 ++++++++-------- src/interaction.ts | 39 ++++++++-------- src/label_rendering.ts | 64 ++++++++++++++++----------- src/regl_rendering.ts | 87 ++++++++++++++---------------------- src/rendering.ts | 17 ++++--- src/scatterplot.ts | 88 ++++++++++++++++++++++++------------- src/utilityFunctions.ts | 20 +-------- tsconfig.json | 2 +- 9 files changed, 180 insertions(+), 177 deletions(-) diff --git a/.eslintrc b/.eslintrc index 55b719926..cc67d8767 100644 --- a/.eslintrc +++ b/.eslintrc @@ -54,6 +54,7 @@ } } ], + "typescript-eslint/no-unnecessary-type-assertion": "off", "unicorn/consistent-destructuring": "off", "unicorn/new-for-builtins": "off", "unicorn/prevent-abbreviations": "off", diff --git a/src/aesthetics/Aesthetic.ts b/src/aesthetics/Aesthetic.ts index 8a8edc1de..63c2a24a1 100644 --- a/src/aesthetics/Aesthetic.ts +++ b/src/aesthetics/Aesthetic.ts @@ -1,7 +1,5 @@ import type { TextureSet } from './AestheticSet'; -import { - isConstantChannel, -} from '../typing'; +import { isConstantChannel } from '../typing'; import { Type, Vector } from 'apache-arrow'; import { StructRowProxy } from 'apache-arrow/row/struct'; import { isNumber } from 'lodash'; @@ -22,7 +20,7 @@ import { Scatterplot } from '../scatterplot'; export abstract class Aesthetic< ChannelType extends DS.ChannelType, Input extends DS.InType = DS.NumberIn, - Output extends DS.OutType = DS.NumberOut + Output extends DS.OutType = DS.NumberOut, > { public abstract default_constant: Output['rangeType']; public abstract default_range: [Output['rangeType'], Output['rangeType']]; @@ -31,8 +29,8 @@ export abstract class Aesthetic< public _texture_buffer: Float32Array | Uint8Array | null = null; protected abstract _func?: (d: Input['domainType']) => Output['rangeType']; public aesthetic_map: TextureSet; - public column : Vector | null; - + public column: Vector | null = null; + // cache of the d3 scale public encoding: ChannelType; public id: string; @@ -40,15 +38,14 @@ export abstract class Aesthetic< encoding: ChannelType | null, scatterplot: Scatterplot, aesthetic_map: TextureSet, - id: string + id: string, ) { this.aesthetic_map = aesthetic_map; if (this.aesthetic_map === undefined) { - throw new Error('Aesthetic map is undefined'); } if (typeof this.aesthetic_map === 'function') { - throw new Error("WTF") + throw new Error('WTF'); } this.scatterplot = scatterplot; @@ -57,7 +54,7 @@ export abstract class Aesthetic< if (encoding === undefined) { throw new Error( - 'Updates with undefined should be handled upstream of the aesthetic.' + 'Updates with undefined should be handled upstream of the aesthetic.', ); } @@ -68,7 +65,7 @@ export abstract class Aesthetic< if (isNumber(encoding)) { throw new Error( - `As of deepscatter 3.0, you must pass {constant: ${encoding}}, not just "${encoding}` + `As of deepscatter 3.0, you must pass {constant: ${encoding}}, not just "${encoding}`, ); } @@ -86,17 +83,17 @@ export abstract class Aesthetic< } abstract apply(point: Datum): Output['rangeType']; - + abstract toGLType(val: Output['rangeType']): Output['glType']; get webGLDomain() { - console.log("No method for webGLDomain") - return [0, 1] as [number, number] + console.log('No method for webGLDomain'); + return [0, 1] as [number, number]; } default_data(): Uint8Array | Float32Array | Array { const default_value = this.toGLType(this.default_constant); return Array(this.aesthetic_map.texture_size).fill( - default_value + default_value, ) as Array; } @@ -118,7 +115,7 @@ export abstract class Aesthetic< return this.aesthetic_map.get_position(this.id); } - get texture_buffer() : Uint8Array { + get texture_buffer(): Uint8Array { if (this._texture_buffer) { return this._texture_buffer as Uint8Array; } @@ -133,14 +130,14 @@ export abstract class Aesthetic< arrow_column(): Vector | null { if (this.column) { - return this.column + return this.column; } if (this.field === null || this.field === undefined) { - return this.column = null; + return (this.column = null); } - return this.column = this.dataset.root_tile.record_batch.getChild(this.field) as Vector< - Input['arrowType'] - >; + return (this.column = this.dataset.root_tile.record_batch.getChild( + this.field, + ) as Vector); } is_dictionary(): boolean { diff --git a/src/interaction.ts b/src/interaction.ts index a6c09066d..9a975b788 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +/* eslint-disable @typescript-eslint/unbound-method */ + import { select } from 'd3-selection'; import { timer } from 'd3-timer'; import { D3ZoomEvent, zoom, zoomIdentity } from 'd3-zoom'; @@ -78,7 +80,6 @@ export class Zoom { .translate(width / 2, height / 2) .scale(k) .translate(-scales.x(x), -scales.y(y)); - canvas.transition().duration(duration).call(zoomer.transform, t); } @@ -102,9 +103,9 @@ export class Zoom { .style('background', 'ivory'), (update) => update.html((d) => - this.scatterplot.tooltip_html(d.data, this.scatterplot) + this.scatterplot.tooltip_html(d.data, this.scatterplot), ), - (exit) => exit.call((e) => e.remove()) + (exit) => exit.call((e) => e.remove()), ); els @@ -150,11 +151,7 @@ export class Zoom { [width, height], ]) .on('zoom', (event: D3ZoomEvent) => { - try { - document.getElementById('tooltipcircle').remove(); - } catch (error) { - // console.log(error); - } + document.getElementById('tooltipcircle')?.remove(); this.transform = event.transform; this.restart_timer(10 * 1000); @@ -179,19 +176,19 @@ export class Zoom { const annotations: Annotation[] = data.map((d) => { return { - x: x_((xdim.apply(d))), - y: y_((ydim.apply(d))), - data: d, - dx: 0, - dy: 30, - } - }) + x: x_(xdim.apply(d)), + y: y_(ydim.apply(d)), + data: d, + dx: 0, + dy: 30, + }; + }); this.html_annotation(annotations); const sel = this.svg_element_selection.select('#mousepoints'); sel .selectAll('circle.label') - .data(data, (d_ : StructRowProxy) => d_.ix as number) // Unique identifier to not remove existing. + .data(data, (d_: StructRowProxy) => d_.ix as number) // Unique identifier to not remove existing. .join( (enter) => enter @@ -208,7 +205,7 @@ export class Zoom { (exit) => exit.call((e) => { e.remove(); - }) + }), ) .on('click', (ev, dd) => { this.scatterplot.click_function(dd, this.scatterplot); @@ -238,14 +235,16 @@ export class Zoom { }); } - current_corners(): Rectangle | undefined { + current_corners(): Rectangle { // The corners of the current zoom transform, in data coordinates. const { width, height } = this; // Use the rescaled versions of the scales. const scales = this.scales(); if (scales === undefined) { - return; + throw new Error( + 'Attempting to get map view before scales have been created', + ); } const { x_, y_ } = scales; @@ -386,7 +385,7 @@ export class Zoom { export function window_transform( x_scale: ScaleLinear, - y_scale: ScaleLinear + y_scale: ScaleLinear, ) { // width and height are svg parameters; x and y scales project from the data x and y into the // the webgl space. diff --git a/src/label_rendering.ts b/src/label_rendering.ts index 350e41f17..8fe83d627 100644 --- a/src/label_rendering.ts +++ b/src/label_rendering.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ import type { GeoJsonObject, GeoJsonProperties } from 'geojson'; import { Renderer } from './rendering'; import { BBox, RBush3D } from 'rbush-3d'; @@ -7,13 +8,15 @@ import { select } from 'd3-selection'; import { drag } from 'd3-drag'; import type * as DS from './shared'; import { Color } from './aesthetics/ColorAesthetic'; +import type { Zoom } from './interaction'; + const handler = drag(); function pixel_ratio(scatterplot: Scatterplot): number { // pixelspace - const [px1, px2] = scatterplot._zoom.scales().x.range() as [number, number]; + const [px1, px2] = scatterplot._zoom!.scales().x.range() as [number, number]; // dataspace - const [dx1, dx2] = scatterplot._zoom.scales().x.domain() as [number, number]; + const [dx1, dx2] = scatterplot._zoom!.scales().x.domain() as [number, number]; const ratio = (px2 - px1) / (dx2 - dx1); return ratio; } @@ -41,14 +44,18 @@ export class LabelMaker extends Renderer { constructor( scatterplot: Scatterplot, id_raw: string, - options: DS.LabelOptions = {} + options: DS.LabelOptions = {}, ) { - super(scatterplot.div.node() as HTMLDivElement, scatterplot._root, scatterplot); + super( + scatterplot.div!.node() as HTMLDivElement, + scatterplot.dataset, + scatterplot, + ); this.options = options; - this.canvas = scatterplot.elements[2] - .selectAll('canvas') + this.canvas = scatterplot + .elements![2].selectAll('canvas') .node() as HTMLCanvasElement; - const svg = scatterplot.elements[3].selectAll('svg').node() as SVGElement; + const svg = scatterplot.elements![3].selectAll('svg').node() as SVGElement; const id = id_raw.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '---'); const labgroup = svg.querySelectorAll(`#${id}`); // eslint-disable-next-line unicorn/prefer-ternary @@ -57,7 +64,7 @@ export class LabelMaker extends Renderer { .select('#labelrects') .append('g') .attr('id', id) - .node(); + .node() as SVGGElement; } else { this.labelgroup = labgroup[1] as SVGGElement; } @@ -65,21 +72,21 @@ export class LabelMaker extends Renderer { if (this.canvas === undefined) { throw new Error('WTF?'); } - this.ctx = this.canvas.getContext('2d'); + this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; this.tree = new DepthTree( this.ctx, pixel_ratio(scatterplot), 0.5, [0.5, 1e6], - options.margin === undefined ? 30 : options.margin + options.margin === undefined ? 30 : options.margin, ); /* this.tree.accessor = (x, y) => { const f = scatterplot._zoom.scales(); return [f.x(x), f.y(y)]; };*/ - this.bind_zoom(scatterplot._renderer.zoom); + this.bind_zoom(scatterplot._renderer!.zoom!); } /** @@ -128,7 +135,7 @@ export class LabelMaker extends Renderer { public update( featureset: GeoJSON.FeatureCollection, label_key: string, - size_key: string | undefined + size_key: string | undefined, // color_key ) { // Insert an entire feature collection all at once. @@ -140,13 +147,17 @@ export class LabelMaker extends Renderer { } if (geometry.type === 'Point') { // The size can be specified; if not, it defaults to 16pt. - const size = (properties[size_key] as number) ?? 16; + const size = + size_key && properties[size_key] + ? (properties[size_key] as number) + : 16; + let label = ''; if ( properties[label_key] !== undefined && properties[label_key] !== null ) { - label = `${properties[label_key]}` + label = `${properties[label_key]}`; } const p: RawPoint = { x: geometry.coordinates[0] + Math.random() * 0.1, @@ -193,7 +204,7 @@ export class LabelMaker extends Renderer { enter .append('rect') .attr('class', 'labellbox') - .style('opacity', RECT_DEFAULT_OPACITY) + .style('opacity', RECT_DEFAULT_OPACITY), ); const Y_BUFFER = 5; @@ -238,7 +249,7 @@ export class LabelMaker extends Renderer { } const exists = (dim.scale.domain() as string[]).indexOf( - datum.properties[dim.field] as string + datum.properties[dim.field] as string, ) > -1; if (exists) { //@ts-expect-error -- it's a string @@ -279,30 +290,31 @@ export class LabelMaker extends Renderer { .attr('class', 'labelbbox') .attr( 'x', - (d: P3d) => x_(d.data.x) - (d.data.pixel_width * this.tree.pixel_ratio) / 2 + (d: P3d) => + x_(d.data.x) - (d.data.pixel_width * this.tree.pixel_ratio) / 2, ) .attr( 'y', (d: P3d) => y_(d.data.y) - (d.data.pixel_height * this.tree.pixel_ratio) / 2 - - Y_BUFFER + Y_BUFFER, ) - .attr('width', (d : P3d) => d.data.pixel_width * this.tree.pixel_ratio) + .attr('width', (d: P3d) => d.data.pixel_width * this.tree.pixel_ratio) .attr('stroke', 'red') .attr( 'height', - (d: P3d) => d.data.pixel_height * this.tree.pixel_ratio + Y_BUFFER * 2 + (d: P3d) => d.data.pixel_height * this.tree.pixel_ratio + Y_BUFFER * 2, ) - .attr('display', (d : P3d) => { - return d.data.properties.__display as string || 'inline'; + .attr('display', (d: P3d) => { + return (d.data.properties.__display as string) || 'inline'; }) .on('mouseover', (event, d) => { select(event.target).style('opacity', RECT_DEFAULT_OPACITY); this.hovered = '' + d.minZ + d.minX; event.stopPropagation(); }) - .on('mousemove', function (event : MouseEvent) { + .on('mousemove', function (event: MouseEvent) { event.stopPropagation(); }) .on('click', (event, d: P3d) => { @@ -319,7 +331,7 @@ export class LabelMaker extends Renderer { d.data.x = x_.invert(event.x); d.data.y = y_.invert(event.y); }); - handler.on('end', (event, d : P3d) => { + handler.on('end', (event, d: P3d) => { console.log({ text: d.data.text, x: d.data.x, y: d.data.y }); }); bboxes.call(handler); @@ -430,7 +442,7 @@ class DepthTree extends RBush3D { pixel_ratio: number, scale_factor = 0.5, zoom = [0.1, 1000], - margin = 10 // in screen pixels + margin = 10, // in screen pixels ) { // scale factor used to determine how quickly points scale. // Not implemented. @@ -537,7 +549,7 @@ class DepthTree extends RBush3D { // The depth until which we're hidden; from min_depth (.1 ish) to max_depth(100 ish) let hidden_until = -1; // The node hiding this one. - let hidden_by : P3d; + let hidden_by: P3d; for (const overlapper of this.search(p3d)) { // Find the most closely overlapping 3d block. // Although the other ones will retain 3d blocks' diff --git a/src/regl_rendering.ts b/src/regl_rendering.ts index 0f3484c94..8d6048ea0 100644 --- a/src/regl_rendering.ts +++ b/src/regl_rendering.ts @@ -6,6 +6,7 @@ import wrapREGL, { Buffer, DrawCommand, DrawConfig, + DefaultContext, } from 'regl'; import { range, sum } from 'd3-array'; // import { contours } from 'd3-contour'; @@ -44,6 +45,7 @@ import { import { Color } from './aesthetics/ColorAesthetic'; import { StatefulAesthetic } from './aesthetics/StatefulAesthetic'; import { Filter, Foreground } from './aesthetics/BooleanAesthetic'; +import { ZoomTransform } from 'd3-zoom'; // eslint-disable-next-line import/prefer-default-export export class ReglRenderer extends Renderer { public regl: Regl; @@ -52,7 +54,6 @@ export class ReglRenderer extends Renderer { private _buffers: MultipurposeBufferSet; public _initializations: Promise; public dataset: Dataset; - public zoom?: Zoom; public _zoom?: Zoom; public most_recent_restart?: number; public _default_webgl_scale?: number[]; @@ -133,14 +134,20 @@ export class ReglRenderer extends Renderer { // Would be better cached per draw call. this.allocate_aesthetic_buffers(); + if (!this.zoom) { + throw new Error('Unable to draw before zoom state set up.'); + } + if (!this.most_recent_restart) + throw new Error('Failed to populate restart'); const { prefs, aes_to_buffer_num, buffer_num_to_variable, variable_to_buffer_num, } = this; - const transform = this.zoom.transform; - const colorScales = this.aes.dim('color'); + const transform: ZoomTransform = this.zoom + .transform as unknown as ZoomTransform; + const colorScales = this.aes.dim('color') as StatefulAesthetic; const [currentColor, lastColor] = [ colorScales.current, colorScales.last, @@ -172,9 +179,9 @@ export class ReglRenderer extends Renderer { last_webgl_scale: this._webgl_scale_history[1], use_scale_for_tiles: this._use_scale_to_download_tiles, grid_mode: 0, - buffer_num_to_variable, - aes_to_buffer_num, - variable_to_buffer_num, + buffer_num_to_variable: buffer_num_to_variable!, + aes_to_buffer_num: aes_to_buffer_num!, + variable_to_buffer_num: variable_to_buffer_num!, color_picker_mode: 0, // whether to draw as a color picker. position_interpolation: this.aes.position_interpolation, zoom_matrix: [ @@ -297,8 +304,7 @@ export class ReglRenderer extends Renderer { try { this.render_all(props); } catch (error) { - console.warn('ERROR NOTED'); - this.reglframe.cancel(); + this.reglframe!.cancel(); throw error; } } @@ -665,16 +671,16 @@ export class ReglRenderer extends Renderer { const { props } = this; props.only_color = only_color; - let v: number; + let v: number = -1; this.fbos.contour.use(() => { this.regl.clear({ color: [0, 0, 0, 0] }); // read onto the contour vals. this.render_points(props); - this.regl.read(this.contour_vals); + this.regl.read(this.contour_vals as Uint8Array); // Could be done faster on the GPU itself. // But would require writing to float textures, which // can be hard. - v = sum(this.contour_vals); + v = sum(this.contour_vals as Uint8Array); }); return v; } @@ -757,29 +763,6 @@ export class ReglRenderer extends Renderer { return point_as_int; } - /* blur(fbo) { - var passes = []; - var radii = [Math.round( - Math.max(1, state.bloom.radius * pixelRatio / state.bloom.downsample))]; - for (var radius = nextPow2(radii[0]) / 2; radius >= 1; radius /= 2) { - radii.push(radius); - } - radii.forEach(radius => { - for (var pass = 0; pass < state.bloom.blur.passes; pass++) { - passes.push({ - kernel: 13, - src: bloomFbo[0], - dst: bloomFbo[1], - direction: [radius, 0] - }, { - kernel: 13, - src: bloomFbo[1], - dst: bloomFbo[0], - direction: [0, radius] - }); - } - }) -} */ get fill_buffer() { // if (!this._fill_buffer) { @@ -811,14 +794,14 @@ export class ReglRenderer extends Renderer { this.regl.clear({ color: [0, 0, 0, 0] }); // read onto the contour vals. this.render_points(props); - this.regl.read(this.contour_vals); + this.regl.read(this.contour_vals!); }); // 3-pass blur this.blur(this.fbos.contour, this.fbos.ping, 3); this.fbos.contour.use(() => { - this.regl.read(this.contour_vals); + this.regl.read(this.contour_vals!); }); let i = 0; @@ -834,6 +817,7 @@ export class ReglRenderer extends Renderer { const { regl } = this; // This should be scoped somewhere to allow resizing. type P = DS.TileDrawProps; + type C = DefaultContext; const parameters: DrawConfig = { depth: { enable: false }, stencil: { enable: false }, @@ -862,7 +846,7 @@ export class ReglRenderer extends Renderer { u_transition_duration(_, props: P) { return props.prefs.duration; // Using seconds, not milliseconds, in there }, - u_only_color(_, props: P) { + u_only_color(_: C, props: P) { if (props.only_color !== undefined) { return props.only_color; } @@ -871,7 +855,7 @@ export class ReglRenderer extends Renderer { // Other values plot a specific value of the color-encoded field. return -2; }, - u_wrap_colors_after: (_, { wrap_colors_after }: P) => { + u_wrap_colors_after: (_: unknown, { wrap_colors_after }: P) => { if (wrap_colors_after === undefined) { throw new Error('wrap_colors_after is undefined'); } @@ -893,7 +877,7 @@ export class ReglRenderer extends Renderer { } return 0; }, - u_grid_mode: (_, { grid_mode }: P) => grid_mode, + u_grid_mode: (_: C, { grid_mode }: P) => grid_mode, u_colors_as_grid: (_, { colors_as_grid }: P) => colors_as_grid, /* u_constant_color: () => (this.aes.dim("color").current.constant !== undefined ? this.aes.dim("color").current.constant @@ -901,19 +885,19 @@ export class ReglRenderer extends Renderer { u_constant_last_color: () => (this.aes.dim("color").last.constant !== undefined ? this.aes.dim("color").last.constant : [-1, -1, -1]),*/ - u_tile_id: (_, props: P) => props.tile_id, - u_width: ({ viewportWidth }) => viewportWidth as number, - u_height: ({ viewportHeight }) => viewportHeight as number, + u_tile_id: (_: C, props: P) => props.tile_id, + u_width: ({ viewportWidth }: C) => viewportWidth, + u_height: ({ viewportHeight }: C) => viewportHeight, u_one_d_aesthetic_map: this.aes.aesthetic_map.one_d_texture, u_color_aesthetic_map: this.aes.aesthetic_map.color_texture, - u_aspect_ratio: ({ viewportWidth, viewportHeight }) => + u_aspect_ratio: ({ viewportWidth, viewportHeight }: C) => viewportWidth / viewportHeight, //@ts-expect-error Don't know about regl props. u_zoom_balance: regl.prop('zoom_balance'), - u_base_size: (_, { point_size }) => point_size as number, - u_maxix: (_, { max_ix }) => max_ix as number, - u_alpha: (_, { alpha }) => alpha as number, - u_foreground_number: (_, { foreground }) => foreground as number, + u_base_size: (_: C, { point_size }: P) => point_size, + u_maxix: (_: C, { max_ix }: P) => max_ix, + u_alpha: (_: C, { alpha }: P) => alpha, + u_foreground_number: (_: C, { foreground }: P) => foreground as number, u_foreground_alpha: () => this.render_props.foreground_opacity, u_background_rgba: () => { const color = this.prefs.background_options.color; @@ -929,7 +913,7 @@ export class ReglRenderer extends Renderer { this.prefs.background_options.mouseover ? 1 : 0, u_background_size: () => this.render_props.background_size, u_foreground_size: () => this.render_props.foreground_size, - u_k: (_, props: P) => { + u_k: (_: DefaultContext, props: P) => { return props.transform.k; }, // Allow interpolation between different coordinate systems. @@ -940,7 +924,7 @@ export class ReglRenderer extends Renderer { u_time: ({ time }: P) => time, u_jitter: () => this.aes.jitter_int_format('current'), u_last_jitter: () => this.aes.jitter_int_format('last'), - u_zoom(_, props: P) { + u_zoom(_: C, props: P) { return props.zoom_matrix; }, }, @@ -1285,10 +1269,7 @@ export class TileBufferManager { if (!column.type || column.type.typeId !== Type.Float32) { const buffer = new Float32Array(tile.record_batch.numRows); const source_buffer = column.data[0]; - - if (column.type['dictionary']) { - // We set the dictionary values down by 2047 so that we can use - // even half-precision floats for direct indexing. + if (column.type.typeId === Type.Dictionary) { for (let i = 0; i < tile.record_batch.numRows; i++) { buffer[i] = (source_buffer as Data>).values[i]; } diff --git a/src/rendering.ts b/src/rendering.ts index 30395c2da..2244357dd 100644 --- a/src/rendering.ts +++ b/src/rendering.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ /* eslint-disable no-underscore-dangle */ import { BaseType, select } from 'd3-selection'; import { min } from 'd3-array'; @@ -130,7 +131,6 @@ export class Renderer { // The renderer handles periodic dispatches of calls public deferred_functions: Array<() => Promise | void>; public _use_scale_to_download_tiles = true; - public zoom?: Zoom; public aes?: AestheticSet; public _zoom?: Zoom; public render_props: RenderProps = new RenderProps(); @@ -142,7 +142,7 @@ export class Renderer { this.scatterplot = scatterplot; this.holder = select(selector as string); this.canvas = select( - this.holder.node().firstElementChild, + this.holder!.node()!.firstElementChild, ).node() as HTMLCanvasElement; this.dataset = tileSet; this.width = +select(this.canvas).attr('width'); @@ -191,8 +191,7 @@ export class Renderer { const total_intended_points = min([ max_ix, this.dataset.highest_known_ix || 1e10, - ]); - + ]) as number; const total_points = total_intended_points * (1 - discard_share); const size_adjust = Math.exp(Math.log(k) * zoom_balance); @@ -217,7 +216,7 @@ export class Renderer { if (!this._use_scale_to_download_tiles) { return max_points; } - const k = this.zoom.transform.k; + const k = this.zoom.transform!.k; const point_size_adjust = Math.exp(Math.log(k) * prefs.zoom_balance); return (max_points * k * k) / point_size_adjust / point_size_adjust; } @@ -229,6 +228,7 @@ export class Renderer { const { dataset: tileSet } = this; // Materialize using a tileset method. + if (!this.aes) throw new Error('Aesthetic missing'); const x = this.aes.dim('x') as StatefulAesthetic; const y = this.aes.dim('x') as StatefulAesthetic; const natural_display = @@ -252,8 +252,13 @@ export class Renderer { return all_tiles; } + get zoom(): Zoom { + if (this._zoom === undefined) throw new Error('Zoom state not yet bound'); + return this._zoom as Zoom; + } + bind_zoom(zoom: Zoom) { - this.zoom = zoom; + this._zoom = zoom; return this; } diff --git a/src/scatterplot.ts b/src/scatterplot.ts index f7d948808..8e04c3dc0 100644 --- a/src/scatterplot.ts +++ b/src/scatterplot.ts @@ -207,6 +207,9 @@ export class Scatterplot { duration = this.prefs.duration, ): Promise { const selection = await this.select_data(params); + if (selection === null) { + throw new Error(`Invalid selection: ${JSON.stringify(params)}`); + } await selection.ready; await this.plotAPI({ duration, @@ -268,7 +271,11 @@ export class Scatterplot { * **or** a keyed of values like `{'Rome': 3, 'Vienna': 13}` in which case the numeric values will be used. * @param key_field The field in which to look for the identifiers. */ - join(name: string, codes: Record, key_field: string) { + join( + name: string, + codes: Record | string[], + key_field: string, + ) { let true_codes: Record; if (Array.isArray(codes)) { @@ -276,8 +283,10 @@ export class Scatterplot { codes.map((next: string | bigint) => [String(next), 1]), ); } else { - this._root.add_label_identifiers(true_codes, name, key_field); + true_codes = codes; } + + this.dataset.add_label_identifiers(true_codes, name, key_field); } async add_labels_from_url( @@ -412,7 +421,7 @@ export class Scatterplot { HTMLDivElement, HTMLCanvasElement >; - const ctx = bkgd.node().getContext('2d'); + const ctx = bkgd.node()!.getContext('2d'); if (ctx === null) throw new Error("Can't acquire canvas context"); ctx.fillStyle = prefs.background_color ?? 'rgba(133, 133, 111, .8)'; @@ -460,18 +469,18 @@ export class Scatterplot { * loaded tiles. Useful for debugging and illustration. */ - const canvas = this.elements[2] - .selectAll('canvas') - .node() as HTMLCanvasElement; + const canvas = this.elements![2].selectAll( + 'canvas', + ).node() as HTMLCanvasElement; - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; // as CanvasRenderingContext2D; ctx.clearRect(0, 0, 10_000, 10_000); - const { x_, y_ } = this._zoom.scales(); + const { x_, y_ } = this._zoom!.scales(); ctx.strokeStyle = '#888888'; - const tiles = this._root.map((t) => t); + const tiles = this.dataset.map((t) => t); for (const i of range(20)) { setTimeout(() => { for (const tile of tiles) { @@ -507,16 +516,18 @@ export class Scatterplot { this._renderer?.regl?.destroy(); const node = this.div?.node() as Node; - node.parentElement.replaceChildren(); + node.parentElement!.replaceChildren(); } update_prefs(prefs: DS.APICall) { // Stash the previous values for interpolation. if (this.prefs.encoding && prefs.encoding) { - for (const k of Object.keys(this.prefs.encoding)) { + for (const k of Object.keys( + this.prefs.encoding, + ) as (keyof DS.Encoding)[]) { if (prefs.encoding[k] !== undefined) { - this.prefs.encoding[k] = prefs.encoding[k] as DS.Encoding; + this.prefs.encoding[k] = prefs.encoding[k]; } } } @@ -553,7 +564,7 @@ export class Scatterplot { if (v && v['label_key'] !== undefined) { (this.secondary_renderers[k] as LabelMaker).stop(); (this.secondary_renderers[k] as LabelMaker).delete(); - this.secondary_renderers[k] = undefined; + delete this.secondary_renderers[k]; } } } @@ -567,7 +578,7 @@ export class Scatterplot { */ public dim(dimension: DS.Dimension): ConcreteAesthetic { - return this._renderer.aes.dim(dimension).current; + return this._renderer!.aes.dim(dimension)!.current; } set tooltip_html(func) { @@ -769,32 +780,33 @@ export class Scatterplot { if (this._zoom === undefined) { await this.reinitialize(); } - - this._renderer.render_props.apply_prefs(this.prefs); + const renderer = this._renderer as ReglRenderer; + const zoom = this._zoom as Zoom; + this._renderer!.render_props.apply_prefs(this.prefs); const { width, height } = this; this.update_prefs(prefs); if (prefs.zoom !== undefined) { if (prefs.zoom === null) { - this._zoom.zoom_to(1, width / 2, height / 2); + zoom.zoom_to(1, width / 2, height / 2); prefs.zoom = undefined; } else if (prefs.zoom?.bbox) { - this._zoom.zoom_to_bbox(prefs.zoom.bbox, prefs.duration); + zoom.zoom_to_bbox(prefs.zoom.bbox, prefs.duration); } } - this._renderer.most_recent_restart = Date.now(); - this._renderer.aes.apply_encoding(prefs.encoding ?? {}); + renderer.most_recent_restart = Date.now(); + renderer.aes.apply_encoding(prefs.encoding ?? {}); - if (this._renderer.reglframe) { - const r = this._renderer.reglframe; + if (renderer.reglframe) { + const r = renderer.reglframe; r.cancel(); - this._renderer.reglframe = undefined; + renderer.reglframe = undefined; } - this._renderer.reglframe = this._renderer.regl.frame(() => { - this._renderer.tick(); + renderer.reglframe = renderer.regl.frame(() => { + renderer.tick(); }); if (prefs.labels !== undefined) { @@ -827,7 +839,7 @@ export class Scatterplot { } } - this._zoom.restart_timer(60_000); + zoom.restart_timer(60_000); } get root_batch() { @@ -843,13 +855,27 @@ export class Scatterplot { */ get query(): DS.APICall { const p = JSON.parse(JSON.stringify(this.prefs)) as DS.APICall; - p.zoom = { bbox: this._renderer.zoom.current_corners() }; + p.zoom = { bbox: this.renderer.zoom.current_corners() }; return p; } + get renderer(): ReglRenderer { + if (this._renderer === undefined) { + throw new Error('No renderer has been initialized'); + } + return this._renderer; + } + + get zoom(): Zoom { + if (this._zoom === undefined) { + throw new Error('No zoom has been initialized'); + } + return this._zoom; + } + sample_points(n = 10): Record[] { const vals: Record[] = []; - for (const p of this._root.points(this._zoom.current_corners())) { + for (const p of this.dataset.points(this.zoom.current_corners())) { vals.push({ ...p }); if (vals.length >= n * 3) { break; @@ -893,7 +919,7 @@ class LabelClick extends SettableFunction { default( feature: GeoJsonProperties, // eslint-disable-next-line @typescript-eslint/no-unused-vars - plot: Scatterplot = undefined, + plot: Scatterplot | undefined = undefined, labelset: LabelMaker | undefined = undefined, ) { let filter: DS.LambdaChannel | null; @@ -931,14 +957,14 @@ class ChangeToHighlitPointFunction extends SettableFunction< StructRowProxy[] > { // eslint-disable-next-line @typescript-eslint/no-unused-vars - default(points: StructRowProxy[], plot: Scatterplot = undefined) { + default(points: StructRowProxy[], plot: Scatterplot | undefined = undefined) { return; } } class TooltipHTML extends SettableFunction { // eslint-disable-next-line @typescript-eslint/no-unused-vars - default(point: StructRowProxy, plot: Scatterplot = undefined) { + default(point: StructRowProxy, plot: Scatterplot | undefined = undefined) { // By default, this returns a let output = '
'; const nope: Set = new Set([ diff --git a/src/utilityFunctions.ts b/src/utilityFunctions.ts index 63c0a7f53..32eb14e09 100644 --- a/src/utilityFunctions.ts +++ b/src/utilityFunctions.ts @@ -9,7 +9,7 @@ import { makeVector, } from 'apache-arrow'; -type IndicesType = null | Int8Array | Int16Array | Int32Array; +type IndicesType = Int8Array | Int16Array | Int32Array; type DictionaryType = Dictionary; // We need to keep track of the current dictionary number @@ -82,21 +82,3 @@ function createDictionaryWithVector( return returnval; } - -export function LazyGetter() { - return function (target: any, key: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.get; - - descriptor.get = function () { - const cacheKey = `_____${key}`; - - if (!this[cacheKey]) { - this[cacheKey] = originalMethod.call(this); - } - - return this[cacheKey]; - }; - - return descriptor; - }; -} diff --git a/tsconfig.json b/tsconfig.json index a2293466a..984330cd8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "noUnusedLocals": true, "experimentalDecorators": true, "sourceMap": true, - "strict": false, + "strict": true, "target": "es2020", "isolatedModules": true, "outDir": "./dist",