diff --git a/LICENSE b/LICENSE index 06abf2881..b3d71b4a5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 Nomic, Inc. +Copyright 2022-23 Nomic, Inc. =========================== diff --git a/dist/deepscatter.js b/dist/deepscatter.js index a35a99cd7..b7983b912 100644 --- a/dist/deepscatter.js +++ b/dist/deepscatter.js @@ -5583,9 +5583,20 @@ class Zoom { }); } zoom_to_bbox(corners, duration = 4e3, buffer = 1.111) { + console.log("ZOOOOM"); const scales2 = this.scales(); - const [x02, x12] = corners.x.map(scales2.x); - const [y02, y12] = corners.y.map(scales2.y); + let [x02, x12] = corners.x.map(scales2.x); + let [y02, y12] = corners.y.map(scales2.y); + console.log(this.scatterplot.prefs.zoom_align, "AAH"); + if (this.scatterplot.prefs.zoom_align === "right") { + const aspect_ratio = this.width / this.height; + const data_aspect_ratio = (x12 - x02) / (y12 - y02); + if (data_aspect_ratio < aspect_ratio) { + const extension = data_aspect_ratio / aspect_ratio; + console.log(extension, { x0: x02 }); + x02 = x02 - (x12 - x02) * extension; + } + } const { svg_element_selection: canvas, zoomer, width, height } = this; const t = identity$3.translate(width / 2, height / 2).scale(1 / buffer / Math.max((x12 - x02) / width, (y12 - y02) / height)).translate(-(x02 + x12) / 2, -(y02 + y12) / 2); canvas.transition().duration(duration).call(zoomer.transform, t); @@ -17290,6 +17301,12 @@ function isLambdaChannel(input) { function isConstantChannel(input) { return input.constant !== void 0; } +function isURLLabels(labels) { + return labels !== null && labels.url !== void 0; +} +function isLabelset(labels) { + return labels !== null && labels.labels !== void 0; +} var lodash = { exports: {} }; /** * @license @@ -23103,6 +23120,9 @@ class BooleanAesthetic extends Aesthetic { if (input.op === "within") { return [4, input.a, input.b]; } + if (input.op === "between") { + return [4, (input.b - input.a) / 2, (input.b + input.a) / 2]; + } const val = [ [null, "lt", "gt", "eq"].indexOf(input.op), input.a, @@ -34097,8 +34117,7 @@ class QuadTile extends Tile { url = url.replace("/public", ""); } const request = { - method: "GET", - credentials: "include" + method: "GET" }; this._download = fetch(url, request).then(async (response) => { const buffer = await response.arrayBuffer(); @@ -35501,6 +35520,7 @@ class LabelMaker extends Renderer { d.data.y = y_.invert(event.y); }); handler.on("end", (event, d) => { + console.log({ text: d.data.text, x: d.data.x, y: d.data.y }); }); bboxes.call(handler); } @@ -35597,7 +35617,7 @@ class DepthTree extends dist.RBush3D { throw "Missing Aspect Ratio" + JSON.stringify(point); return p; } - insert_point(point, mindepth = 1) { + insert_point(point, mindepth = 1 / 4) { if (point.text === void 0 || point.text === "") { return; } @@ -35660,7 +35680,8 @@ const default_API_call = { encoding: {}, point_size: 1, alpha: 40, - background_options: default_background_options + background_options: default_background_options, + zoom_align: "center" }; const base_elements = [ { @@ -35754,6 +35775,26 @@ class Scatterplot { this.secondary_renderers[name] = labels; this.secondary_renderers[name].start(); } + add_api_label(labelset) { + const geojson = { + type: "FeatureCollection", + features: labelset.labels.map((label) => { + return { + type: "Feature", + geometry: { + type: "Point", + coordinates: [label.x, label.y] + }, + properties: { + text: label.text, + size: label.size || void 0 + } + }; + }) + }; + console.log("OPTIONS", labelset.options); + this.add_labels(geojson, labelset.name, "text", "size", labelset.options || {}); + } async reinitialize() { const { prefs } = this; if (prefs.source_url !== void 0) { @@ -35915,6 +35956,7 @@ class Scatterplot { delete this.hooks[name]; } stop_labellers() { + console.log("Stopping labels"); for (const [k, v] of Object.entries(this.secondary_renderers)) { if (v && v["label_key"] !== void 0) { this.secondary_renderers[k].stop(); @@ -36014,6 +36056,7 @@ class Scatterplot { }); } async unsafe_plotAPI(prefs) { + var _a2; if (prefs === null) { return; } @@ -36031,19 +36074,6 @@ class Scatterplot { prefs.background_options.size = [prefs.background_options.size, 1]; } } - if (prefs.labels) { - const { url, label_field, size_field } = prefs.labels; - const name = prefs.labels.name || url; - if (!this.secondary_renderers[name]) { - this.stop_labellers(); - this.add_labels_from_url(url, name, label_field, size_field).catch( - (error) => { - console.error("Label addition failed."); - console.error(error); - } - ); - } - } this.update_prefs(prefs); if (this._root === void 0) { await this.reinitialize(); @@ -36059,7 +36089,7 @@ class Scatterplot { if (prefs.zoom === null) { this._zoom.zoom_to(1, width / 2, height / 2); prefs.zoom = void 0; - } else if (prefs.zoom.bbox) { + } else if ((_a2 = prefs.zoom) == null ? void 0 : _a2.bbox) { this._zoom.zoom_to_bbox(prefs.zoom.bbox, prefs.duration); } } @@ -36075,6 +36105,31 @@ class Scatterplot { this._renderer.reglframe = this._renderer.regl.frame(() => { this._renderer.tick("Basic"); }); + if (prefs.labels !== void 0) { + if (isURLLabels(prefs.labels)) { + const { url, label_field, size_field } = prefs.labels; + const name = prefs.labels.name || url; + if (!this.secondary_renderers[name]) { + this.stop_labellers(); + this.add_labels_from_url(url, name, label_field, size_field).catch( + (error) => { + console.error("Label addition failed."); + console.error(error); + } + ); + } + } else if (isLabelset(prefs.labels)) { + if (!prefs.labels.name) { + throw new Error("API field `labels` must have a name."); + } + this.stop_labellers(); + this.add_api_label(prefs.labels); + } else if (prefs.labels === null) { + this.stop_labellers(); + } else { + throw new Error("API field `labels` format not recognized."); + } + } this._zoom.restart_timer(6e4); } async root_table() { diff --git a/package.json b/package.json index fc7d97a48..630e5558a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "deepscatter", "type": "module", - "version": "2.9.3", + "version": "2.10.0", "description": "Fast, animated zoomable scatterplots scaling to billions of points", "files": [ "dist" diff --git a/release_notes.md b/release_notes.md index 8caed7672..6332f4ac6 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,9 @@ + +# 2.10.0 + +- Fully supported 'between' as alternative to 'within' for filter operations. +- Allow passing labels through API directly. + # 2.9.2 - Fix bug in manually-assigned categorical color schemes involving the first color always being gray. diff --git a/src/Aesthetic.ts b/src/Aesthetic.ts index fe9b4b3d9..bd07aca66 100644 --- a/src/Aesthetic.ts +++ b/src/Aesthetic.ts @@ -506,6 +506,9 @@ abstract class BooleanAesthetic extends Aesthetic< if (input.op === 'within') { return [4, input.a, input.b]; } + if (input.op === 'between') { + return [4, (input.b - input.a) / 2, (input.b + input.a) / 2]; + } const val: OpArray = [ // Encoding of op as number. [null, 'lt', 'gt', 'eq'].indexOf(input.op), diff --git a/src/deepscatter.ts b/src/deepscatter.ts index f7ee143e0..4429f2752 100644 --- a/src/deepscatter.ts +++ b/src/deepscatter.ts @@ -11,7 +11,7 @@ import { LabelMaker, LabelOptions } from './label_rendering'; import { Renderer } from './rendering'; import { ArrowTile, QuadTile, Tile } from './tile'; import type { ConcreteAesthetic } from './StatefulAesthetic'; - +import { isURLLabels, isLabelset } from './typing'; // DOM elements that deepscatter uses. const base_elements = [ @@ -209,7 +209,28 @@ export default class Scatterplot { this.secondary_renderers[name] = labels; this.secondary_renderers[name].start(); } - + add_api_label( + labelset: Labelset + ) { + const geojson : FeatureCollection = { + type: 'FeatureCollection', + features: labelset.labels.map((label : Label) => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [label.x, label.y], + }, + properties: { + text: label.text, + size: label.size || undefined, + }, + }; + }) + }; + console.log("OPTIONS", labelset.options) + this.add_labels(geojson, labelset.name, 'text', 'size', labelset.options || {}); + } async reinitialize() { const { prefs } = this; if (prefs.source_url !== undefined) { @@ -451,6 +472,7 @@ export default class Scatterplot { } public stop_labellers() { + console.log("Stopping labels") for (const [k, v] of Object.entries(this.secondary_renderers)) { // Stop any existing labels if (v && v['label_key'] !== undefined) { @@ -621,19 +643,7 @@ export default class Scatterplot { prefs.background_options.size = [prefs.background_options.size, 1] } } - if (prefs.labels) { - const { url, label_field, size_field } = prefs.labels; - const name = (prefs.labels.name || url) as string; - if (!this.secondary_renderers[name]) { - this.stop_labellers(); - this.add_labels_from_url(url, name, label_field, size_field).catch( - (error) => { - console.error('Label addition failed.'); - console.error(error); - } - ); - } - } + this.update_prefs(prefs); // Some things have to be done *before* we can actually run this; @@ -644,7 +654,7 @@ export default class Scatterplot { if (this._root === undefined) { await this.reinitialize(); } - + // Doesn't block. /* if (prefs.basemap_geojson) { @@ -676,7 +686,7 @@ export default class Scatterplot { if (prefs.zoom === null) { this._zoom.zoom_to(1, width / 2, height / 2); prefs.zoom = undefined; - } else if (prefs.zoom.bbox) { + } else if (prefs.zoom?.bbox) { this._zoom.zoom_to_bbox(prefs.zoom.bbox, prefs.duration); } } @@ -696,6 +706,33 @@ export default class Scatterplot { this._renderer.tick('Basic'); }); + if (prefs.labels !== undefined) { + if (isURLLabels(prefs.labels)) { + const { url, label_field, size_field } = prefs.labels; + const name = (prefs.labels.name || url) as string; + if (!this.secondary_renderers[name]) { + this.stop_labellers(); + this.add_labels_from_url(url, name, label_field, size_field).catch( + (error) => { + console.error('Label addition failed.'); + console.error(error); + } + ); + } + } else if (isLabelset(prefs.labels)) { + if (!prefs.labels.name) { + throw new Error('API field `labels` must have a name.'); + } + this.stop_labellers(); + this.add_api_label(prefs.labels); + } else if (prefs.labels === null) { + this.stop_labellers() + } else { + throw new Error('API field `labels` format not recognized.'); + } + + } + this._zoom.restart_timer(60_000); } diff --git a/src/defaults.ts b/src/defaults.ts index 8b6e5424f..ab5be4703 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -18,4 +18,5 @@ export const default_API_call: APICall = { point_size: 1, // base size before aes modifications. alpha: 40, // Default screen saturation target. background_options: default_background_options, + zoom_align: 'center' } as const; diff --git a/src/global.d.ts b/src/global.d.ts index 63e87fa05..c5b093bbf 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -199,6 +199,13 @@ declare global { values: Record>; }; + type ZoomCall = { + 'bbox': { + x: [number, number]; + y: [number, number]; + } + } + export type Dimension = keyof Encoding; // Data can be passed in three ways: @@ -220,6 +227,30 @@ declare global { */ export type onZoomCallback = (transform: d3.ZoomTransform) => null; + export type Label = { + x: number, + y: number, + text: string, + } + export type URLLabels = { + url: string, + options: LabelOptions, + label_field: string, + size_field: string, + } + export type LabelOptions = { + useColorScale?: boolean; // Whether the colors of text should inherit from the active color scale. + margin?: number; // The number of pixels around each box. Default 30. + draggable_labels?: boolean; // Should labels be draggable in place? + }; + + export type Labelset = { + labels: Label[], + name: string, + options?: LabelOptions, + } + export type Labelcall = Labelset | URLLabels | null; + // An APICall is a JSON-serializable specification of the chart. export type APICall = { /** The magnification coefficient for a zooming item */ @@ -242,13 +273,14 @@ declare global { // encoding?: Encoding; - + labels?: Labelcall; background_options?: BackgroundOptions; - - bearer_token?: string; + zoom?: ZoomCall; + zoom_align? : undefined | 'right' | 'left' | 'top' | 'bottom' | 'center'; }; + type InitialAPICall = APICall & { encoding: Encoding; } & DataSpec; diff --git a/src/interaction.ts b/src/interaction.ts index de5be4195..4bc4e4625 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -107,12 +107,22 @@ export default class Zoom { }); } - zoom_to_bbox(corners, duration = 4000, buffer = 1.111) { + zoom_to_bbox(corners : Rectangle, duration = 4000, buffer = 1.111, ) { // Zooms to two points. + console.log("ZOOOOM") const scales = this.scales(); - const [x0, x1] = corners.x.map(scales.x); - const [y0, y1] = corners.y.map(scales.y); - + let [x0, x1] = corners.x.map(scales.x); + let [y0, y1] = corners.y.map(scales.y); + console.log(this.scatterplot.prefs.zoom_align, "AAH") + if (this.scatterplot.prefs.zoom_align === 'right') { + const aspect_ratio = this.width / this.height; + const data_aspect_ratio = (x1 - x0) / (y1 - y0); + if (data_aspect_ratio < aspect_ratio) { + const extension = data_aspect_ratio / aspect_ratio; + console.log(extension, {x0}) + x0 = x0 - (x1 - x0) * extension; + } + } const { svg_element_selection: canvas, zoomer, width, height } = this; const t = zoomIdentity diff --git a/src/label_rendering.ts b/src/label_rendering.ts index 37a7f5dcc..82cfc2f39 100644 --- a/src/label_rendering.ts +++ b/src/label_rendering.ts @@ -9,12 +9,6 @@ import type { Tile } from './tile'; const handler = drag(); -export type LabelOptions = { - useColorScale?: boolean; // Whether the colors of text should inherit from the active color scale. - margin?: number; // The number of pixels around each box. Default 30. - draggable_labels?: boolean; // Should labels be draggable in place? -}; - function pixel_ratio(scatterplot: Scatterplot): number { // pixelspace const [px1, px2] = scatterplot._zoom.scales().x.range() as [number, number]; @@ -337,6 +331,7 @@ export class LabelMaker extends Renderer { d.data.y = y_.invert(event.y); }); handler.on('end', (event, d) => { + console.log({text: d.data.text, x: d.data.x, y: d.data.y}); }); bboxes.call(handler); } @@ -518,7 +513,7 @@ class DepthTree extends RBush3D { return p; } - insert_point(point: RawPoint | Point, mindepth = 1) { + insert_point(point: RawPoint | Point, mindepth = 1/4) { if (point.text === undefined || point.text === '') { return; } diff --git a/src/tile.ts b/src/tile.ts index 8c86d838e..516ecade3 100644 --- a/src/tile.ts +++ b/src/tile.ts @@ -419,7 +419,7 @@ export class QuadTile extends Tile { const request: RequestInit = { method: 'GET', - credentials: 'include', +// credentials: 'include', }; this._download = fetch(url, request) diff --git a/src/typing.ts b/src/typing.ts index b747487a2..fc396a0cb 100644 --- a/src/typing.ts +++ b/src/typing.ts @@ -12,7 +12,11 @@ export function isConstantChannel( return (input as ConstantChannel).constant !== undefined; } -const isTypedArray = (function () { - const TypedArray = Object.getPrototypeOf(Uint8Array); - return (obj) => obj instanceof TypedArray; -})(); + +export function isURLLabels(labels: Labelcall): labels is URLLabels { + return labels !== null && (labels as URLLabels).url !== undefined; +} + +export function isLabelset(labels: Labelcall): labels is Labelset { + return labels !== null && (labels as Labelset).labels !== undefined; +} \ No newline at end of file