From 34994fe57d8e0cad3b2109e4ec1ec12b0b9d05ce Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 2 Feb 2025 14:11:00 -0800 Subject: [PATCH 01/37] feat: mvp of Publish package --- packages/inputs/src/Menu.js | 9 +- packages/inputs/src/Search.js | 9 +- packages/inputs/src/Slider.js | 9 +- packages/inputs/src/Table.js | 23 +- packages/inputs/src/input.js | 24 +- packages/plot/src/interactors/Highlight.js | 9 +- packages/plot/src/interactors/Interactor.js | 29 ++ packages/plot/src/interactors/Interval1D.js | 5 +- packages/plot/src/interactors/Interval2D.js | 5 +- packages/plot/src/interactors/Nearest.js | 15 +- packages/plot/src/interactors/PanZoom.js | 28 +- packages/plot/src/interactors/Region.js | 5 +- packages/plot/src/interactors/Toggle.js | 12 +- packages/publish/README.md | 27 ++ packages/publish/package.json | 27 ++ packages/publish/src/MosaicPublisher.ts | 315 ++++++++++++++++++++ packages/publish/src/index.ts | 92 ++++++ packages/publish/src/util/Logger.ts | 25 ++ packages/publish/src/util/constants.ts | 171 +++++++++++ packages/publish/src/util/index.ts | 4 + packages/publish/src/util/publishHelpers.ts | 36 +++ packages/publish/src/util/renderHelpers.ts | 32 ++ packages/publish/tsconfig.json | 14 + 23 files changed, 875 insertions(+), 50 deletions(-) create mode 100644 packages/plot/src/interactors/Interactor.js create mode 100644 packages/publish/README.md create mode 100644 packages/publish/package.json create mode 100644 packages/publish/src/MosaicPublisher.ts create mode 100755 packages/publish/src/index.ts create mode 100644 packages/publish/src/util/Logger.ts create mode 100644 packages/publish/src/util/constants.ts create mode 100644 packages/publish/src/util/index.ts create mode 100644 packages/publish/src/util/publishHelpers.ts create mode 100644 packages/publish/src/util/renderHelpers.ts create mode 100644 packages/publish/tsconfig.json diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index bcfbb60c2..cc7053bbb 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -1,6 +1,6 @@ -import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; +import { Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; -import { input } from './input.js'; +import { input, Input } from './input.js'; const isObject = v => { return v && typeof v === 'object' && !Array.isArray(v); @@ -8,7 +8,7 @@ const isObject = v => { export const menu = options => input(Menu, options); -export class Menu extends MosaicClient { +export class Menu extends Input { /** * Create a new menu input. * @param {object} [options] Options object @@ -125,8 +125,7 @@ export class Menu extends MosaicClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(clausePoint(this.field, 0, { source: this })); + if (isSelection(this.selection)) this.selection.activate(clausePoint(this.field, 0, { source: this })); } publish(value) { diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index 4bdc838d6..6dcdd9c26 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -1,12 +1,12 @@ -import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; +import { Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; -import { input } from './input.js'; +import { input, Input } from './input.js'; let _id = 0; export const search = options => input(Search, options); -export class Search extends MosaicClient { +export class Search extends Input { /** * Create a new text search input. * @param {object} [options] Options object @@ -97,8 +97,7 @@ export class Search extends MosaicClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(this.clause('')); + if (isSelection(this.selection)) this.selection.activate(this.clause('')); } publish(value) { diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index 3f5cefa54..bbb4cd3d8 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -1,12 +1,12 @@ -import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; +import { Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; import { Query, max, min } from '@uwdata/mosaic-sql'; -import { input } from './input.js'; +import { input, Input } from './input.js'; let _id = 0; export const slider = options => input(Slider, options); -export class Slider extends MosaicClient { +export class Slider extends Input { /** * Create a new slider input. * @param {object} [options] Options object @@ -165,8 +165,7 @@ export class Slider extends MosaicClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(this.clause(0)); + if (isSelection(this.selection)) this.selection.activate(this.clause(0)); } publish(value) { diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index f02c75db3..12f9667c0 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -1,13 +1,13 @@ -import { MosaicClient, clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; +import { clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; -import { input } from './input.js'; +import { input, Input } from './input.js'; let _id = -1; export const table = options => input(Table, options); -export class Table extends MosaicClient { +export class Table extends Input { /** * Create a new Table instance. * @param {object} options Options object @@ -208,6 +208,23 @@ export class Table extends MosaicClient { return this; } + activate() { + const { data } = this; + const n = data.length - 1; + + const { numRows } = data[n]; + + for (let i = 0; i < numRows; i++) { + this.currentRow = i; + this.selection.update(this.clause([i])); + } + + if (numRows > 0) { + this.currentRow = -1; + this.selection.update(this.clause()); + } + } + sort(event, column) { if (column === this.sortColumn) { this.sortDesc = !this.sortDesc; diff --git a/packages/inputs/src/input.js b/packages/inputs/src/input.js index e4f788da3..8b7263896 100644 --- a/packages/inputs/src/input.js +++ b/packages/inputs/src/input.js @@ -1,7 +1,27 @@ -import { coordinator } from '@uwdata/mosaic-core'; + +// TODO: change this to be at same level as MosaicClient to avoid duplicate in interactors +import { coordinator, MosaicClient } from '@uwdata/mosaic-core'; + +/** + * Base class for all inputs. + * This class should not be instantiated directly. + */ +export class Input extends MosaicClient { + constructor(filterBy) { + super(filterBy); + } + + /** + * Activates the input. + * @throws {Error} If the method is not implemented by the subclass. + */ + activate() { + throw new Error('activate method must be implemented by subclass'); + } +} export function input(InputClass, options) { const input = new InputClass(options); coordinator().connect(input); return input.element; -} +} \ No newline at end of file diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index be10c26f8..34879f1a6 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -2,6 +2,7 @@ import { throttle } from '@uwdata/mosaic-core'; import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +import { Interactor } from './Interactor.js'; function configureMark(mark) { const { channels } = mark; @@ -34,12 +35,12 @@ function configureMark(mark) { return mark; } -export class Highlight { +export class Highlight extends Interactor { constructor(mark, { selection, channels = {} }) { - this.mark = configureMark(mark); + super(configureMark(mark)); this.selection = selection; const c = Object.entries(sanitizeStyles(channels)); this.channels = c.length ? c : [['opacity', 0.2]]; @@ -80,6 +81,10 @@ export class Highlight { } } } + + activate() { + return this.update(); + } } async function predicateFunction(mark, selection) { diff --git a/packages/plot/src/interactors/Interactor.js b/packages/plot/src/interactors/Interactor.js new file mode 100644 index 000000000..e17c50839 --- /dev/null +++ b/packages/plot/src/interactors/Interactor.js @@ -0,0 +1,29 @@ +/* eslint-disable no-unused-vars */ +// TODO: change this to be at same level as MosaicClient to avoid duplicate in inputs + +/** + * Base class for all interactors. + * This class should not be instantiated directly. + */ +export class Interactor { + constructor(mark) { + this.mark = mark; + } + + /** + * Initializes the interactor. + * @param {SVGElement} svg - The SVG element. + * @throws {Error} If the method is not implemented by the subclass. + */ + init(svg) { + throw new Error('init method must be implemented by subclass'); + } + + /** + * Activates the interactor. + * @throws {Error} If the method is not implemented by the subclass. + */ + activate() { + throw new Error('activate method must be implemented by subclass'); + } + } \ No newline at end of file diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index c6e8b772f..1e5885f54 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -5,8 +5,9 @@ import { closeTo } from './util/close-to.js'; import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +import { Interactor } from './Interactor.js'; -export class Interval1D { +export class Interval1D extends Interactor { constructor(mark, { channel, selection, @@ -15,7 +16,7 @@ export class Interval1D { peers = true, brush: style }) { - this.mark = mark; + super(mark); this.channel = channel; this.pixelSize = pixelSize || 1; this.selection = selection; diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index 9774f81cd..c9ca2af64 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -5,8 +5,9 @@ import { closeTo } from './util/close-to.js'; import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +import { Interactor } from './Interactor.js'; -export class Interval2D { +export class Interval2D extends Interactor { constructor(mark, { selection, xfield, @@ -15,7 +16,7 @@ export class Interval2D { peers = true, brush: style }) { - this.mark = mark; + super(mark); this.pixelSize = pixelSize || 1; this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index b1dd5fb44..2eddfd8d9 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -1,8 +1,9 @@ import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; +import { Interactor } from './Interactor.js'; -export class Nearest { +export class Nearest extends Interactor { constructor(mark, { selection, pointer, @@ -10,7 +11,7 @@ export class Nearest { fields, maxRadius = 40 }) { - this.mark = mark; + super(mark); this.selection = selection; this.clients = new Set().add(mark); this.pointer = pointer; @@ -71,12 +72,14 @@ export class Nearest { // trigger activation updates svg.addEventListener('pointerenter', evt => { - if (!evt.buttons) { - const v = this.channels.map(() => 0); - selection.activate(this.clause(v)); - } + if (!evt.buttons) this.activate(); }); } + + activate() { + const v = this.channels.map(() => 0); + this.selection.activate(this.clause(v)); + } } /** diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index 5b9cc8535..62558d58d 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -1,10 +1,11 @@ import { Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; +import { Interactor } from './Interactor.js'; const asc = (a, b) => a - b; -export class PanZoom { +export class PanZoom extends Interactor { constructor(mark, { x = new Selection(), y = new Selection(), @@ -14,7 +15,7 @@ export class PanZoom { panx = true, pany = true }) { - this.mark = mark; + super(mark); this.xsel = x; this.ysel = y; this.xfield = xfield || getField(mark, 'x'); @@ -59,7 +60,7 @@ export class PanZoom { this.svg = svg; if (this.initialized) return; else this.initialized = true; - const { panx, pany, mark: { plot: { element } }, xsel, ysel } = this; + const { panx, pany, mark: { plot: { element } }} = this; this.xscale = svg.scale('x'); this.yscale = svg.scale('y'); @@ -85,19 +86,22 @@ export class PanZoom { let enter = false; element.addEventListener('pointerenter', evt => { if (enter) return; else enter = true; - if (evt.buttons) return; // don't activate if mouse down - if (panx) { - const { xscale, xfield } = this; - xsel.activate(this.clause(xscale.domain, xfield, xscale)); - } - if (pany) { - const { yscale, yfield } = this; - ysel.activate(this.clause(yscale.domain, yfield, yscale)); - } + if (!evt.buttons) this.activate(); // don't activate if mouse down }); element.addEventListener('pointerleave', () => enter = false); } } + + activate() { + if (this.panx) { + const { xscale, xfield } = this; + this.xsel.activate(this.clause(xscale.domain, xfield, xscale)); + } + if (this.pany) { + const { yscale, yfield } = this; + this.ysel.activate(this.clause(yscale.domain, yfield, yscale)); + } + } } function extent(ext, defaultTrue, defaultFalse) { diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index 76ffc2b0b..ab729ab78 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -7,8 +7,9 @@ import { patchScreenCTM } from './util/patchScreenCTM.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; +import { Interactor } from './Interactor.js'; -export class Region { +export class Region extends Interactor { constructor(mark, { channels, selection, @@ -19,7 +20,7 @@ export class Region { strokeDasharray: '1,1' } }) { - this.mark = mark; + super(mark); this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index 5dc55fa69..1109c6b7c 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -1,8 +1,9 @@ import { clausePoints } from '@uwdata/mosaic-core'; import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; +import { Interactor } from './Interactor.js'; -export class Toggle { +export class Toggle extends Interactor { /** * @param {*} mark The mark to interact with. * @param {*} options The interactor options. @@ -12,8 +13,8 @@ export class Toggle { channels, peers = true }) { + super(mark) this.value = null; - this.mark = mark; this.selection = selection; this.peers = peers; const fields = this.fields = []; @@ -75,10 +76,13 @@ export class Toggle { }); svg.addEventListener('pointerenter', evt => { - if (evt.buttons) return; - this.selection.activate(this.clause([this.fields.map(() => 0)])); + if (!evt.buttons) this.activate(); }); } + + activate() { + this.selection.activate(this.clause([this.fields.map(() => 0)])); + } } function isTargetElement(groups, node) { diff --git a/packages/publish/README.md b/packages/publish/README.md new file mode 100644 index 000000000..29bd92c36 --- /dev/null +++ b/packages/publish/README.md @@ -0,0 +1,27 @@ +# mosaic-publish TODO: Adjust this doc + +A CLI tool for compiling and optimizing specifications. The tool processes spec files, handles dataset optimizations, and provides a configurable way to manage different compile and optimization tasks. + +## Installation + +This tool is designed to be used via `npx` or can be installed globally. + +### Run with `npx` + +```bash +npx @uwdata/publish --spec ./path/to/spec.json +``` + +### Global Installation + +To install the CLI tool globally: + +```bash +npm install -g @uwdata/publish +``` + +Now you can run the tool from anywhere: + +```bash +mosaic-publish --spec ./path/to/spec.json +``` \ No newline at end of file diff --git a/packages/publish/package.json b/packages/publish/package.json new file mode 100644 index 000000000..bcf63297d --- /dev/null +++ b/packages/publish/package.json @@ -0,0 +1,27 @@ +{ + "name": "@uwdata/publish", + "version": "1.0.0", + "description": "CLI tool for spec optimization", + "main": "dist/index.js", + "bin": { + "mosaic-publish": "./dist/publish/src/index.js" + }, + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "chalk": "^4.1.2", + "skia-canvas": "^2.0.2", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/yargs": "^17.0.33", + "typescript": "^5.6.3" + }, + "files": [ + "dist" + ], + "type": "module" +} diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts new file mode 100644 index 000000000..f2b5d2892 --- /dev/null +++ b/packages/publish/src/MosaicPublisher.ts @@ -0,0 +1,315 @@ +import yaml from "yaml"; +import fs from 'fs'; +import { JSDOM } from 'jsdom'; +import chalk from 'chalk'; +import path from "path"; + +// Mosaic modules +import { + parseSpec, astToESM, astToDOM, + SpecNode, DataNode, FileDataNode, + ParquetDataNode, OptionsNode, + CodegenContext, + QueryDataNode, +} from '@uwdata/mosaic-spec'; +import { ExprNode, ColumnNameRefNode } from '@uwdata/mosaic-sql'; +import { MosaicClient } from '../../vgplot/src/index.js'; +import { Interactor } from '@uwdata/mosaic-plot/src/interactors/Interactor.js'; +import { Input } from '@uwdata/mosaic-inputs/src/input.js'; + +// Utility imports +import { + clientsReady, htmlTemplate, mockCanvas, + publishConnector, PublishContext, templateCSS, VGPLOT, + LogLevel, Logger, + preamble +} from './util/index.js'; + +/** + * Class to facilitate publishing a Mosaic specification. + * @param specPath Path to the Mosaic specification file. + * @param outputPath Path to the desired output directory. + * @param title Optional title for the HTML file. + * @param optimize Level of optimization for published visualization. + * @param logLevel Desired logging level (debug, info, warn, error, silent) + * + * The `publish` method is the main entry point for the class + */ +export class MosaicPublisher { + private specPath: string; + private outputPath: string; + private title?: string; + private optimize: 'minimal' | 'more' | 'most'; + private logger: Logger; + + // Internal references used throughout + private ctx: PublishContext; + private ast?: SpecNode; + private data?: Record; + + constructor( + specPath: string, + outputPath: string, + title: string | undefined, + optimize: 'minimal' | 'more' | 'most', + logLevel: LogLevel + ) { + this.specPath = specPath; + this.outputPath = outputPath; + this.title = title; + this.optimize = optimize; + + // Create our logger instance + this.logger = new Logger(logLevel); + + // Create PublishContext + const connector = publishConnector(); + this.ctx = new PublishContext(connector); + if (logLevel !== 'debug') { + this.ctx.coordinator.logger(false); + } + } + + /** + * Main entry point for publishing a Mosaic specification. + */ + public async publish() { + this.logger.info(chalk.cyan('Parsing and processing specification...')); + + // Parse specification + this.parseSpecification(); + if (!this.ast) return; + this.data = this.ast.data; + + // If spec is valid create relevant output directory + if (fs.existsSync(this.outputPath)) { + this.logger.debug(`Removing existing directory: ${this.outputPath}`); + fs.rmSync(this.outputPath, { recursive: true }); + } + this.logger.debug(`Creating directory: ${this.outputPath}`); + fs.mkdirSync(this.outputPath, { recursive: true }); + + // Setup jsdom + this.logger.debug('Setting up jsdom environment...'); + const dom = new JSDOM( + ``, + { pretendToBeVisual: true } + ); + globalThis.window = dom.window as any; + globalThis.document = dom.window.document; + globalThis.navigator ??= dom.window.navigator; + globalThis.requestAnimationFrame = window.requestAnimationFrame; + mockCanvas(globalThis.window); + + // Load the visualization in the DOM and gather interactors/inputs + // TODO: fix type issue with astToDOM to remove the any cast + const { element } = await astToDOM(this.ast, {api: this.ctx.api} as any); + document.body.appendChild(element); + this.logger.debug('Waiting for clients to be ready...'); + await clientsReady(this.ctx); + + const { interactors, inputs, tables } = this.processClients(); + const isInteractive = interactors.size + inputs.size !== 0; + + if (isInteractive) { + // Activate interactors and inputs + this.logger.info(chalk.cyan('Activating interactive elements...')); + await this.activateInteractorsAndInputs(interactors, inputs); + + // Modify AST and process data (extensions, data definitions, etc.) + const og = FileDataNode.prototype.codegenQuery; + FileDataNode.prototype.codegenQuery = function(ctx: CodegenContext) { + const code = og.call(this, ctx); + const { file } = this; + return code?.replace(`"${file}"`, `window.location.origin + "/${file}"`); + }; + this.logger.debug('Updating data nodes...'); + await this.updateDataNodes(tables); + + // Export relevant data from DuckDB to Parquet + this.logger.info(chalk.cyan('Exporting data to Parquet...')); + await this.exportDataFromDuckDB(tables); + } + + this.logger.info(chalk.cyan('Writing output files...')); + this.writeFiles(isInteractive, element); + } + + private parseSpecification() { + try { + const specFile = fs.readFileSync(this.specPath, 'utf-8'); + this.ast = parseSpec(yaml.parse(specFile)); + } catch (e) { + console.error('Error parsing spec:', e); + process.exit(1); + } + } + + private processClients() { + this.logger.debug('--------------- Clients ---------------'); + + const interactors = new Set(); + const inputs = new Set(); + const tables: Record> = {}; + + for (const client of this.ctx.coordinator.clients) { + this.logger.debug(client.constructor.name); + if (client instanceof MosaicClient) { + const fields = client.fields(); + if (fields) { + for (const field of fields) { + const { table, column } = field; + if (!(table in tables)) tables[table] = new Set(); + if (column instanceof ExprNode) { + if (column instanceof ColumnNameRefNode) { + tables[table].add(column.column); + } + } else { + tables[table].add(column); + } + } + } + } + if (client instanceof Input) { + inputs.add(client); + + const input = client as any; // TODO: change Input class to make this cleaner + if (input.from && input.column) { + if (!(input.from in tables)) tables[input.from] = new Set(); + tables[input.from].add(input.column); + } + } + if (client.plot) { + for (const interactor of client.plot.interactors) { + interactors.add(interactor); + } + } + } + return { interactors, inputs, tables }; + } + + /** + * Activate the Interactors and Inputs, waiting + * for queries to finish. + */ + private async activateInteractorsAndInputs(interactors: Set, inputs: Set) { + this.logger.debug('--------------- Activating ---------------'); + for (const interactor of interactors) { + this.logger.debug(interactor.constructor.name); + interactor.activate(); + await this.waitForQueryToFinish(); + } + + for (const input of inputs) { + this.logger.debug(input.constructor.name); + input.activate(); + await this.waitForQueryToFinish(); + } + } + + private async waitForQueryToFinish() { + while (this.ctx.coordinator.manager.pendingExec) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + /** + * Process data definitions (DataNodes) and converts them to ParquetDataNode + * objects based on specified tables. + */ + private async updateDataNodes(tables: Record>) { + // process data definitions + for (const node of Object.values(this.data!)) { + if (!(node.name in tables)) { + delete this.ast!.data[node.name]; + continue; + } + const name = node.name; + const file = `data/${name}.parquet`; + this.ast!.data[name] = new ParquetDataNode(name, file, new OptionsNode({})); + } + + // process tables from DuckDB + const db_tables = await this.ctx.coordinator.query('SHOW ALL TABLES', { type: 'json' }); + if (db_tables.some((table: any) => table.name.startsWith('preagg_'))) { + this.ast!.data['schema'] = new SchemaCreateNode('schema', 'mosaic'); + for (const table of db_tables) { + if (table.name.startsWith('preagg_')) { + const name = `${table.schema}.${table.name}`; + tables[name] = new Set(table.column_names); + const file = `data/.mosaic/${table.name}.parquet`; + this.ast!.data[name] = new ParquetDataNode(name, file, new OptionsNode({})); + } + } + } + } + + /** + * Write out the index.js, index.html, and create data/ directory as needed. + */ + private writeFiles(isInteractive: boolean, element?: HTMLElement | SVGElement) { + const code = astToESM(this.ast!, { + connector: 'wasm', + imports: new Map([[VGPLOT, '* as vg']]), + preamble: this.optimize == 'most' ? preamble : undefined + }); + const html = htmlTemplate(isInteractive, this.title, element, templateCSS); + + fs.writeFileSync(path.join(this.outputPath, 'index.html'), html); + if (isInteractive) { + fs.writeFileSync(path.join(this.outputPath, 'index.js'), code); + } + } + + /** + * Export relevant data from DuckDB to local Parquet + */ + private async exportDataFromDuckDB(tables: Record>) { + const copy_queries: string[] = []; + for (const node of Object.values(this.data!)) { + if (!(node instanceof ParquetDataNode)) continue; + const table = node.name; + const file = node.file; + const relevant_columns = tables[table]; + if (relevant_columns.size === 0) { + this.logger.debug(`No relevant columns found for table ${table}`); + continue; + } + + let query: string; + switch (this.optimize) { + case 'minimal': + query = `COPY (SELECT * FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + break; + case 'more': + case 'most': + query = `COPY (SELECT ${Array.from(relevant_columns).join(', ')} FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + break; + default: + throw new Error(`Invalid optimization level: ${this.optimize}`); + } + copy_queries.push(query); + } + + if (copy_queries.length > 0) { + // TODO: Make the following conditionally on data needs + fs.mkdirSync(path.join(this.outputPath, 'data'), { recursive: true }); + fs.mkdirSync(path.join(this.outputPath, 'data/.mosaic'), { recursive: true }); + + await this.ctx.coordinator.exec(copy_queries); + } + } +} + +class SchemaCreateNode extends QueryDataNode { + schema: string; + + constructor(name: string, schema: string) { + super(name, "schema"); + this.schema = schema; + } + + public codegenQuery(ctx: CodegenContext) { + return `'CREATE SCHEMA IF NOT EXISTS ${this.schema};'`; + } +} \ No newline at end of file diff --git a/packages/publish/src/index.ts b/packages/publish/src/index.ts new file mode 100755 index 000000000..0773a4801 --- /dev/null +++ b/packages/publish/src/index.ts @@ -0,0 +1,92 @@ +#!/usr/bin/env node + +import { MosaicPublisher } from './MosaicPublisher.js'; +import { LogLevel } from './util/index.js'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import chalk from 'chalk'; + +interface CliArgs { + spec: string; + optimize: 'minimal' | 'more' | 'most'; + output?: string; + title?: string; + logLevel: LogLevel; +} + +// Build the command-line parser without using the yargs type parameter: +const parsed = yargs(hideBin(process.argv)) + .scriptName('mosaic-publish') + .usage('$0 [args]') + .options({ + spec: { + alias: 's', + type: 'string', + describe: 'Path to the specification file', + demandOption: true, + }, + optimize: { + alias: 'o', + type: 'string', + describe: 'Level of optimizations', + choices: ['minimal', 'more', 'most'] as const, + default: 'minimal', + }, + output: { + alias: 'out', + type: 'string', + describe: 'Output folder path for the result', + }, + title: { + alias: 't', + type: 'string', + describe: 'Title of published visualization', + }, + logLevel: { + alias: 'l', + type: 'string', + describe: 'Logging level (debug, info, warn, error, silent)', + choices: ['debug', 'info', 'warn', 'error', 'silent'] as const, + default: 'info' + } + }) + .help() + .parseSync(); + +// Cast the results to our interface +const argv: CliArgs = { + spec: parsed.spec, + optimize: parsed.optimize as 'minimal' | 'more' | 'most', + output: parsed.output, + title: parsed.title, + logLevel: parsed.logLevel as LogLevel +}; + +// Provide user-facing pretty-print statements +console.log(chalk.bold.blue('Spec file:'), chalk.yellowBright(argv.spec)); +console.log(chalk.bold.blue('Optimize:'), chalk.yellowBright(argv.optimize)); +console.log(chalk.bold.blue('Output:'), chalk.yellowBright(argv.output ?? 'out/')); +if (argv.title) { + console.log(chalk.bold.blue('Title:'), chalk.yellowBright(argv.title)); +} + +// Instantiate the publisher +const publisher = new MosaicPublisher( + argv.spec, + argv.output ?? 'out', + argv.title, + argv.optimize, + argv.logLevel +); + +// Execute publishing in an async context +(async () => { + try { + console.log(chalk.cyan('Publishing visualization...')); + await publisher.publish(); + console.log(chalk.bold.green('✔ Publish complete!')); + } catch (err) { + console.error(chalk.red('An error occurred while publishing:'), err); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/packages/publish/src/util/Logger.ts b/packages/publish/src/util/Logger.ts new file mode 100644 index 000000000..fd966ee18 --- /dev/null +++ b/packages/publish/src/util/Logger.ts @@ -0,0 +1,25 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; +export class Logger { + constructor(private level: LogLevel) {} + + debug(...args: unknown[]) { + if (this.level === 'debug') { + console.log(...args); + } + } + info(...args: unknown[]) { + if (this.level === 'debug' || this.level === 'info') { + console.log(...args); + } + } + warn(...args: unknown[]) { + if (['debug','info','warn'].includes(this.level)) { + console.warn(...args); + } + } + error(...args: unknown[]) { + if (this.level !== 'silent') { + console.error(...args); + } + } +} \ No newline at end of file diff --git a/packages/publish/src/util/constants.ts b/packages/publish/src/util/constants.ts new file mode 100644 index 000000000..c7d46a915 --- /dev/null +++ b/packages/publish/src/util/constants.ts @@ -0,0 +1,171 @@ +export const htmlTemplate = (isInteractive: boolean, title?: string, element?: HTMLElement | SVGElement, css = templateCSS) => ` + + + + ${title ?? 'Mosaic Visualization'} + + +
+ ${isInteractive ? '
' : ''} + ${element?.outerHTML ?? ''} +
+ +${isInteractive ? ` +` : ''} +${css} +`; + +export const templateCSS =``; + +export const VGPLOT = 'https://cdn.jsdelivr.net/npm/@uwdata/vgplot@latest/dist/vgplot.js'; + +// TODO: switch this to ./renderHelpers version when changes pushed to npm +// Currently, this is hack to see when clients are ready use .pending when it is available +export const preamble = `export function clientsReady() { + const clients = [...vg.coordinator().clients]; + return Promise.allSettled(clients.map(c => c.initialize())) +}` \ No newline at end of file diff --git a/packages/publish/src/util/index.ts b/packages/publish/src/util/index.ts new file mode 100644 index 000000000..0e4422e4f --- /dev/null +++ b/packages/publish/src/util/index.ts @@ -0,0 +1,4 @@ +export * from './constants.js'; +export * from './publishHelpers.js'; +export * from './renderHelpers.js'; +export * from './Logger.js'; \ No newline at end of file diff --git a/packages/publish/src/util/publishHelpers.ts b/packages/publish/src/util/publishHelpers.ts new file mode 100644 index 000000000..d67c97886 --- /dev/null +++ b/packages/publish/src/util/publishHelpers.ts @@ -0,0 +1,36 @@ +import { Coordinator, decodeIPC } from "@uwdata/mosaic-core"; +import { DuckDB } from "@uwdata/mosaic-duckdb"; +import { InstantiateContext } from "@uwdata/mosaic-spec"; +import { createAPIContext } from "../../../vgplot/src/index.js"; + +export function publishConnector() { + const db = new DuckDB(); + db.exec('PRAGMA force_compression=Uncompressed'); + + type Query = { + type: 'exec' | 'arrow' | 'json'; + sql: string; + } + + return { + query: async (query: Query) => { + const { type, sql } = query; + switch (type) { + case 'exec': + return db.exec(sql); + case 'arrow': + return decodeIPC(await db.arrowBuffer(sql)); + default: + return db.query(sql); + } + } + }; +} + +export class PublishContext extends InstantiateContext { + constructor(connector: any) { + const coordinator = new Coordinator(connector); + const api = createAPIContext({ coordinator }); + super({ api }); + } +} \ No newline at end of file diff --git a/packages/publish/src/util/renderHelpers.ts b/packages/publish/src/util/renderHelpers.ts new file mode 100644 index 000000000..7cc2b12a6 --- /dev/null +++ b/packages/publish/src/util/renderHelpers.ts @@ -0,0 +1,32 @@ +import { MosaicClient } from "@uwdata/mosaic-core/src/MosaicClient.js"; +import { InstantiateContext } from "@uwdata/mosaic-spec"; +import { Canvas } from "skia-canvas"; + +export function mockCanvas (window: any) { + const og = window.document.createElement + window.document.createElement = function (tagName: string) { + if (tagName !== 'canvas') { + return og.call(this, tagName) + } else { + const canvas = new Canvas(); + const el = og.call(this, tagName) as HTMLCanvasElement; + // TODO: this is a hack to get around the fact that the canvas is not a real canvas + el.getContext = canvas.getContext.bind(canvas) as unknown as HTMLCanvasElement['getContext']; + el.toDataURL = canvas.toDataURLSync.bind(canvas); + const set = el.setAttribute; + el.setAttribute = function (name: string, value: string) { + if (name !== 'width' && name !== 'height') { + set.call(this, name, value); + } else { + canvas[name] = parseInt(value); + } + } + return el; + } + } +} + +export function clientsReady(ctx: InstantiateContext) { + const clients = [...ctx.coordinator.clients] as MosaicClient[]; + return Promise.allSettled(clients.map(c => c.pending)); +} \ No newline at end of file diff --git a/packages/publish/tsconfig.json b/packages/publish/tsconfig.json new file mode 100644 index 000000000..6663df935 --- /dev/null +++ b/packages/publish/tsconfig.json @@ -0,0 +1,14 @@ +{ + "include": ["src/**/*.js", "src/**/*.ts"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "allowJs": true, + "outDir": "dist/", + "declaration": true, + "strict": true, + "moduleResolution": "Node16", + "module": "Node16", + "esModuleInterop": true, + "skipLibCheck": true, + }, +} From bf8cf2dd38fd89622a75909972031c0cc7ed0ba8 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 9 Feb 2025 13:00:26 -0800 Subject: [PATCH 02/37] feat: consolidated activatable logic to core --- packages/core/src/ActivatableClient.ts | 23 ++++++++++++++++ packages/core/src/index.js | 1 + packages/inputs/src/Menu.js | 6 ++--- packages/inputs/src/Search.js | 6 ++--- packages/inputs/src/Slider.js | 6 ++--- packages/inputs/src/Table.js | 6 ++--- packages/inputs/src/input.js | 22 +--------------- packages/plot/src/interactors/Highlight.js | 8 +++--- packages/plot/src/interactors/Interactor.js | 29 --------------------- packages/plot/src/interactors/Interval1D.js | 8 +++--- packages/plot/src/interactors/Interval2D.js | 8 +++--- packages/plot/src/interactors/Nearest.js | 8 +++--- packages/plot/src/interactors/PanZoom.js | 8 +++--- packages/plot/src/interactors/Region.js | 8 +++--- packages/plot/src/interactors/Toggle.js | 8 +++--- 15 files changed, 65 insertions(+), 90 deletions(-) create mode 100644 packages/core/src/ActivatableClient.ts delete mode 100644 packages/plot/src/interactors/Interactor.js diff --git a/packages/core/src/ActivatableClient.ts b/packages/core/src/ActivatableClient.ts new file mode 100644 index 000000000..a9b5e571b --- /dev/null +++ b/packages/core/src/ActivatableClient.ts @@ -0,0 +1,23 @@ +import { MosaicClient } from "./MosaicClient.js"; + +/** + * Interface for activatable objects. + * @see {@link ActivatableClient} + * @see {@link ActivatableClient.activate} + */ +export abstract class Activatable { + abstract activate(): void; +} + +/** + * Base class for activatable Mosaic clients. + */ +export class ActivatableClient extends MosaicClient implements Activatable { + /** + * Activates the input. + * @throws {Error} If the method is not implemented by the subclass. + */ + activate(): void { + throw new Error("activate method must be implemented by subclass."); + } +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 82f187152..bcea47395 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,4 +1,5 @@ export { MosaicClient } from './MosaicClient.js'; +export { Activatable, ActivatableClient } from './ActivatableClient.js'; export { Coordinator, coordinator } from './Coordinator.js'; export { Selection, isSelection } from './Selection.js'; export { Param, isParam } from './Param.js'; diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index cc7053bbb..0d7ab6b65 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -1,6 +1,6 @@ -import { Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; -import { input, Input } from './input.js'; +import { input } from './input.js'; const isObject = v => { return v && typeof v === 'object' && !Array.isArray(v); @@ -8,7 +8,7 @@ const isObject = v => { export const menu = options => input(Menu, options); -export class Menu extends Input { +export class Menu extends ActivatableClient { /** * Create a new menu input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index 6dcdd9c26..c81fdd85a 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -1,12 +1,12 @@ -import { Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; -import { input, Input } from './input.js'; +import { input } from './input.js'; let _id = 0; export const search = options => input(Search, options); -export class Search extends Input { +export class Search extends ActivatableClient { /** * Create a new text search input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index bbb4cd3d8..6c4511150 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -1,12 +1,12 @@ -import { Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; import { Query, max, min } from '@uwdata/mosaic-sql'; -import { input, Input } from './input.js'; +import { input } from './input.js'; let _id = 0; export const slider = options => input(Slider, options); -export class Slider extends Input { +export class Slider extends ActivatableClient { /** * Create a new slider input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index 12f9667c0..f2dc6deb4 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -1,13 +1,13 @@ -import { clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; +import { ActivatableClient, clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; -import { input, Input } from './input.js'; +import { input } from './input.js'; let _id = -1; export const table = options => input(Table, options); -export class Table extends Input { +export class Table extends ActivatableClient { /** * Create a new Table instance. * @param {object} options Options object diff --git a/packages/inputs/src/input.js b/packages/inputs/src/input.js index 8b7263896..75a59a90c 100644 --- a/packages/inputs/src/input.js +++ b/packages/inputs/src/input.js @@ -1,24 +1,4 @@ - -// TODO: change this to be at same level as MosaicClient to avoid duplicate in interactors -import { coordinator, MosaicClient } from '@uwdata/mosaic-core'; - -/** - * Base class for all inputs. - * This class should not be instantiated directly. - */ -export class Input extends MosaicClient { - constructor(filterBy) { - super(filterBy); - } - - /** - * Activates the input. - * @throws {Error} If the method is not implemented by the subclass. - */ - activate() { - throw new Error('activate method must be implemented by subclass'); - } -} +import { coordinator } from '@uwdata/mosaic-core'; export function input(InputClass, options) { const input = new InputClass(options); diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index 34879f1a6..69f3935a1 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -1,8 +1,7 @@ -import { throttle } from '@uwdata/mosaic-core'; +import { Activatable, throttle } from '@uwdata/mosaic-core'; import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -import { Interactor } from './Interactor.js'; function configureMark(mark) { const { channels } = mark; @@ -35,12 +34,13 @@ function configureMark(mark) { return mark; } -export class Highlight extends Interactor { +export class Highlight extends Activatable { constructor(mark, { selection, channels = {} }) { - super(configureMark(mark)); + super(); + this.mark = configureMark(mark); this.selection = selection; const c = Object.entries(sanitizeStyles(channels)); this.channels = c.length ? c : [['opacity', 0.2]]; diff --git a/packages/plot/src/interactors/Interactor.js b/packages/plot/src/interactors/Interactor.js deleted file mode 100644 index e17c50839..000000000 --- a/packages/plot/src/interactors/Interactor.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-unused-vars */ -// TODO: change this to be at same level as MosaicClient to avoid duplicate in inputs - -/** - * Base class for all interactors. - * This class should not be instantiated directly. - */ -export class Interactor { - constructor(mark) { - this.mark = mark; - } - - /** - * Initializes the interactor. - * @param {SVGElement} svg - The SVG element. - * @throws {Error} If the method is not implemented by the subclass. - */ - init(svg) { - throw new Error('init method must be implemented by subclass'); - } - - /** - * Activates the interactor. - * @throws {Error} If the method is not implemented by the subclass. - */ - activate() { - throw new Error('activate method must be implemented by subclass'); - } - } \ No newline at end of file diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index 1e5885f54..13cd167fa 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -1,13 +1,12 @@ -import { clauseInterval } from '@uwdata/mosaic-core'; +import { Activatable, clauseInterval } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brushGroups, brushX, brushY } from './util/brush.js'; import { closeTo } from './util/close-to.js'; import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -import { Interactor } from './Interactor.js'; -export class Interval1D extends Interactor { +export class Interval1D extends Activatable { constructor(mark, { channel, selection, @@ -16,7 +15,8 @@ export class Interval1D extends Interactor { peers = true, brush: style }) { - super(mark); + super(); + this.mark = mark; this.channel = channel; this.pixelSize = pixelSize || 1; this.selection = selection; diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index c9ca2af64..e43c42c4a 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -1,13 +1,12 @@ -import { clauseIntervals } from '@uwdata/mosaic-core'; +import { Activatable, clauseIntervals } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brush, brushGroups } from './util/brush.js'; import { closeTo } from './util/close-to.js'; import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -import { Interactor } from './Interactor.js'; -export class Interval2D extends Interactor { +export class Interval2D extends Activatable { constructor(mark, { selection, xfield, @@ -16,7 +15,8 @@ export class Interval2D extends Interactor { peers = true, brush: style }) { - super(mark); + super(); + this.mark = mark; this.pixelSize = pixelSize || 1; this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index 2eddfd8d9..fb77023e9 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -1,9 +1,8 @@ -import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; +import { Activatable, clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; -import { Interactor } from './Interactor.js'; -export class Nearest extends Interactor { +export class Nearest extends Activatable { constructor(mark, { selection, pointer, @@ -11,7 +10,8 @@ export class Nearest extends Interactor { fields, maxRadius = 40 }) { - super(mark); + super(); + this.mark = mark; this.selection = selection; this.clients = new Set().add(mark); this.pointer = pointer; diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index 62558d58d..abc138735 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -1,11 +1,10 @@ -import { Selection, clauseInterval } from '@uwdata/mosaic-core'; +import { Activatable, Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; -import { Interactor } from './Interactor.js'; const asc = (a, b) => a - b; -export class PanZoom extends Interactor { +export class PanZoom extends Activatable { constructor(mark, { x = new Selection(), y = new Selection(), @@ -15,7 +14,8 @@ export class PanZoom extends Interactor { panx = true, pany = true }) { - super(mark); + super(); + this.mark = mark; this.xsel = x; this.ysel = y; this.xfield = xfield || getField(mark, 'x'); diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index ab729ab78..28661c82d 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -1,4 +1,4 @@ -import { clausePoints } from '@uwdata/mosaic-core'; +import { Activatable, clausePoints } from '@uwdata/mosaic-core'; import { select } from 'd3'; import { brush } from './util/brush.js'; import { getFields } from './util/get-field.js'; @@ -7,9 +7,8 @@ import { patchScreenCTM } from './util/patchScreenCTM.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; -import { Interactor } from './Interactor.js'; -export class Region extends Interactor { +export class Region extends Activatable { constructor(mark, { channels, selection, @@ -20,7 +19,8 @@ export class Region extends Interactor { strokeDasharray: '1,1' } }) { - super(mark); + super(); + this.mark = mark; this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index 1109c6b7c..fb8f80d1c 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -1,9 +1,8 @@ -import { clausePoints } from '@uwdata/mosaic-core'; +import { Activatable, clausePoints } from '@uwdata/mosaic-core'; import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; -import { Interactor } from './Interactor.js'; -export class Toggle extends Interactor { +export class Toggle extends Activatable { /** * @param {*} mark The mark to interact with. * @param {*} options The interactor options. @@ -13,7 +12,8 @@ export class Toggle extends Interactor { channels, peers = true }) { - super(mark) + super(); + this.mark = mark; this.value = null; this.selection = selection; this.peers = peers; From 4e6d78b7cbce8fd410d39d4cea7e9bbf2ed63231 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 9 Feb 2025 13:00:26 -0800 Subject: [PATCH 03/37] feat: consolidated activatable logic to core --- packages/core/src/ActivatableClient.ts | 23 +++++++++++++++++++++ packages/core/src/index.js | 1 + packages/inputs/src/Menu.js | 4 ++-- packages/inputs/src/Search.js | 4 ++-- packages/inputs/src/Slider.js | 4 ++-- packages/inputs/src/Table.js | 4 ++-- packages/plot/src/interactors/Highlight.js | 5 +++-- packages/plot/src/interactors/Interval1D.js | 5 +++-- packages/plot/src/interactors/Interval2D.js | 5 +++-- packages/plot/src/interactors/Nearest.js | 5 +++-- packages/plot/src/interactors/PanZoom.js | 5 +++-- packages/plot/src/interactors/Region.js | 5 +++-- packages/plot/src/interactors/Toggle.js | 6 ++++-- 13 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/ActivatableClient.ts diff --git a/packages/core/src/ActivatableClient.ts b/packages/core/src/ActivatableClient.ts new file mode 100644 index 000000000..a9b5e571b --- /dev/null +++ b/packages/core/src/ActivatableClient.ts @@ -0,0 +1,23 @@ +import { MosaicClient } from "./MosaicClient.js"; + +/** + * Interface for activatable objects. + * @see {@link ActivatableClient} + * @see {@link ActivatableClient.activate} + */ +export abstract class Activatable { + abstract activate(): void; +} + +/** + * Base class for activatable Mosaic clients. + */ +export class ActivatableClient extends MosaicClient implements Activatable { + /** + * Activates the input. + * @throws {Error} If the method is not implemented by the subclass. + */ + activate(): void { + throw new Error("activate method must be implemented by subclass."); + } +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 82f187152..bcea47395 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,4 +1,5 @@ export { MosaicClient } from './MosaicClient.js'; +export { Activatable, ActivatableClient } from './ActivatableClient.js'; export { Coordinator, coordinator } from './Coordinator.js'; export { Selection, isSelection } from './Selection.js'; export { Param, isParam } from './Param.js'; diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index bcfbb60c2..a679c4767 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -1,4 +1,4 @@ -import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -8,7 +8,7 @@ const isObject = v => { export const menu = options => input(Menu, options); -export class Menu extends MosaicClient { +export class Menu extends ActivatableClient { /** * Create a new menu input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index 4bdc838d6..39f973fad 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -1,4 +1,4 @@ -import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -6,7 +6,7 @@ let _id = 0; export const search = options => input(Search, options); -export class Search extends MosaicClient { +export class Search extends ActivatableClient { /** * Create a new text search input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index 3f5cefa54..8e1d812a4 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -1,4 +1,4 @@ -import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; +import { ActivatableClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; import { Query, max, min } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -6,7 +6,7 @@ let _id = 0; export const slider = options => input(Slider, options); -export class Slider extends MosaicClient { +export class Slider extends ActivatableClient { /** * Create a new slider input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index f02c75db3..0582b9357 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -1,4 +1,4 @@ -import { MosaicClient, clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; +import { ActivatableClient, clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; import { input } from './input.js'; @@ -7,7 +7,7 @@ let _id = -1; export const table = options => input(Table, options); -export class Table extends MosaicClient { +export class Table extends ActivatableClient { /** * Create a new Table instance. * @param {object} options Options object diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index be10c26f8..29d8ebfb4 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -1,4 +1,4 @@ -import { throttle } from '@uwdata/mosaic-core'; +import { Activatable, throttle } from '@uwdata/mosaic-core'; import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; @@ -34,11 +34,12 @@ function configureMark(mark) { return mark; } -export class Highlight { +export class Highlight extends Activatable { constructor(mark, { selection, channels = {} }) { + super(); this.mark = configureMark(mark); this.selection = selection; const c = Object.entries(sanitizeStyles(channels)); diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index c6e8b772f..13cd167fa 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -1,4 +1,4 @@ -import { clauseInterval } from '@uwdata/mosaic-core'; +import { Activatable, clauseInterval } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brushGroups, brushX, brushY } from './util/brush.js'; import { closeTo } from './util/close-to.js'; @@ -6,7 +6,7 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -export class Interval1D { +export class Interval1D extends Activatable { constructor(mark, { channel, selection, @@ -15,6 +15,7 @@ export class Interval1D { peers = true, brush: style }) { + super(); this.mark = mark; this.channel = channel; this.pixelSize = pixelSize || 1; diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index 9774f81cd..e43c42c4a 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -1,4 +1,4 @@ -import { clauseIntervals } from '@uwdata/mosaic-core'; +import { Activatable, clauseIntervals } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brush, brushGroups } from './util/brush.js'; import { closeTo } from './util/close-to.js'; @@ -6,7 +6,7 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -export class Interval2D { +export class Interval2D extends Activatable { constructor(mark, { selection, xfield, @@ -15,6 +15,7 @@ export class Interval2D { peers = true, brush: style }) { + super(); this.mark = mark; this.pixelSize = pixelSize || 1; this.selection = selection; diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index b1dd5fb44..3b8325680 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -1,8 +1,8 @@ -import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; +import { Activatable, clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; -export class Nearest { +export class Nearest extends Activatable { constructor(mark, { selection, pointer, @@ -10,6 +10,7 @@ export class Nearest { fields, maxRadius = 40 }) { + super(); this.mark = mark; this.selection = selection; this.clients = new Set().add(mark); diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index 5b9cc8535..00c74f574 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -1,10 +1,10 @@ -import { Selection, clauseInterval } from '@uwdata/mosaic-core'; +import { Activatable, Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; const asc = (a, b) => a - b; -export class PanZoom { +export class PanZoom extends Activatable { constructor(mark, { x = new Selection(), y = new Selection(), @@ -14,6 +14,7 @@ export class PanZoom { panx = true, pany = true }) { + super(); this.mark = mark; this.xsel = x; this.ysel = y; diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index 76ffc2b0b..28661c82d 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -1,4 +1,4 @@ -import { clausePoints } from '@uwdata/mosaic-core'; +import { Activatable, clausePoints } from '@uwdata/mosaic-core'; import { select } from 'd3'; import { brush } from './util/brush.js'; import { getFields } from './util/get-field.js'; @@ -8,7 +8,7 @@ import { sanitizeStyles } from './util/sanitize-styles.js'; import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; -export class Region { +export class Region extends Activatable { constructor(mark, { channels, selection, @@ -19,6 +19,7 @@ export class Region { strokeDasharray: '1,1' } }) { + super(); this.mark = mark; this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index 5dc55fa69..407543a20 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -1,8 +1,8 @@ -import { clausePoints } from '@uwdata/mosaic-core'; +import { Activatable, clausePoints } from '@uwdata/mosaic-core'; import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; -export class Toggle { +export class Toggle extends Activatable { /** * @param {*} mark The mark to interact with. * @param {*} options The interactor options. @@ -12,6 +12,8 @@ export class Toggle { channels, peers = true }) { + super(); + this.mark = mark; this.value = null; this.mark = mark; this.selection = selection; From a2d1a18ba863940bb0f9f67ee2f9007a83e6fd53 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 9 Feb 2025 13:33:57 -0800 Subject: [PATCH 04/37] fix: implement Activatable interface for inputs and interactors --- packages/inputs/src/Menu.js | 3 +-- packages/inputs/src/Search.js | 3 +-- packages/inputs/src/Slider.js | 3 +-- packages/inputs/src/Table.js | 17 ++++++++++++++++ packages/inputs/src/input.js | 2 +- packages/plot/src/interactors/Highlight.js | 4 ++++ packages/plot/src/interactors/Nearest.js | 10 ++++++---- packages/plot/src/interactors/PanZoom.js | 23 ++++++++++++---------- packages/plot/src/interactors/Toggle.js | 8 +++++--- 9 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index a679c4767..0d7ab6b65 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -125,8 +125,7 @@ export class Menu extends ActivatableClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(clausePoint(this.field, 0, { source: this })); + if (isSelection(this.selection)) this.selection.activate(clausePoint(this.field, 0, { source: this })); } publish(value) { diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index 39f973fad..c81fdd85a 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -97,8 +97,7 @@ export class Search extends ActivatableClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(this.clause('')); + if (isSelection(this.selection)) this.selection.activate(this.clause('')); } publish(value) { diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index 8e1d812a4..6c4511150 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -165,8 +165,7 @@ export class Slider extends ActivatableClient { } activate() { - // @ts-ignore - activate is only called for a Selection - this.selection.activate(this.clause(0)); + if (isSelection(this.selection)) this.selection.activate(this.clause(0)); } publish(value) { diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index 0582b9357..f2dc6deb4 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -208,6 +208,23 @@ export class Table extends ActivatableClient { return this; } + activate() { + const { data } = this; + const n = data.length - 1; + + const { numRows } = data[n]; + + for (let i = 0; i < numRows; i++) { + this.currentRow = i; + this.selection.update(this.clause([i])); + } + + if (numRows > 0) { + this.currentRow = -1; + this.selection.update(this.clause()); + } + } + sort(event, column) { if (column === this.sortColumn) { this.sortDesc = !this.sortDesc; diff --git a/packages/inputs/src/input.js b/packages/inputs/src/input.js index e4f788da3..75a59a90c 100644 --- a/packages/inputs/src/input.js +++ b/packages/inputs/src/input.js @@ -4,4 +4,4 @@ export function input(InputClass, options) { const input = new InputClass(options); coordinator().connect(input); return input.element; -} +} \ No newline at end of file diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index 29d8ebfb4..69f3935a1 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -81,6 +81,10 @@ export class Highlight extends Activatable { } } } + + activate() { + return this.update(); + } } async function predicateFunction(mark, selection) { diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index 3b8325680..fb77023e9 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -72,12 +72,14 @@ export class Nearest extends Activatable { // trigger activation updates svg.addEventListener('pointerenter', evt => { - if (!evt.buttons) { - const v = this.channels.map(() => 0); - selection.activate(this.clause(v)); - } + if (!evt.buttons) this.activate(); }); } + + activate() { + const v = this.channels.map(() => 0); + this.selection.activate(this.clause(v)); + } } /** diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index 00c74f574..abc138735 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -60,7 +60,7 @@ export class PanZoom extends Activatable { this.svg = svg; if (this.initialized) return; else this.initialized = true; - const { panx, pany, mark: { plot: { element } }, xsel, ysel } = this; + const { panx, pany, mark: { plot: { element } }} = this; this.xscale = svg.scale('x'); this.yscale = svg.scale('y'); @@ -86,19 +86,22 @@ export class PanZoom extends Activatable { let enter = false; element.addEventListener('pointerenter', evt => { if (enter) return; else enter = true; - if (evt.buttons) return; // don't activate if mouse down - if (panx) { - const { xscale, xfield } = this; - xsel.activate(this.clause(xscale.domain, xfield, xscale)); - } - if (pany) { - const { yscale, yfield } = this; - ysel.activate(this.clause(yscale.domain, yfield, yscale)); - } + if (!evt.buttons) this.activate(); // don't activate if mouse down }); element.addEventListener('pointerleave', () => enter = false); } } + + activate() { + if (this.panx) { + const { xscale, xfield } = this; + this.xsel.activate(this.clause(xscale.domain, xfield, xscale)); + } + if (this.pany) { + const { yscale, yfield } = this; + this.ysel.activate(this.clause(yscale.domain, yfield, yscale)); + } + } } function extent(ext, defaultTrue, defaultFalse) { diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index 407543a20..fb8f80d1c 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -15,7 +15,6 @@ export class Toggle extends Activatable { super(); this.mark = mark; this.value = null; - this.mark = mark; this.selection = selection; this.peers = peers; const fields = this.fields = []; @@ -77,10 +76,13 @@ export class Toggle extends Activatable { }); svg.addEventListener('pointerenter', evt => { - if (evt.buttons) return; - this.selection.activate(this.clause([this.fields.map(() => 0)])); + if (!evt.buttons) this.activate(); }); } + + activate() { + this.selection.activate(this.clause([this.fields.map(() => 0)])); + } } function isTargetElement(groups, node) { From 7a13cf79a8f34be5dcc83e114d288c4c37f8b45b Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Mon, 10 Feb 2025 11:47:09 -0800 Subject: [PATCH 05/37] fix: change Activatable to interface and remove ActivatableClient --- packages/core/src/Activatable.ts | 10 ++++++++++ packages/core/src/ActivatableClient.ts | 23 ---------------------- packages/core/src/index.js | 4 +++- packages/core/src/util/is-activatable.js | 9 +++++++++ packages/inputs/src/Menu.js | 9 +++++++-- packages/inputs/src/Search.js | 9 +++++++-- packages/inputs/src/Slider.js | 9 +++++++-- packages/inputs/src/Table.js | 25 +++++++++--------------- packages/inputs/src/input.js | 2 +- 9 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 packages/core/src/Activatable.ts delete mode 100644 packages/core/src/ActivatableClient.ts create mode 100644 packages/core/src/util/is-activatable.js diff --git a/packages/core/src/Activatable.ts b/packages/core/src/Activatable.ts new file mode 100644 index 000000000..0a590f247 --- /dev/null +++ b/packages/core/src/Activatable.ts @@ -0,0 +1,10 @@ +import { isActivatable } from "./util/is-activatable.js"; + +/** + * Interface for activatable objects. + * + * @see {@link isActivatable} + */ +export interface Activatable { + activate(): void; +} \ No newline at end of file diff --git a/packages/core/src/ActivatableClient.ts b/packages/core/src/ActivatableClient.ts deleted file mode 100644 index a9b5e571b..000000000 --- a/packages/core/src/ActivatableClient.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MosaicClient } from "./MosaicClient.js"; - -/** - * Interface for activatable objects. - * @see {@link ActivatableClient} - * @see {@link ActivatableClient.activate} - */ -export abstract class Activatable { - abstract activate(): void; -} - -/** - * Base class for activatable Mosaic clients. - */ -export class ActivatableClient extends MosaicClient implements Activatable { - /** - * Activates the input. - * @throws {Error} If the method is not implemented by the subclass. - */ - activate(): void { - throw new Error("activate method must be implemented by subclass."); - } -} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js index bcea47395..94ad4506b 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,5 +1,4 @@ export { MosaicClient } from './MosaicClient.js'; -export { Activatable, ActivatableClient } from './ActivatableClient.js'; export { Coordinator, coordinator } from './Coordinator.js'; export { Selection, isSelection } from './Selection.js'; export { Param, isParam } from './Param.js'; @@ -25,6 +24,7 @@ export { throttle } from './util/throttle.js'; export { toDataColumns } from './util/to-data-columns.js'; export { queryFieldInfo } from './util/field-info.js'; export { jsType } from './util/js-type.js'; +export { isActivatable } from './util/is-activatable.js'; /** * @typedef {import('./util/selection-types.js').ClauseMetadata} ClauseMetadata @@ -37,4 +37,6 @@ export { jsType } from './util/js-type.js'; * @typedef {import('./util/selection-types.js').BinMethod} BinMethod * @typedef {import('./util/selection-types.js').IntervalMetadata} IntervalMetadata * @typedef {import('./util/selection-types.js').SelectionClause} SelectionClause + * + * @typedef {import('./Activatable.js').Activatable} Activatable */ diff --git a/packages/core/src/util/is-activatable.js b/packages/core/src/util/is-activatable.js new file mode 100644 index 000000000..27fab7166 --- /dev/null +++ b/packages/core/src/util/is-activatable.js @@ -0,0 +1,9 @@ +/** + * Test if a value implements the Activatable interface. + * + * @param {*} value The value to test. + * @returns {value is import("../Activatable.js").Activatable} + */ +export function isActivatable(value) { + return typeof value?.activate === 'function' && value.activate.length === 0; +} \ No newline at end of file diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index 0d7ab6b65..8702589fc 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -1,4 +1,4 @@ -import { ActivatableClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; +import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -8,7 +8,12 @@ const isObject = v => { export const menu = options => input(Menu, options); -export class Menu extends ActivatableClient { +/** + * A HTML select based dropdown menu input. + * + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Menu extends MosaicClient { /** * Create a new menu input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index c81fdd85a..d64dcb146 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -1,4 +1,4 @@ -import { ActivatableClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; +import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/mosaic-core'; import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -6,7 +6,12 @@ let _id = 0; export const search = options => input(Search, options); -export class Search extends ActivatableClient { +/** + * A HTML input based text search input. + * + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Search extends MosaicClient { /** * Create a new text search input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index 6c4511150..3ff3b8fd9 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -1,4 +1,4 @@ -import { ActivatableClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; +import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection } from '@uwdata/mosaic-core'; import { Query, max, min } from '@uwdata/mosaic-sql'; import { input } from './input.js'; @@ -6,7 +6,12 @@ let _id = 0; export const slider = options => input(Slider, options); -export class Slider extends ActivatableClient { +/** + * A HTML range based slider input. + * + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Slider extends MosaicClient { /** * Create a new slider input. * @param {object} [options] Options object diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index f2dc6deb4..bb0baff69 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -1,4 +1,4 @@ -import { ActivatableClient, clausePoints, coordinator, isParam, toDataColumns } from '@uwdata/mosaic-core'; +import { MosaicClient, clausePoints, coordinator, isParam, isSelection, toDataColumns } from '@uwdata/mosaic-core'; import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; import { input } from './input.js'; @@ -7,7 +7,13 @@ let _id = -1; export const table = options => input(Table, options); -export class Table extends ActivatableClient { + +/** + * A HTML table based table component. + * + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Table extends MosaicClient { /** * Create a new Table instance. * @param {object} options Options object @@ -209,20 +215,7 @@ export class Table extends ActivatableClient { } activate() { - const { data } = this; - const n = data.length - 1; - - const { numRows } = data[n]; - - for (let i = 0; i < numRows; i++) { - this.currentRow = i; - this.selection.update(this.clause([i])); - } - - if (numRows > 0) { - this.currentRow = -1; - this.selection.update(this.clause()); - } + if (isSelection(this.selection)) this.selection.activate(this.clause([])); } sort(event, column) { diff --git a/packages/inputs/src/input.js b/packages/inputs/src/input.js index 75a59a90c..e4f788da3 100644 --- a/packages/inputs/src/input.js +++ b/packages/inputs/src/input.js @@ -4,4 +4,4 @@ export function input(InputClass, options) { const input = new InputClass(options); coordinator().connect(input); return input.element; -} \ No newline at end of file +} From a50afbaa17d67fd6a567a6d76a70ecc19a39d3b9 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Mon, 10 Feb 2025 11:56:41 -0800 Subject: [PATCH 06/37] fix: Changed intteractors to use new Activatable interface --- packages/core/src/util/is-activatable.js | 2 +- packages/plot/src/interactors/Highlight.js | 8 +++++--- packages/plot/src/interactors/Interval1D.js | 8 +++++--- packages/plot/src/interactors/Interval2D.js | 8 +++++--- packages/plot/src/interactors/Nearest.js | 8 +++++--- packages/plot/src/interactors/PanZoom.js | 8 +++++--- packages/plot/src/interactors/Region.js | 8 +++++--- packages/plot/src/interactors/Toggle.js | 9 ++++++--- 8 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/core/src/util/is-activatable.js b/packages/core/src/util/is-activatable.js index 27fab7166..262f2e698 100644 --- a/packages/core/src/util/is-activatable.js +++ b/packages/core/src/util/is-activatable.js @@ -5,5 +5,5 @@ * @returns {value is import("../Activatable.js").Activatable} */ export function isActivatable(value) { - return typeof value?.activate === 'function' && value.activate.length === 0; + return typeof value?.activate === 'function' && value.activate.length === 0; } \ No newline at end of file diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index 69f3935a1..351cd4902 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -1,4 +1,4 @@ -import { Activatable, throttle } from '@uwdata/mosaic-core'; +import { throttle } from '@uwdata/mosaic-core'; import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; @@ -34,12 +34,14 @@ function configureMark(mark) { return mark; } -export class Highlight extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Highlight { constructor(mark, { selection, channels = {} }) { - super(); this.mark = configureMark(mark); this.selection = selection; const c = Object.entries(sanitizeStyles(channels)); diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index 13cd167fa..571f2c4af 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -1,4 +1,4 @@ -import { Activatable, clauseInterval } from '@uwdata/mosaic-core'; +import { clauseInterval } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brushGroups, brushX, brushY } from './util/brush.js'; import { closeTo } from './util/close-to.js'; @@ -6,7 +6,10 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -export class Interval1D extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Interval1D { constructor(mark, { channel, selection, @@ -15,7 +18,6 @@ export class Interval1D extends Activatable { peers = true, brush: style }) { - super(); this.mark = mark; this.channel = channel; this.pixelSize = pixelSize || 1; diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index e43c42c4a..f0f6f73f9 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -1,4 +1,4 @@ -import { Activatable, clauseIntervals } from '@uwdata/mosaic-core'; +import { clauseIntervals } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brush, brushGroups } from './util/brush.js'; import { closeTo } from './util/close-to.js'; @@ -6,7 +6,10 @@ import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -export class Interval2D extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Interval2D { constructor(mark, { selection, xfield, @@ -15,7 +18,6 @@ export class Interval2D extends Activatable { peers = true, brush: style }) { - super(); this.mark = mark; this.pixelSize = pixelSize || 1; this.selection = selection; diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index fb77023e9..80b2df7b6 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -1,8 +1,11 @@ -import { Activatable, clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; +import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core'; import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; -export class Nearest extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Nearest { constructor(mark, { selection, pointer, @@ -10,7 +13,6 @@ export class Nearest extends Activatable { fields, maxRadius = 40 }) { - super(); this.mark = mark; this.selection = selection; this.clients = new Set().add(mark); diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index abc138735..ebc18bd8c 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -1,10 +1,13 @@ -import { Activatable, Selection, clauseInterval } from '@uwdata/mosaic-core'; +import { Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; const asc = (a, b) => a - b; -export class PanZoom extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class PanZoom { constructor(mark, { x = new Selection(), y = new Selection(), @@ -14,7 +17,6 @@ export class PanZoom extends Activatable { panx = true, pany = true }) { - super(); this.mark = mark; this.xsel = x; this.ysel = y; diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index 28661c82d..b6d6e3c85 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -1,4 +1,4 @@ -import { Activatable, clausePoints } from '@uwdata/mosaic-core'; +import { clausePoints } from '@uwdata/mosaic-core'; import { select } from 'd3'; import { brush } from './util/brush.js'; import { getFields } from './util/get-field.js'; @@ -8,7 +8,10 @@ import { sanitizeStyles } from './util/sanitize-styles.js'; import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; -export class Region extends Activatable { +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Region { constructor(mark, { channels, selection, @@ -19,7 +22,6 @@ export class Region extends Activatable { strokeDasharray: '1,1' } }) { - super(); this.mark = mark; this.selection = selection; this.peers = peers; diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index fb8f80d1c..f8ae80c58 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -1,8 +1,12 @@ -import { Activatable, clausePoints } from '@uwdata/mosaic-core'; +import { clausePoints } from '@uwdata/mosaic-core'; import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; -export class Toggle extends Activatable { + +/** + * @implements {import('@uwdata/mosaic-core').Activatable} + */ +export class Toggle { /** * @param {*} mark The mark to interact with. * @param {*} options The interactor options. @@ -12,7 +16,6 @@ export class Toggle extends Activatable { channels, peers = true }) { - super(); this.mark = mark; this.value = null; this.selection = selection; From 0730ad2877a3bb8faf5720f89047fa55bf74bcb5 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Mon, 10 Feb 2025 12:17:29 -0800 Subject: [PATCH 07/37] fix: update to use new isActivatable duck typing method --- packages/publish/src/MosaicPublisher.ts | 63 +++++++++++++------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index f2b5d2892..9ee5bea98 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -5,25 +5,24 @@ import chalk from 'chalk'; import path from "path"; // Mosaic modules -import { - parseSpec, astToESM, astToDOM, - SpecNode, DataNode, FileDataNode, - ParquetDataNode, OptionsNode, - CodegenContext, +import { + parseSpec, astToESM, astToDOM, + SpecNode, DataNode, FileDataNode, + ParquetDataNode, OptionsNode, + CodegenContext, QueryDataNode, } from '@uwdata/mosaic-spec'; import { ExprNode, ColumnNameRefNode } from '@uwdata/mosaic-sql'; import { MosaicClient } from '../../vgplot/src/index.js'; -import { Interactor } from '@uwdata/mosaic-plot/src/interactors/Interactor.js'; -import { Input } from '@uwdata/mosaic-inputs/src/input.js'; // Utility imports -import { - clientsReady, htmlTemplate, mockCanvas, +import { + clientsReady, htmlTemplate, mockCanvas, publishConnector, PublishContext, templateCSS, VGPLOT, LogLevel, Logger, preamble } from './util/index.js'; +import { isActivatable } from "@uwdata/mosaic-core/src/index.js"; /** * Class to facilitate publishing a Mosaic specification. @@ -46,11 +45,11 @@ export class MosaicPublisher { private ctx: PublishContext; private ast?: SpecNode; private data?: Record; - + constructor( - specPath: string, - outputPath: string, - title: string | undefined, + specPath: string, + outputPath: string, + title: string | undefined, optimize: 'minimal' | 'more' | 'most', logLevel: LogLevel ) { @@ -75,7 +74,7 @@ export class MosaicPublisher { */ public async publish() { this.logger.info(chalk.cyan('Parsing and processing specification...')); - + // Parse specification this.parseSpecification(); if (!this.ast) return; @@ -88,7 +87,7 @@ export class MosaicPublisher { } this.logger.debug(`Creating directory: ${this.outputPath}`); fs.mkdirSync(this.outputPath, { recursive: true }); - + // Setup jsdom this.logger.debug('Setting up jsdom environment...'); const dom = new JSDOM( @@ -103,14 +102,14 @@ export class MosaicPublisher { // Load the visualization in the DOM and gather interactors/inputs // TODO: fix type issue with astToDOM to remove the any cast - const { element } = await astToDOM(this.ast, {api: this.ctx.api} as any); + const { element } = await astToDOM(this.ast, { api: this.ctx.api } as any); document.body.appendChild(element); this.logger.debug('Waiting for clients to be ready...'); await clientsReady(this.ctx); const { interactors, inputs, tables } = this.processClients(); const isInteractive = interactors.size + inputs.size !== 0; - + if (isInteractive) { // Activate interactors and inputs this.logger.info(chalk.cyan('Activating interactive elements...')); @@ -118,7 +117,7 @@ export class MosaicPublisher { // Modify AST and process data (extensions, data definitions, etc.) const og = FileDataNode.prototype.codegenQuery; - FileDataNode.prototype.codegenQuery = function(ctx: CodegenContext) { + FileDataNode.prototype.codegenQuery = function (ctx: CodegenContext) { const code = og.call(this, ctx); const { file } = this; return code?.replace(`"${file}"`, `window.location.origin + "/${file}"`); @@ -148,8 +147,8 @@ export class MosaicPublisher { private processClients() { this.logger.debug('--------------- Clients ---------------'); - const interactors = new Set(); - const inputs = new Set(); + const interactors = new Set(); + const inputs = new Set(); const tables: Record> = {}; for (const client of this.ctx.coordinator.clients) { @@ -169,14 +168,15 @@ export class MosaicPublisher { } } } - } - if (client instanceof Input) { - inputs.add(client); - - const input = client as any; // TODO: change Input class to make this cleaner - if (input.from && input.column) { - if (!(input.from in tables)) tables[input.from] = new Set(); - tables[input.from].add(input.column); + + if (isActivatable(client)) { + inputs.add(client); + + const input = client as any; + if (input.from && input.column) { + if (!(input.from in tables)) tables[input.from] = new Set(); + tables[input.from].add(input.column); + } } } if (client.plot) { @@ -192,17 +192,18 @@ export class MosaicPublisher { * Activate the Interactors and Inputs, waiting * for queries to finish. */ - private async activateInteractorsAndInputs(interactors: Set, inputs: Set) { + private async activateInteractorsAndInputs(interactors: Set, inputs: Set) { this.logger.debug('--------------- Activating ---------------'); + this.ctx.coordinator.manager._logQueries = true; for (const interactor of interactors) { this.logger.debug(interactor.constructor.name); - interactor.activate(); + if (isActivatable(interactor)) interactor.activate(); await this.waitForQueryToFinish(); } for (const input of inputs) { this.logger.debug(input.constructor.name); - input.activate(); + if (isActivatable(input)) input.activate(); await this.waitForQueryToFinish(); } } From 394563e6ddda9bc999a1c8c89db9617bddd75c6c Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Mon, 10 Feb 2025 13:12:06 -0800 Subject: [PATCH 08/37] fix: Add typedef to fix JSDoc import with implements errors --- packages/inputs/src/Menu.js | 6 +++++- packages/inputs/src/Search.js | 6 +++++- packages/inputs/src/Slider.js | 6 +++++- packages/inputs/src/Table.js | 6 +++++- packages/plot/src/interactors/Highlight.js | 6 +++++- packages/plot/src/interactors/Interval1D.js | 6 +++++- packages/plot/src/interactors/Interval2D.js | 6 +++++- packages/plot/src/interactors/Nearest.js | 6 +++++- packages/plot/src/interactors/PanZoom.js | 6 +++++- packages/plot/src/interactors/Region.js | 6 +++++- packages/plot/src/interactors/Toggle.js | 5 ++++- 11 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index 8702589fc..a030ce8bd 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -2,6 +2,10 @@ import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/ import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + const isObject = v => { return v && typeof v === 'object' && !Array.isArray(v); }; @@ -11,7 +15,7 @@ export const menu = options => input(Menu, options); /** * A HTML select based dropdown menu input. * - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Menu extends MosaicClient { /** diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index d64dcb146..91953aa2d 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -2,6 +2,10 @@ import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/ import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + let _id = 0; export const search = options => input(Search, options); @@ -9,7 +13,7 @@ export const search = options => input(Search, options); /** * A HTML input based text search input. * - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Search extends MosaicClient { /** diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index 3ff3b8fd9..a9bcc8184 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -2,6 +2,10 @@ import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection import { Query, max, min } from '@uwdata/mosaic-sql'; import { input } from './input.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + let _id = 0; export const slider = options => input(Slider, options); @@ -9,7 +13,7 @@ export const slider = options => input(Slider, options); /** * A HTML range based slider input. * - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Slider extends MosaicClient { /** diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index bb0baff69..1a28fd3a2 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -3,6 +3,10 @@ import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; import { input } from './input.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + let _id = -1; export const table = options => input(Table, options); @@ -11,7 +15,7 @@ export const table = options => input(Table, options); /** * A HTML table based table component. * - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Table extends MosaicClient { /** diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index 351cd4902..6b805bd71 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -3,6 +3,10 @@ import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + function configureMark(mark) { const { channels } = mark; const dims = new Set; @@ -35,7 +39,7 @@ function configureMark(mark) { } /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Highlight { constructor(mark, { diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index 571f2c4af..bc6fe556d 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -7,7 +7,11 @@ import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + +/** + * @implements {Activatable} */ export class Interval1D { constructor(mark, { diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index f0f6f73f9..82fc12cdc 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -7,7 +7,11 @@ import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + +/** + * @implements {Activatable} */ export class Interval2D { constructor(mark, { diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index 80b2df7b6..021a71bd9 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -3,7 +3,11 @@ import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + +/** + * @implements {Activatable} */ export class Nearest { constructor(mark, { diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index ebc18bd8c..ed25dc40c 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -2,10 +2,14 @@ import { Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + const asc = (a, b) => a - b; /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class PanZoom { constructor(mark, { diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index b6d6e3c85..dfcdbcde2 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -9,7 +9,11 @@ import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ + +/** + * @implements {Activatable} */ export class Region { constructor(mark, { diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index f8ae80c58..0932baaf2 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -2,9 +2,12 @@ import { clausePoints } from '@uwdata/mosaic-core'; import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; +/** + * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable + */ /** - * @implements {import('@uwdata/mosaic-core').Activatable} + * @implements {Activatable} */ export class Toggle { /** From f42351986e1c1131c9c399be5933507c75c6d1b7 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Mon, 10 Feb 2025 18:37:01 -0800 Subject: [PATCH 09/37] chore: address and remove TODOs --- packages/publish/README.md | 2 +- packages/publish/src/MosaicPublisher.ts | 8 ++++++-- packages/publish/src/util/renderHelpers.ts | 7 +++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/publish/README.md b/packages/publish/README.md index 29bd92c36..10d9fae04 100644 --- a/packages/publish/README.md +++ b/packages/publish/README.md @@ -1,4 +1,4 @@ -# mosaic-publish TODO: Adjust this doc +# mosaic-publish A CLI tool for compiling and optimizing specifications. The tool processes spec files, handles dataset optimizations, and provides a configurable way to manage different compile and optimization tasks. diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index 9ee5bea98..592db3ed8 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -267,6 +267,7 @@ export class MosaicPublisher { */ private async exportDataFromDuckDB(tables: Record>) { const copy_queries: string[] = []; + let containsPreAggData = false; for (const node of Object.values(this.data!)) { if (!(node instanceof ParquetDataNode)) continue; const table = node.name; @@ -290,12 +291,15 @@ export class MosaicPublisher { throw new Error(`Invalid optimization level: ${this.optimize}`); } copy_queries.push(query); + + if (file.startsWith('.mosaic')) { + containsPreAggData = true; + } } if (copy_queries.length > 0) { - // TODO: Make the following conditionally on data needs fs.mkdirSync(path.join(this.outputPath, 'data'), { recursive: true }); - fs.mkdirSync(path.join(this.outputPath, 'data/.mosaic'), { recursive: true }); + if (containsPreAggData) fs.mkdirSync(path.join(this.outputPath, 'data/.mosaic'), { recursive: true }); await this.ctx.coordinator.exec(copy_queries); } diff --git a/packages/publish/src/util/renderHelpers.ts b/packages/publish/src/util/renderHelpers.ts index 7cc2b12a6..3d85e60c2 100644 --- a/packages/publish/src/util/renderHelpers.ts +++ b/packages/publish/src/util/renderHelpers.ts @@ -2,7 +2,7 @@ import { MosaicClient } from "@uwdata/mosaic-core/src/MosaicClient.js"; import { InstantiateContext } from "@uwdata/mosaic-spec"; import { Canvas } from "skia-canvas"; -export function mockCanvas (window: any) { +export function mockCanvas(window: any) { const og = window.document.createElement window.document.createElement = function (tagName: string) { if (tagName !== 'canvas') { @@ -10,7 +10,6 @@ export function mockCanvas (window: any) { } else { const canvas = new Canvas(); const el = og.call(this, tagName) as HTMLCanvasElement; - // TODO: this is a hack to get around the fact that the canvas is not a real canvas el.getContext = canvas.getContext.bind(canvas) as unknown as HTMLCanvasElement['getContext']; el.toDataURL = canvas.toDataURLSync.bind(canvas); const set = el.setAttribute; @@ -27,6 +26,6 @@ export function mockCanvas (window: any) { } export function clientsReady(ctx: InstantiateContext) { - const clients = [...ctx.coordinator.clients] as MosaicClient[]; - return Promise.allSettled(clients.map(c => c.pending)); + const clients = [...ctx.coordinator.clients] as MosaicClient[]; + return Promise.allSettled(clients.map(c => c.pending)); } \ No newline at end of file From 5060a1967008c5b21379f1ec80cb7e9bb139140c Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Wed, 12 Feb 2025 15:36:52 -0800 Subject: [PATCH 10/37] fix: Change core type export to index-types.ts pattern --- packages/core/package.json | 2 +- packages/core/src/index-types.ts | 2 ++ packages/core/src/index.js | 15 --------------- packages/core/src/types.ts | 5 ++++- packages/inputs/src/Menu.js | 5 +---- packages/inputs/src/Search.js | 5 +---- packages/inputs/src/Slider.js | 5 +---- packages/inputs/src/Table.js | 5 +---- packages/plot/src/interactors/Highlight.js | 5 +---- packages/plot/src/interactors/Interval1D.js | 5 +---- packages/plot/src/interactors/Interval2D.js | 5 +---- packages/plot/src/interactors/Nearest.js | 5 +---- packages/plot/src/interactors/PanZoom.js | 5 +---- packages/plot/src/interactors/Region.js | 5 +---- packages/plot/src/interactors/Toggle.js | 5 +---- 15 files changed, 18 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/index-types.ts diff --git a/packages/core/package.json b/packages/core/package.json index d70dc8749..4090c7505 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,7 +16,7 @@ "module": "src/index.js", "jsdelivr": "dist/mosaic-core.min.js", "unpkg": "dist/mosaic-core.min.js", - "types": "dist/types/index.d.ts", + "types": "dist/types/index-types.d.ts", "repository": { "type": "git", "url": "https://github.com/uwdata/mosaic.git" diff --git a/packages/core/src/index-types.ts b/packages/core/src/index-types.ts new file mode 100644 index 000000000..fd48ba7e2 --- /dev/null +++ b/packages/core/src/index-types.ts @@ -0,0 +1,2 @@ +export * from './index.js'; +export * from './types.js'; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 94ad4506b..3a075e89d 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -25,18 +25,3 @@ export { toDataColumns } from './util/to-data-columns.js'; export { queryFieldInfo } from './util/field-info.js'; export { jsType } from './util/js-type.js'; export { isActivatable } from './util/is-activatable.js'; - -/** - * @typedef {import('./util/selection-types.js').ClauseMetadata} ClauseMetadata - * @typedef {import('./util/selection-types.js').PointMetadata} PointMetadata - * @typedef {import('./util/selection-types.js').MatchMethod} MatchMethod - * @typedef {import('./util/selection-types.js').MatchMetadata} MatchMetadata - * @typedef {import('./util/selection-types.js').ScaleType} ScaleType - * @typedef {import('./util/selection-types.js').Extent} Extent - * @typedef {import('./util/selection-types.js').Scale} Scale - * @typedef {import('./util/selection-types.js').BinMethod} BinMethod - * @typedef {import('./util/selection-types.js').IntervalMetadata} IntervalMetadata - * @typedef {import('./util/selection-types.js').SelectionClause} SelectionClause - * - * @typedef {import('./Activatable.js').Activatable} Activatable - */ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 13832895a..f146cdb30 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,8 @@ import type { DescribeQuery, ExprNode, Query } from '@uwdata/mosaic-sql'; +export * from './util/selection-types.js'; +export * from './Activatable.js'; + /** Query type accepted by a coordinator. */ export type QueryType = | string @@ -51,4 +54,4 @@ export interface ColumnDescription { column_name: string, column_type: string, null: 'YES' | 'NO' -} +} \ No newline at end of file diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index a030ce8bd..e9a40d64b 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -2,10 +2,6 @@ import { MosaicClient, Param, isParam, isSelection, clausePoint } from '@uwdata/ import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - const isObject = v => { return v && typeof v === 'object' && !Array.isArray(v); }; @@ -15,6 +11,7 @@ export const menu = options => input(Menu, options); /** * A HTML select based dropdown menu input. * + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Menu extends MosaicClient { diff --git a/packages/inputs/src/Search.js b/packages/inputs/src/Search.js index 91953aa2d..c24bbd5ca 100644 --- a/packages/inputs/src/Search.js +++ b/packages/inputs/src/Search.js @@ -2,10 +2,6 @@ import { MosaicClient, Param, isParam, isSelection, clauseMatch } from '@uwdata/ import { Query } from '@uwdata/mosaic-sql'; import { input } from './input.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - let _id = 0; export const search = options => input(Search, options); @@ -13,6 +9,7 @@ export const search = options => input(Search, options); /** * A HTML input based text search input. * + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Search extends MosaicClient { diff --git a/packages/inputs/src/Slider.js b/packages/inputs/src/Slider.js index a9bcc8184..9e827de3a 100644 --- a/packages/inputs/src/Slider.js +++ b/packages/inputs/src/Slider.js @@ -2,10 +2,6 @@ import { MosaicClient, Param, clauseInterval, clausePoint, isParam, isSelection import { Query, max, min } from '@uwdata/mosaic-sql'; import { input } from './input.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - let _id = 0; export const slider = options => input(Slider, options); @@ -13,6 +9,7 @@ export const slider = options => input(Slider, options); /** * A HTML range based slider input. * + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Slider extends MosaicClient { diff --git a/packages/inputs/src/Table.js b/packages/inputs/src/Table.js index 1a28fd3a2..4fe0a34bc 100644 --- a/packages/inputs/src/Table.js +++ b/packages/inputs/src/Table.js @@ -3,10 +3,6 @@ import { Query, desc } from '@uwdata/mosaic-sql'; import { formatDate, formatLocaleAuto, formatLocaleNumber } from './util/format.js'; import { input } from './input.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - let _id = -1; export const table = options => input(Table, options); @@ -15,6 +11,7 @@ export const table = options => input(Table, options); /** * A HTML table based table component. * + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Table extends MosaicClient { diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index 6b805bd71..a2de0a283 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -3,10 +3,6 @@ import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - function configureMark(mark) { const { channels } = mark; const dims = new Set; @@ -39,6 +35,7 @@ function configureMark(mark) { } /** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Highlight { diff --git a/packages/plot/src/interactors/Interval1D.js b/packages/plot/src/interactors/Interval1D.js index bc6fe556d..50825feb7 100644 --- a/packages/plot/src/interactors/Interval1D.js +++ b/packages/plot/src/interactors/Interval1D.js @@ -7,10 +7,7 @@ import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; /** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - -/** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Interval1D { diff --git a/packages/plot/src/interactors/Interval2D.js b/packages/plot/src/interactors/Interval2D.js index 82fc12cdc..3b6a630ed 100644 --- a/packages/plot/src/interactors/Interval2D.js +++ b/packages/plot/src/interactors/Interval2D.js @@ -7,10 +7,7 @@ import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; /** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - -/** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Interval2D { diff --git a/packages/plot/src/interactors/Nearest.js b/packages/plot/src/interactors/Nearest.js index 021a71bd9..fe6fcceb7 100644 --- a/packages/plot/src/interactors/Nearest.js +++ b/packages/plot/src/interactors/Nearest.js @@ -3,10 +3,7 @@ import { select, pointer, min } from 'd3'; import { getField } from './util/get-field.js'; /** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - -/** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Nearest { diff --git a/packages/plot/src/interactors/PanZoom.js b/packages/plot/src/interactors/PanZoom.js index ed25dc40c..d83f147e4 100644 --- a/packages/plot/src/interactors/PanZoom.js +++ b/packages/plot/src/interactors/PanZoom.js @@ -2,13 +2,10 @@ import { Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; -/** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - const asc = (a, b) => a - b; /** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class PanZoom { diff --git a/packages/plot/src/interactors/Region.js b/packages/plot/src/interactors/Region.js index dfcdbcde2..8a616682f 100644 --- a/packages/plot/src/interactors/Region.js +++ b/packages/plot/src/interactors/Region.js @@ -9,10 +9,7 @@ import { neqSome } from './util/neq.js'; import { getDatum } from './util/get-datum.js'; /** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - -/** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Region { diff --git a/packages/plot/src/interactors/Toggle.js b/packages/plot/src/interactors/Toggle.js index 0932baaf2..0f8d60bd3 100644 --- a/packages/plot/src/interactors/Toggle.js +++ b/packages/plot/src/interactors/Toggle.js @@ -3,10 +3,7 @@ import { getDatum } from './util/get-datum.js'; import { neq, neqSome } from './util/neq.js'; /** - * @typedef {import('@uwdata/mosaic-core').Activatable} Activatable - */ - -/** + * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Toggle { From 6de3271c21ed786fce478fb76ee7a5502fc3ab05 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Wed, 12 Feb 2025 18:06:38 -0800 Subject: [PATCH 11/37] chore: move exports to index-types and remove redundant file --- packages/core/src/Activatable.ts | 10 ---------- packages/core/src/index-types.ts | 1 + packages/core/src/types.ts | 10 +++++++--- 3 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 packages/core/src/Activatable.ts diff --git a/packages/core/src/Activatable.ts b/packages/core/src/Activatable.ts deleted file mode 100644 index 0a590f247..000000000 --- a/packages/core/src/Activatable.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { isActivatable } from "./util/is-activatable.js"; - -/** - * Interface for activatable objects. - * - * @see {@link isActivatable} - */ -export interface Activatable { - activate(): void; -} \ No newline at end of file diff --git a/packages/core/src/index-types.ts b/packages/core/src/index-types.ts index fd48ba7e2..40393abe0 100644 --- a/packages/core/src/index-types.ts +++ b/packages/core/src/index-types.ts @@ -1,2 +1,3 @@ export * from './index.js'; export * from './types.js'; +export * from './util/selection-types.js'; \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f146cdb30..f2c86d0a4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,8 +1,5 @@ import type { DescribeQuery, ExprNode, Query } from '@uwdata/mosaic-sql'; -export * from './util/selection-types.js'; -export * from './Activatable.js'; - /** Query type accepted by a coordinator. */ export type QueryType = | string @@ -54,4 +51,11 @@ export interface ColumnDescription { column_name: string, column_type: string, null: 'YES' | 'NO' +} + +/** + * Interface for activatable objects. + */ +export interface Activatable { + activate(): void; } \ No newline at end of file From c3607e52373dd81c0ef4593cec6ff2ee3c3a52e5 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 12:51:33 -0800 Subject: [PATCH 12/37] chore: update package-lock.json --- package-lock.json | 462 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 461 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b89e0a0a3..8dd3c4581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4016,6 +4016,18 @@ "@types/sizzle": "*" } }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4101,6 +4113,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/underscore": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz", @@ -4122,6 +4141,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -4159,6 +4195,10 @@ "resolved": "packages/sql", "link": true }, + "node_modules/@uwdata/publish": { + "resolved": "packages/publish", + "link": true + }, "node_modules/@uwdata/vgplot": { "resolved": "packages/vgplot", "link": true @@ -5326,6 +5366,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.9.tgz", + "integrity": "sha512-6F+UYzTaGB+awsTXg0uSJA1/b/B3DDJzpKVRu0UmyI7DmNeaAl2RFHuTGIN6fEgpadRxoXGb7gbC1xo4C3IdyA==", + "license": "MIT", + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -6630,6 +6679,21 @@ "dev": true, "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -10665,6 +10729,18 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -11445,6 +11521,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11749,6 +11834,12 @@ "node": ">=6" } }, + "node_modules/parenthesis": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz", + "integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==", + "license": "MIT" + }, "node_modules/parse-conflict-json": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", @@ -11848,7 +11939,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -13162,6 +13252,51 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -13175,6 +13310,305 @@ "node": ">=10" } }, + "node_modules/skia-canvas": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/skia-canvas/-/skia-canvas-2.0.2.tgz", + "integrity": "sha512-LUa7P41NRNoCWhvPyX4aKP5SpeWDXmWYbonCt4FfkEdTuSssxpvYiK5Y69B0MudDR6LVNt9RBwpZfuCRpVSbbw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "cargo-cp-artifact": "^0.1", + "glob": "^11.0.0", + "path-browserify": "^1.0.1", + "simple-get": "^4.0.1", + "string-split-by": "^1.0.0" + } + }, + "node_modules/skia-canvas/node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/skia-canvas/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/skia-canvas/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/skia-canvas/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/skia-canvas/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/skia-canvas/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/skia-canvas/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/skia-canvas/node_modules/jackspeak": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.3.tgz", + "integrity": "sha512-oSwM7q8PTHQWuZAlp995iPpPJ4Vkl7qT0ZRD+9duL9j2oBy6KcTfyxc8mEuHJYC+z/kbps80aJLkaNzTOrf/kw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/skia-canvas/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/skia-canvas/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/skia-canvas/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/skia-canvas/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/skia-canvas/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/skia-canvas/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skia-canvas/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13409,6 +13843,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-split-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", + "integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==", + "license": "MIT", + "dependencies": { + "parenthesis": "^3.1.5" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -16176,6 +16619,23 @@ "isoformat": "^0.2.1" } }, + "packages/publish": { + "name": "@uwdata/publish", + "version": "1.0.0", + "dependencies": { + "chalk": "^4.1.2", + "skia-canvas": "^2.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "mosaic-publish": "dist/publish/src/index.js" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/yargs": "^17.0.33", + "typescript": "^5.6.3" + } + }, "packages/spec": { "name": "@uwdata/mosaic-spec", "version": "0.12.2", From 97fc6a9b9238f715c2161e88d748aa334f2a614b Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 13:02:15 -0800 Subject: [PATCH 13/37] fix: fix core import to work with new types exporting --- packages/publish/src/MosaicPublisher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index 592db3ed8..d9d2f1724 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -22,7 +22,7 @@ import { LogLevel, Logger, preamble } from './util/index.js'; -import { isActivatable } from "@uwdata/mosaic-core/src/index.js"; +import { isActivatable } from "@uwdata/mosaic-core"; /** * Class to facilitate publishing a Mosaic specification. From ccbed1532c897f2ff3db71a5b1bf9b4e4175b84b Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 13:09:17 -0800 Subject: [PATCH 14/37] fix: Fix core import in renderHelpers --- packages/publish/src/util/renderHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/publish/src/util/renderHelpers.ts b/packages/publish/src/util/renderHelpers.ts index 3d85e60c2..b57edad28 100644 --- a/packages/publish/src/util/renderHelpers.ts +++ b/packages/publish/src/util/renderHelpers.ts @@ -1,4 +1,4 @@ -import { MosaicClient } from "@uwdata/mosaic-core/src/MosaicClient.js"; +import { MosaicClient } from "@uwdata/mosaic-core"; import { InstantiateContext } from "@uwdata/mosaic-spec"; import { Canvas } from "skia-canvas"; From 229cf72e2de822d5429f4ad31519f332b53a563f Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 13:17:02 -0800 Subject: [PATCH 15/37] fix: remove unnecessary changes from #682 --- packages/core/src/util/is-activatable.js | 1 - packages/plot/src/interactors/Highlight.js | 8 -------- 2 files changed, 9 deletions(-) diff --git a/packages/core/src/util/is-activatable.js b/packages/core/src/util/is-activatable.js index 62f37e1c0..631791ece 100644 --- a/packages/core/src/util/is-activatable.js +++ b/packages/core/src/util/is-activatable.js @@ -6,4 +6,3 @@ export function isActivatable(value) { return typeof value?.activate === 'function' && value.activate.length === 0; } - diff --git a/packages/plot/src/interactors/Highlight.js b/packages/plot/src/interactors/Highlight.js index a2de0a283..be10c26f8 100644 --- a/packages/plot/src/interactors/Highlight.js +++ b/packages/plot/src/interactors/Highlight.js @@ -34,10 +34,6 @@ function configureMark(mark) { return mark; } -/** - * @import {Activatable} from '@uwdata/mosaic-core' - * @implements {Activatable} - */ export class Highlight { constructor(mark, { selection, @@ -84,10 +80,6 @@ export class Highlight { } } } - - activate() { - return this.update(); - } } async function predicateFunction(mark, selection) { From cf778e6ab4774fd91deb86ba95a06e7cd9159cb7 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 13:30:51 -0800 Subject: [PATCH 16/37] feat: polish README --- packages/publish/README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/publish/README.md b/packages/publish/README.md index 10d9fae04..0011e3c93 100644 --- a/packages/publish/README.md +++ b/packages/publish/README.md @@ -1,8 +1,8 @@ # mosaic-publish -A CLI tool for compiling and optimizing specifications. The tool processes spec files, handles dataset optimizations, and provides a configurable way to manage different compile and optimization tasks. +A CLI tool and API for compiling and optimizing specifications. The tool processes spec files, handles dataset optimizations, and provides a configurable way to manage different compile and optimization tasks. -## Installation +## CLI Installation This tool is designed to be used via `npx` or can be installed globally. @@ -24,4 +24,26 @@ Now you can run the tool from anywhere: ```bash mosaic-publish --spec ./path/to/spec.json +``` + +## API Usage + +The functionality of mosaic-publish can also be accessed programmatically via its API. For example: + +```js +import { MosaicPublisher } from '@uwdata/publish'; + +const publisher = new MosaicPublisher({ + spec: './path/to/spec.json', + outputDir: './docs/public/schema', + verbose: true +}); + +publisher.publish() + .then(() => { + console.log('Specification compiled and optimized successfully.'); + }) + .catch(err => { + console.error('Error during publish:', err); + }); ``` \ No newline at end of file From d4cc2673fae6b97fa3a3e1d3790a6a0a545081af Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 16 Feb 2025 16:37:31 -0800 Subject: [PATCH 17/37] feat: improve logging and npm exporting --- package-lock.json | 2 +- packages/publish/src/MosaicPublisher.ts | 130 ++++++++++---------- packages/publish/src/cli.ts | 103 ++++++++++++++++ packages/publish/src/index.ts | 93 +------------- packages/publish/src/util/Logger.ts | 49 +++++--- packages/publish/src/util/constants.ts | 6 +- packages/publish/src/util/publishHelpers.ts | 1 + 7 files changed, 211 insertions(+), 173 deletions(-) create mode 100755 packages/publish/src/cli.ts mode change 100755 => 100644 packages/publish/src/index.ts diff --git a/package-lock.json b/package-lock.json index 8dd3c4581..7896141da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16628,7 +16628,7 @@ "yargs": "^17.7.2" }, "bin": { - "mosaic-publish": "dist/publish/src/index.js" + "mosaic-publish": "dist/publish/src/cli.js" }, "devDependencies": { "@types/jsdom": "^21.1.7", diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index d9d2f1724..c010a9aa1 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -24,20 +24,31 @@ import { } from './util/index.js'; import { isActivatable } from "@uwdata/mosaic-core"; +/** + * Error class for know publishing errors. + * @param message Error message. + */ +export class PublishError extends Error { + constructor(message: string) { + super(message); + this.name = 'PublishError'; + } +} + /** * Class to facilitate publishing a Mosaic specification. - * @param specPath Path to the Mosaic specification file. + * @param spec Contents of JSON Mosaic specification file. * @param outputPath Path to the desired output directory. * @param title Optional title for the HTML file. * @param optimize Level of optimization for published visualization. - * @param logLevel Desired logging level (debug, info, warn, error, silent) + * @param logger Optional logger instance to use for logging. * * The `publish` method is the main entry point for the class */ export class MosaicPublisher { - private specPath: string; + private spec: string; private outputPath: string; - private title?: string; + private title: string; private optimize: 'minimal' | 'more' | 'most'; private logger: Logger; @@ -47,49 +58,39 @@ export class MosaicPublisher { private data?: Record; constructor( - specPath: string, - outputPath: string, - title: string | undefined, - optimize: 'minimal' | 'more' | 'most', - logLevel: LogLevel + spec: string, + outputPath = 'out', + title = 'Mosaic Visualization', + optimize: 'minimal' | 'more' | 'most' = 'minimal', + logger: Logger = new Logger(LogLevel.INFO) ) { - this.specPath = specPath; + this.spec = spec; this.outputPath = outputPath; this.title = title; this.optimize = optimize; - - // Create our logger instance - this.logger = new Logger(logLevel); + this.logger = logger; // Create PublishContext const connector = publishConnector(); this.ctx = new PublishContext(connector); - if (logLevel !== 'debug') { - this.ctx.coordinator.logger(false); - } } /** * Main entry point for publishing a Mosaic specification. */ public async publish() { - this.logger.info(chalk.cyan('Parsing and processing specification...')); + this.logger.info('Parsing and processing spec...'); // Parse specification - this.parseSpecification(); + try { + this.ast = parseSpec(yaml.parse(this.spec)); + } catch (err) { + throw new PublishError(`Failed to parse specification: ${err}`); + } if (!this.ast) return; this.data = this.ast.data; - // If spec is valid create relevant output directory - if (fs.existsSync(this.outputPath)) { - this.logger.debug(`Removing existing directory: ${this.outputPath}`); - fs.rmSync(this.outputPath, { recursive: true }); - } - this.logger.debug(`Creating directory: ${this.outputPath}`); - fs.mkdirSync(this.outputPath, { recursive: true }); - // Setup jsdom - this.logger.debug('Setting up jsdom environment...'); const dom = new JSDOM( ``, { pretendToBeVisual: true } @@ -104,15 +105,23 @@ export class MosaicPublisher { // TODO: fix type issue with astToDOM to remove the any cast const { element } = await astToDOM(this.ast, { api: this.ctx.api } as any); document.body.appendChild(element); - this.logger.debug('Waiting for clients to be ready...'); await clientsReady(this.ctx); const { interactors, inputs, tables } = this.processClients(); const isInteractive = interactors.size + inputs.size !== 0; + // If spec is valid create relevant output directory + if (fs.existsSync(this.outputPath)) { + this.logger.warn(`Clearing output directory: ${this.outputPath}`); + fs.rmSync(this.outputPath, { recursive: true }); + fs.mkdirSync(this.outputPath, { recursive: true }); + } else { + this.logger.info(`Creating output directory: ${this.outputPath}`); + fs.mkdirSync(this.outputPath, { recursive: true }); + } + if (isInteractive) { // Activate interactors and inputs - this.logger.info(chalk.cyan('Activating interactive elements...')); await this.activateInteractorsAndInputs(interactors, inputs); // Modify AST and process data (extensions, data definitions, etc.) @@ -122,37 +131,21 @@ export class MosaicPublisher { const { file } = this; return code?.replace(`"${file}"`, `window.location.origin + "/${file}"`); }; - this.logger.debug('Updating data nodes...'); await this.updateDataNodes(tables); // Export relevant data from DuckDB to Parquet - this.logger.info(chalk.cyan('Exporting data to Parquet...')); await this.exportDataFromDuckDB(tables); } - this.logger.info(chalk.cyan('Writing output files...')); this.writeFiles(isInteractive, element); } - private parseSpecification() { - try { - const specFile = fs.readFileSync(this.specPath, 'utf-8'); - this.ast = parseSpec(yaml.parse(specFile)); - } catch (e) { - console.error('Error parsing spec:', e); - process.exit(1); - } - } - private processClients() { - this.logger.debug('--------------- Clients ---------------'); - const interactors = new Set(); const inputs = new Set(); const tables: Record> = {}; for (const client of this.ctx.coordinator.clients) { - this.logger.debug(client.constructor.name); if (client instanceof MosaicClient) { const fields = client.fields(); if (fields) { @@ -193,16 +186,13 @@ export class MosaicPublisher { * for queries to finish. */ private async activateInteractorsAndInputs(interactors: Set, inputs: Set) { - this.logger.debug('--------------- Activating ---------------'); this.ctx.coordinator.manager._logQueries = true; for (const interactor of interactors) { - this.logger.debug(interactor.constructor.name); if (isActivatable(interactor)) interactor.activate(); await this.waitForQueryToFinish(); } for (const input of inputs) { - this.logger.debug(input.constructor.name); if (isActivatable(input)) input.activate(); await this.waitForQueryToFinish(); } @@ -218,7 +208,7 @@ export class MosaicPublisher { * Process data definitions (DataNodes) and converts them to ParquetDataNode * objects based on specified tables. */ - private async updateDataNodes(tables: Record>) { + private async updateDataNodes(tables: Record | null>) { // process data definitions for (const node of Object.values(this.data!)) { if (!(node.name in tables)) { @@ -237,9 +227,11 @@ export class MosaicPublisher { for (const table of db_tables) { if (table.name.startsWith('preagg_')) { const name = `${table.schema}.${table.name}`; - tables[name] = new Set(table.column_names); + tables[name] = null; const file = `data/.mosaic/${table.name}.parquet`; this.ast!.data[name] = new ParquetDataNode(name, file, new OptionsNode({})); + } else if (table.name in tables && tables[table.name] == new Set(table.column_names)) { + tables[table.name] = null; } } } @@ -265,16 +257,17 @@ export class MosaicPublisher { /** * Export relevant data from DuckDB to local Parquet */ - private async exportDataFromDuckDB(tables: Record>) { - const copy_queries: string[] = []; - let containsPreAggData = false; + private async exportDataFromDuckDB(tables: Record | null>) { + const table_copy_queries: string[] = []; + const materialized_view_copy_queries: string[] = []; for (const node of Object.values(this.data!)) { if (!(node instanceof ParquetDataNode)) continue; const table = node.name; const file = node.file; const relevant_columns = tables[table]; - if (relevant_columns.size === 0) { - this.logger.debug(`No relevant columns found for table ${table}`); + if (relevant_columns?.size === 0) { + this.logger.warn(`Skipping export of table: ${table}`); + this.logger.warn(`No columns are being used from this table.`); continue; } @@ -285,23 +278,34 @@ export class MosaicPublisher { break; case 'more': case 'most': - query = `COPY (SELECT ${Array.from(relevant_columns).join(', ')} FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + if (relevant_columns === null) { + query = `COPY (SELECT * FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + } else { + this.logger.warn(`Partially exporting table: ${table}`); + query = `COPY (SELECT ${Array.from(relevant_columns).join(', ')} FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + } break; default: - throw new Error(`Invalid optimization level: ${this.optimize}`); + throw new PublishError(`Invalid optimization level: ${this.optimize}`); } - copy_queries.push(query); - if (file.startsWith('.mosaic')) { - containsPreAggData = true; + if (table.startsWith('mosaic.preagg_')) { + materialized_view_copy_queries.push(query); + } else { + table_copy_queries.push(query); } } - if (copy_queries.length > 0) { + if (table_copy_queries.length > 0 || materialized_view_copy_queries.length > 0) { + this.logger.info('Exporting data tables to Parquet...'); fs.mkdirSync(path.join(this.outputPath, 'data'), { recursive: true }); - if (containsPreAggData) fs.mkdirSync(path.join(this.outputPath, 'data/.mosaic'), { recursive: true }); + await this.ctx.coordinator.exec(table_copy_queries); - await this.ctx.coordinator.exec(copy_queries); + if (materialized_view_copy_queries.length > 0) { + this.logger.info('Exporting materialized views to Parquet...'); + fs.mkdirSync(path.join(this.outputPath, 'data/.mosaic'), { recursive: true }); + await this.ctx.coordinator.exec(materialized_view_copy_queries); + } } } } diff --git a/packages/publish/src/cli.ts b/packages/publish/src/cli.ts new file mode 100755 index 000000000..054397fee --- /dev/null +++ b/packages/publish/src/cli.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env node + +import { MosaicPublisher, PublishError } from './MosaicPublisher.js'; +import { LogLevel, toLogLevel, Logger } from './util/index.js'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import chalk from 'chalk'; +import fs from 'fs'; + +// Build the command-line parser without using the yargs type parameter: +const parsed = yargs(hideBin(process.argv)) + .scriptName('mosaic-publish') + .usage('$0 [args]') + .options({ + spec: { + alias: 's', + type: 'string', + describe: 'Path to the specification file', + demandOption: true, + }, + optimize: { + alias: 'o', + type: 'string', + describe: 'Level of optimizations', + choices: ['minimal', 'more', 'most'] as const, + default: 'minimal', + }, + output: { + alias: 'out', + type: 'string', + describe: 'Output folder path for the result', + }, + title: { + alias: 't', + type: 'string', + describe: 'Title of published visualization', + }, + logLevel: { + alias: 'l', + type: 'string', + describe: 'Logging level', + choices: Object.values(LogLevel) + .filter((key) => typeof key === 'string') + .map((key) => key.toLowerCase()), + default: 'info' + } + }) + .help() + .parseSync(); + +const argv = { + spec: parsed.spec as string, + optimize: parsed.optimize as 'minimal' | 'more' | 'most', + output: parsed.output as string | undefined, + title: parsed.title as string | undefined, +}; +const logger = new Logger(toLogLevel(parsed.logLevel)); + +// Pretty-print the publishing configuration +logger.info(chalk.dim('================================')) +logger.info(chalk.bold(chalk.yellow('Publishing Configuration'))); +logger.info(chalk.blue('Spec Path:'), argv.spec); +logger.info(chalk.blue('Optimization Level:'), argv.optimize); +if (argv.output) { + logger.info(chalk.blue('Output Path:'), argv.output); +} +if (argv.title) { + logger.info(chalk.bold('Visualization Title:'), argv.title); +} +logger.info(chalk.dim('================================')) + +let spec: string; +try { + spec = fs.readFileSync(argv.spec, 'utf-8'); + logger.info(chalk.greenBright('Specification file successfully read.')); +} catch (err) { + logger.error('Failed to read the spec file:\n', err); + process.exit(1); +} + +// Instantiate the publisher +const publisher = new MosaicPublisher( + spec, + argv.output, + argv.title, + argv.optimize, + logger +); + +// Execute publishing in an async context +(async () => { + try { + await publisher.publish(); + logger.info(chalk.greenBright('✔ Publishing complete! ✔')); + } catch (err) { + if (err instanceof PublishError) { + logger.error('An error occurred while publishing:\n', err); + } else { + logger.error('An unexpected error occurred:\n', err); + } + process.exit(1); + } +})(); \ No newline at end of file diff --git a/packages/publish/src/index.ts b/packages/publish/src/index.ts old mode 100755 new mode 100644 index 0773a4801..f02746489 --- a/packages/publish/src/index.ts +++ b/packages/publish/src/index.ts @@ -1,92 +1 @@ -#!/usr/bin/env node - -import { MosaicPublisher } from './MosaicPublisher.js'; -import { LogLevel } from './util/index.js'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import chalk from 'chalk'; - -interface CliArgs { - spec: string; - optimize: 'minimal' | 'more' | 'most'; - output?: string; - title?: string; - logLevel: LogLevel; -} - -// Build the command-line parser without using the yargs type parameter: -const parsed = yargs(hideBin(process.argv)) - .scriptName('mosaic-publish') - .usage('$0 [args]') - .options({ - spec: { - alias: 's', - type: 'string', - describe: 'Path to the specification file', - demandOption: true, - }, - optimize: { - alias: 'o', - type: 'string', - describe: 'Level of optimizations', - choices: ['minimal', 'more', 'most'] as const, - default: 'minimal', - }, - output: { - alias: 'out', - type: 'string', - describe: 'Output folder path for the result', - }, - title: { - alias: 't', - type: 'string', - describe: 'Title of published visualization', - }, - logLevel: { - alias: 'l', - type: 'string', - describe: 'Logging level (debug, info, warn, error, silent)', - choices: ['debug', 'info', 'warn', 'error', 'silent'] as const, - default: 'info' - } - }) - .help() - .parseSync(); - -// Cast the results to our interface -const argv: CliArgs = { - spec: parsed.spec, - optimize: parsed.optimize as 'minimal' | 'more' | 'most', - output: parsed.output, - title: parsed.title, - logLevel: parsed.logLevel as LogLevel -}; - -// Provide user-facing pretty-print statements -console.log(chalk.bold.blue('Spec file:'), chalk.yellowBright(argv.spec)); -console.log(chalk.bold.blue('Optimize:'), chalk.yellowBright(argv.optimize)); -console.log(chalk.bold.blue('Output:'), chalk.yellowBright(argv.output ?? 'out/')); -if (argv.title) { - console.log(chalk.bold.blue('Title:'), chalk.yellowBright(argv.title)); -} - -// Instantiate the publisher -const publisher = new MosaicPublisher( - argv.spec, - argv.output ?? 'out', - argv.title, - argv.optimize, - argv.logLevel -); - -// Execute publishing in an async context -(async () => { - try { - console.log(chalk.cyan('Publishing visualization...')); - await publisher.publish(); - console.log(chalk.bold.green('✔ Publish complete!')); - } catch (err) { - console.error(chalk.red('An error occurred while publishing:'), err); - process.exit(1); - } -})(); \ No newline at end of file +export { MosaicPublisher, PublishError } from './MosaicPublisher.js'; \ No newline at end of file diff --git a/packages/publish/src/util/Logger.ts b/packages/publish/src/util/Logger.ts index fd966ee18..3ea9b573d 100644 --- a/packages/publish/src/util/Logger.ts +++ b/packages/publish/src/util/Logger.ts @@ -1,25 +1,46 @@ -export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; +import chalk from 'chalk'; +import { format } from 'util'; + +export enum LogLevel { + INFO = 0, + WARN = 1, + ERROR = 2, + SILENT = 3 +} + +export const toLogLevel = (level: string): LogLevel => { + switch (level.toUpperCase()) { + case 'INFO': return LogLevel.INFO; + case 'WARN': return LogLevel.WARN; + case 'ERROR': return LogLevel.ERROR; + case 'SILENT': return LogLevel.SILENT; + default: throw new Error(`Invalid log level: ${level}`); + } +} + export class Logger { - constructor(private level: LogLevel) {} + constructor(private level: LogLevel) { } - debug(...args: unknown[]) { - if (this.level === 'debug') { - console.log(...args); - } + private log(logFunction: (message: string) => void, prefix: string, ...args: any[]) { + const str = args.map(arg => format('%s', arg)).join(' '); + logFunction(str.split('\n').map(line => prefix + line).join('\n')); } + info(...args: unknown[]) { - if (this.level === 'debug' || this.level === 'info') { - console.log(...args); + if (this.level <= LogLevel.INFO) { + this.log(console.log, chalk.blueBright('[INFO] '), ...args); } } - warn(...args: unknown[]) { - if (['debug','info','warn'].includes(this.level)) { - console.warn(...args); + + warn(...args: any[]) { + if (this.level <= LogLevel.WARN) { + this.log(console.warn, chalk.yellowBright('[WARN] '), ...args); } } - error(...args: unknown[]) { - if (this.level !== 'silent') { - console.error(...args); + + error(...args: any[]) { + if (this.level <= LogLevel.ERROR) { + this.log(console.error, chalk.redBright('[ERROR] '), ...args); } } } \ No newline at end of file diff --git a/packages/publish/src/util/constants.ts b/packages/publish/src/util/constants.ts index c7d46a915..6dde42ec0 100644 --- a/packages/publish/src/util/constants.ts +++ b/packages/publish/src/util/constants.ts @@ -1,8 +1,8 @@ -export const htmlTemplate = (isInteractive: boolean, title?: string, element?: HTMLElement | SVGElement, css = templateCSS) => ` +export const htmlTemplate = (isInteractive: boolean, title: string, element?: HTMLElement | SVGElement, css = templateCSS) => ` - ${title ?? 'Mosaic Visualization'} + ${title}
@@ -21,7 +21,7 @@ ${isInteractive ? ` ${css} `; -export const templateCSS =``; export const VGPLOT = 'https://cdn.jsdelivr.net/npm/@uwdata/vgplot@latest/dist/vgplot.js'; +export const FLECHETTE = 'https://cdn.jsdelivr.net/npm/@uwdata/flechette@latest/dist/flechette.js'; // TODO: switch this to ./renderHelpers version when changes pushed to npm // Currently, this is hack to see when clients are ready use .pending when it is available -export const preamble = `export function clientsReady() { +const clientsReady = `export function clientsReady() { const clients = [...vg.coordinator().clients]; return Promise.allSettled(clients.map(c => c.initialize())) -}` \ No newline at end of file +}` + +const loadCache = (cacheFile: string) => ` +const cacheBytes = await fetch(window.location.origin + "/${cacheFile}").then(res => res.arrayBuffer()); +vg.coordinator().manager.cache().import(tableFromIPC(cacheBytes).get(0).cache);`; + +export const preamble = (needsClientReady: boolean, cacheFile?: string) => { + if (!needsClientReady && !cacheFile) { + return undefined + } + return ` +${needsClientReady ? clientsReady : ''} +${cacheFile ? loadCache(cacheFile) : ''} +` +} \ No newline at end of file diff --git a/packages/spec/src/ast-to-dom.js b/packages/spec/src/ast-to-dom.js index 1006dd214..9b3712450 100644 --- a/packages/spec/src/ast-to-dom.js +++ b/packages/spec/src/ast-to-dom.js @@ -1,4 +1,4 @@ -import { Param, Selection } from '@uwdata/mosaic-core'; +import { Coordinator, Param, Selection } from '@uwdata/mosaic-core'; import { createAPIContext, loadExtension } from '@uwdata/vgplot'; import { SpecNode } from './ast/SpecNode.js'; import { resolveExtensions } from './config/extensions.js'; @@ -69,6 +69,7 @@ export class InstantiateContext { this.plotDefaults = plotDefaults; this.activeParams = params; this.baseURL = baseURL; + /** @type {Coordinator} */ this.coordinator = api.context.coordinator; } From 600c702da386acdf84d4d0ae80256ae07386f2c4 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Thu, 20 Mar 2025 16:15:53 -0700 Subject: [PATCH 22/37] feat: add rollup to not be dependent on cdn --- package-lock.json | 358 +++++++++++++++++------- packages/publish/package.json | 5 + packages/publish/src/MosaicPublisher.ts | 39 ++- packages/publish/src/util/constants.ts | 6 +- 4 files changed, 293 insertions(+), 115 deletions(-) diff --git a/package-lock.json b/package-lock.json index 065084370..14bc2c6c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1755,7 +1755,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -3359,14 +3358,161 @@ "react": "^16.14.0 || >=17" } }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", + "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", - "integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", + "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3374,13 +3520,12 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz", - "integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", + "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3388,13 +3533,12 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz", - "integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", + "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3402,13 +3546,12 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz", - "integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", + "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3416,13 +3559,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz", - "integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", + "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3430,13 +3572,12 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz", - "integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", + "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3444,13 +3585,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz", - "integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", + "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3458,13 +3598,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz", - "integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", + "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3472,13 +3611,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz", - "integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", + "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3486,13 +3624,12 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz", - "integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", + "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3500,13 +3637,12 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz", - "integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", + "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3514,13 +3650,12 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz", - "integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", + "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3528,13 +3663,12 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz", - "integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", + "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3542,13 +3676,12 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz", - "integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", + "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3556,9 +3689,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz", - "integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", + "integrity": "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA==", "cpu": [ "x64" ], @@ -3569,13 +3702,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz", - "integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.35.0.tgz", + "integrity": "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3583,13 +3715,12 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz", - "integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", + "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3597,13 +3728,12 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz", - "integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", + "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3611,13 +3741,12 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz", - "integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", + "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4106,6 +4235,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, "node_modules/@types/sizzle": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", @@ -5809,6 +5944,12 @@ "dev": true, "license": "ISC" }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -6730,7 +6871,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8255,7 +8395,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8270,7 +8409,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8764,7 +8902,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9196,7 +9333,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -9272,6 +9408,12 @@ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", "license": "MIT" }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10242,7 +10384,6 @@ "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" @@ -11973,7 +12114,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -12750,7 +12890,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -12971,10 +13110,9 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.34.7", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz", - "integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==", - "dev": true, + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", + "integrity": "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -12987,25 +13125,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.7", - "@rollup/rollup-android-arm64": "4.34.7", - "@rollup/rollup-darwin-arm64": "4.34.7", - "@rollup/rollup-darwin-x64": "4.34.7", - "@rollup/rollup-freebsd-arm64": "4.34.7", - "@rollup/rollup-freebsd-x64": "4.34.7", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.7", - "@rollup/rollup-linux-arm-musleabihf": "4.34.7", - "@rollup/rollup-linux-arm64-gnu": "4.34.7", - "@rollup/rollup-linux-arm64-musl": "4.34.7", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.7", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", - "@rollup/rollup-linux-riscv64-gnu": "4.34.7", - "@rollup/rollup-linux-s390x-gnu": "4.34.7", - "@rollup/rollup-linux-x64-gnu": "4.34.7", - "@rollup/rollup-linux-x64-musl": "4.34.7", - "@rollup/rollup-win32-arm64-msvc": "4.34.7", - "@rollup/rollup-win32-ia32-msvc": "4.34.7", - "@rollup/rollup-win32-x64-msvc": "4.34.7", + "@rollup/rollup-android-arm-eabi": "4.35.0", + "@rollup/rollup-android-arm64": "4.35.0", + "@rollup/rollup-darwin-arm64": "4.35.0", + "@rollup/rollup-darwin-x64": "4.35.0", + "@rollup/rollup-freebsd-arm64": "4.35.0", + "@rollup/rollup-freebsd-x64": "4.35.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", + "@rollup/rollup-linux-arm-musleabihf": "4.35.0", + "@rollup/rollup-linux-arm64-gnu": "4.35.0", + "@rollup/rollup-linux-arm64-musl": "4.35.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", + "@rollup/rollup-linux-riscv64-gnu": "4.35.0", + "@rollup/rollup-linux-s390x-gnu": "4.35.0", + "@rollup/rollup-linux-x64-gnu": "4.35.0", + "@rollup/rollup-linux-x64-musl": "4.35.0", + "@rollup/rollup-win32-arm64-msvc": "4.35.0", + "@rollup/rollup-win32-ia32-msvc": "4.35.0", + "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" } }, @@ -14014,7 +14152,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -16623,8 +16760,13 @@ "name": "@uwdata/publish", "version": "1.0.0", "dependencies": { + "@rollup/plugin-commonjs": "^28.0.3", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-virtual": "^3.0.2", "@uwdata/flechette": "^2.0.0", + "@uwdata/vgplot": "^0.12.2", "chalk": "^4.1.2", + "rollup": "^4.35.0", "skia-canvas": "^2.0.2", "yargs": "^17.7.2" }, diff --git a/packages/publish/package.json b/packages/publish/package.json index 2460fa5c2..65a3b0609 100644 --- a/packages/publish/package.json +++ b/packages/publish/package.json @@ -13,8 +13,13 @@ "test": "npm run build && vitest run" }, "dependencies": { + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-virtual": "^3.0.2", + "@rollup/plugin-commonjs": "^28.0.3", "@uwdata/flechette": "^2.0.0", + "@uwdata/vgplot": "^0.12.2", "chalk": "^4.1.2", + "rollup": "^4.35.0", "skia-canvas": "^2.0.2", "yargs": "^17.7.2" }, diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index 2bf8de972..cd5f04336 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -2,6 +2,11 @@ import yaml from "yaml"; import fs from 'fs'; import { JSDOM } from 'jsdom'; import path from "path"; +import { rollup } from "rollup"; +import virtual from "@rollup/plugin-virtual"; +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; + // Mosaic modules import { @@ -12,7 +17,7 @@ import { QueryDataNode, } from '@uwdata/mosaic-spec'; import { ExprNode, ColumnNameRefNode } from '@uwdata/mosaic-sql'; -import { MosaicClient } from '../../vgplot/src/index.js'; +import { MosaicClient, isActivatable } from '@uwdata/mosaic-core'; // Utility imports import { @@ -22,7 +27,6 @@ import { LogLevel, Logger, clientsReady, } from './util/index.js'; -import { isActivatable } from "@uwdata/mosaic-core"; import { binary, map, tableFromArrays, tableToIPC, utf8 } from "@uwdata/flechette"; /** @@ -137,7 +141,7 @@ export class MosaicPublisher { await this.exportDataFromDuckDB(tables); } - this.writeFiles(isInteractive, element); + await this.writeFiles(isInteractive, element); } private processClients() { @@ -240,7 +244,7 @@ export class MosaicPublisher { /** * Write out the index.js, index.html, and create data/ directory as needed. */ - private writeFiles(isInteractive: boolean, element?: HTMLElement | SVGElement) { + private async writeFiles(isInteractive: boolean, element?: HTMLElement | SVGElement) { const cache = this.ctx.coordinator.manager.cache().export(); const cacheFile = '.cache.arrow'; if (cache) { @@ -261,7 +265,32 @@ export class MosaicPublisher { fs.writeFileSync(path.join(this.outputPath, 'index.html'), html); if (isInteractive) { - fs.writeFileSync(path.join(this.outputPath, 'index.js'), code); + const bundle = await rollup({ + input: 'entry.js', + plugins: [ // We ts-ignore these because they use cjs default exports + // @ts-ignore + virtual({ 'entry.js': code }), + // @ts-ignore + resolve({ browser: true }), + // @ts-ignore + commonjs({ defaultIsModuleExports: true }), + ], + output: { + manualChunks: { + vgplot: ['@uwdata/vgplot'], + flechette: ['@uwdata/flechette'], + }, + minifyInternalExports: true, + }, + logLevel: 'silent', + treeshake: true, + }); + + await bundle.write({ + dir: this.outputPath, + format: 'esm', + entryFileNames: 'index.js', + }); } } diff --git a/packages/publish/src/util/constants.ts b/packages/publish/src/util/constants.ts index f491b4aee..c7c6f3df1 100644 --- a/packages/publish/src/util/constants.ts +++ b/packages/publish/src/util/constants.ts @@ -161,8 +161,10 @@ export const templateCSS = ``; -export const VGPLOT = 'https://cdn.jsdelivr.net/npm/@uwdata/vgplot@latest/dist/vgplot.js'; -export const FLECHETTE = 'https://cdn.jsdelivr.net/npm/@uwdata/flechette@latest/dist/flechette.js'; +// export const VGPLOT = 'https://cdn.jsdelivr.net/npm/@uwdata/vgplot@latest/dist/vgplot.js'; +// export const FLECHETTE = 'https://cdn.jsdelivr.net/npm/@uwdata/flechette@latest/dist/flechette.js'; +export const VGPLOT = '@uwdata/vgplot'; +export const FLECHETTE = '@uwdata/flechette'; // TODO: switch this to ./renderHelpers version when changes pushed to npm // Currently, this is hack to see when clients are ready use .pending when it is available From c5803948a8d0bde4ba0e53e9c352343a4b8eed16 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 30 Mar 2025 13:31:39 -0700 Subject: [PATCH 23/37] fix: remove dependence on fields method --- package-lock.json | 52 --------------------- packages/publish/package.json | 2 - packages/publish/src/MosaicPublisher.ts | 34 ++------------ packages/publish/src/util/publishHelpers.ts | 28 ++++++++++- packages/publish/src/util/renderHelpers.ts | 2 +- 5 files changed, 33 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42a0945c2..5223cc97a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3028,26 +3028,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@rjsf/utils": { - "version": "5.24.3", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.24.3.tgz", - "integrity": "sha512-IFzMyr6RDRgSi9jdfFBomwc8LMul6zLBsRSAgzD9wAB9laDcYHwPNEL2kZhOkAGrc+4iNQSPsawGFOA6CXH87Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "json-schema-merge-allof": "^0.8.1", - "jsonpointer": "^5.0.1", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "react-is": "^18.2.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.14.0 || >=17" - } - }, "node_modules/@rollup/plugin-commonjs": { "version": "28.0.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", @@ -3824,16 +3804,6 @@ "@types/unist": "*" } }, - "node_modules/@types/jquery": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", - "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/jsdom": { "version": "21.1.7", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", @@ -3923,13 +3893,6 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, - "node_modules/@types/sizzle": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -3937,13 +3900,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/underscore": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz", - "integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -16216,8 +16172,6 @@ "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-virtual": "^3.0.2", - "@uwdata/flechette": "^2.0.0", - "@uwdata/vgplot": "^0.12.2", "chalk": "^4.1.2", "rollup": "^4.35.0", "skia-canvas": "^2.0.2", @@ -16232,12 +16186,6 @@ "typescript": "^5.6.3" } }, - "packages/publish/node_modules/@uwdata/flechette": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@uwdata/flechette/-/flechette-2.0.0.tgz", - "integrity": "sha512-4E5mQLNV2VcrHrDS77w4e3luusYGk/jchJSWgonGGUKJ2eNf1yt1qNCy+wzB7SqZ4WOPaK6j1mtzRFtRPzA8Xg==", - "license": "BSD-3-Clause" - }, "packages/spec": { "name": "@uwdata/mosaic-spec", "version": "0.15.0", diff --git a/packages/publish/package.json b/packages/publish/package.json index 65a3b0609..55d175b92 100644 --- a/packages/publish/package.json +++ b/packages/publish/package.json @@ -16,8 +16,6 @@ "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-virtual": "^3.0.2", "@rollup/plugin-commonjs": "^28.0.3", - "@uwdata/flechette": "^2.0.0", - "@uwdata/vgplot": "^0.12.2", "chalk": "^4.1.2", "rollup": "^4.35.0", "skia-canvas": "^2.0.2", diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index cd5f04336..252aedbe2 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -16,7 +16,6 @@ import { CodegenContext, QueryDataNode, } from '@uwdata/mosaic-spec'; -import { ExprNode, ColumnNameRefNode } from '@uwdata/mosaic-sql'; import { MosaicClient, isActivatable } from '@uwdata/mosaic-core'; // Utility imports @@ -111,7 +110,7 @@ export class MosaicPublisher { document.body.appendChild(element); await clientsReady(this.ctx); - const { interactors, inputs, tables } = this.processClients(); + const { interactors, inputs } = this.processClients(); const isInteractive = interactors.size + inputs.size !== 0; // If spec is valid create relevant output directory @@ -135,6 +134,7 @@ export class MosaicPublisher { const { file } = this; return code?.replace(`"${file}"`, `window.location.origin + "/${file}"`); }; + const tables = this.ctx.coordinator.databaseConnector().tables(); await this.updateDataNodes(tables); // Export relevant data from DuckDB to Parquet @@ -147,34 +147,10 @@ export class MosaicPublisher { private processClients() { const interactors = new Set(); const inputs = new Set(); - const tables: Record> = {}; for (const client of this.ctx.coordinator.clients) { - if (client instanceof MosaicClient) { - const fields = client.fields(); - if (fields) { - for (const field of fields) { - const { table, column } = field; - if (!(table in tables)) tables[table] = new Set(); - if (column instanceof ExprNode) { - if (column instanceof ColumnNameRefNode) { - tables[table].add(column.column); - } - } else { - tables[table].add(column); - } - } - } - - if (isActivatable(client)) { - inputs.add(client); - - const input = client as any; - if (input.from && input.column) { - if (!(input.from in tables)) tables[input.from] = new Set(); - tables[input.from].add(input.column); - } - } + if (client instanceof MosaicClient && isActivatable(client)) { + inputs.add(client); } if (client.plot) { for (const interactor of client.plot.interactors) { @@ -182,7 +158,7 @@ export class MosaicPublisher { } } } - return { interactors, inputs, tables }; + return { interactors, inputs }; } /** diff --git a/packages/publish/src/util/publishHelpers.ts b/packages/publish/src/util/publishHelpers.ts index 3c316fa87..d1f254af5 100644 --- a/packages/publish/src/util/publishHelpers.ts +++ b/packages/publish/src/util/publishHelpers.ts @@ -3,6 +3,8 @@ import { DuckDB } from "@uwdata/mosaic-duckdb"; import { InstantiateContext } from "@uwdata/mosaic-spec"; import { createAPIContext } from "../../../vgplot/src/index.js"; +const SCAN_OP = 'SEQ_SCAN '; + export function publishConnector() { const db = new DuckDB(); db.exec('PRAGMA force_compression=Uncompressed'); @@ -12,8 +14,31 @@ export function publishConnector() { sql: string; } + const tables: Record> = {}; + return { query: async (query: Query) => { + // run an explain query and extract the result + const explain = await db.query(`EXPLAIN (FORMAT json) ${query.sql}`); + const plan = JSON.parse(explain[0].explain_value); + + // Find SEQ_SCAN by recursively looping over the plan + const findSeqScan = (node: any): void => { + if (node.name === SCAN_OP) { + const info = node.extra_info; + if (info.Table && info.Projections) { + const table = info.Table.toLowerCase(); + const columns = typeof info.Projections === 'string' ? [info.Projections] : info.Projections + if (!(table in tables)) tables[table] = new Set(); + columns.forEach((col: string) => tables[table].add(col)); + } + } + if (node.children) { + node.children.some(findSeqScan); + } + }; + plan.some(findSeqScan); + const { type, sql } = query; switch (type) { case 'exec': @@ -23,7 +48,8 @@ export function publishConnector() { default: return db.query(sql); } - } + }, + tables: () => tables, }; } diff --git a/packages/publish/src/util/renderHelpers.ts b/packages/publish/src/util/renderHelpers.ts index b57edad28..ee4e51568 100644 --- a/packages/publish/src/util/renderHelpers.ts +++ b/packages/publish/src/util/renderHelpers.ts @@ -11,7 +11,7 @@ export function mockCanvas(window: any) { const canvas = new Canvas(); const el = og.call(this, tagName) as HTMLCanvasElement; el.getContext = canvas.getContext.bind(canvas) as unknown as HTMLCanvasElement['getContext']; - el.toDataURL = canvas.toDataURLSync.bind(canvas); + el.toDataURL = canvas.toDataURLSync.bind(canvas) as unknown as HTMLCanvasElement['toDataURL']; const set = el.setAttribute; el.setAttribute = function (name: string, value: string) { if (name !== 'width' && name !== 'height') { From 9260640c47ac713fc49a04b484e341cee7a4248f Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 30 Mar 2025 15:57:47 -0700 Subject: [PATCH 24/37] fix: handle undefinied --- packages/publish/src/MosaicPublisher.ts | 1 + packages/publish/src/util/renderHelpers.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index 252aedbe2..be73263ca 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -147,6 +147,7 @@ export class MosaicPublisher { private processClients() { const interactors = new Set(); const inputs = new Set(); + if (!this.ctx.coordinator.clients) return { interactors, inputs }; for (const client of this.ctx.coordinator.clients) { if (client instanceof MosaicClient && isActivatable(client)) { diff --git a/packages/publish/src/util/renderHelpers.ts b/packages/publish/src/util/renderHelpers.ts index ee4e51568..98de45d68 100644 --- a/packages/publish/src/util/renderHelpers.ts +++ b/packages/publish/src/util/renderHelpers.ts @@ -26,6 +26,8 @@ export function mockCanvas(window: any) { } export function clientsReady(ctx: InstantiateContext) { + if (!ctx.coordinator.clients) return Promise.resolve([]); + const clients = [...ctx.coordinator.clients] as MosaicClient[]; return Promise.allSettled(clients.map(c => c.pending)); } \ No newline at end of file From 218e8e8179eda2baf295307377097ea2c78e856d Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Sun, 30 Mar 2025 16:02:00 -0700 Subject: [PATCH 25/37] fix: cache exporting --- packages/core/src/util/cache.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/util/cache.js b/packages/core/src/util/cache.js index 7aee47222..10bac567e 100644 --- a/packages/core/src/util/cache.js +++ b/packages/core/src/util/cache.js @@ -87,7 +87,7 @@ export function lruCache({ set, clear() { cache = new Map; }, export() { - return new Map(Array.from(cache).map(([key, entry]) => [key, tableToIPC(entry.value)])); + return new Map(Array.from(cache).map(([key, entry]) => [key, tableToIPC(entry.value, {})])); }, import(data) { for (const [key, entry] of data) { From 58bf43c02e8e28dcc0aa37ca82aed408c35e6273 Mon Sep 17 00:00:00 2001 From: Ron Pechuk Date: Thu, 24 Apr 2025 20:45:55 -0700 Subject: [PATCH 26/37] feat: add more fine-grain control over optimizations --- packages/publish/src/MosaicPublisher.ts | 76 ++++++++++++--------- packages/publish/src/cli.ts | 4 +- packages/publish/src/util/Logger.ts | 6 ++ packages/publish/src/util/constants.ts | 53 ++++++++++---- packages/publish/src/util/publishHelpers.ts | 15 ++-- 5 files changed, 99 insertions(+), 55 deletions(-) diff --git a/packages/publish/src/MosaicPublisher.ts b/packages/publish/src/MosaicPublisher.ts index be73263ca..bca0e5ab3 100644 --- a/packages/publish/src/MosaicPublisher.ts +++ b/packages/publish/src/MosaicPublisher.ts @@ -20,11 +20,12 @@ import { MosaicClient, isActivatable } from '@uwdata/mosaic-core'; // Utility imports import { - preamble, htmlTemplate, templateCSS, + preamble, PreambleOptions, htmlTemplate, templateCSS, publishConnector, PublishContext, mockCanvas, VGPLOT, FLECHETTE, LogLevel, Logger, clientsReady, + OPTIMIZATION_LEVEL_TO_OPTIMIZATIONS, Optimizations, } from './util/index.js'; import { binary, map, tableFromArrays, tableToIPC, utf8 } from "@uwdata/flechette"; @@ -53,7 +54,7 @@ export class MosaicPublisher { private spec: string; private outputPath: string; private title: string; - private optimize: 'minimal' | 'more' | 'most'; + private optimizations: Optimizations[]; private logger: Logger; // Internal references used throughout @@ -65,13 +66,13 @@ export class MosaicPublisher { spec: string, outputPath = 'out', title = 'Mosaic Visualization', - optimize: 'minimal' | 'more' | 'most' = 'minimal', + optimize: 'none' | 'minimal' | 'more' | 'most' = 'minimal', logger: Logger = new Logger(LogLevel.INFO) ) { this.spec = spec; this.outputPath = outputPath; this.title = title; - this.optimize = optimize; + this.optimizations = OPTIMIZATION_LEVEL_TO_OPTIMIZATIONS[optimize]; this.logger = logger; // Create PublishContext @@ -141,7 +142,10 @@ export class MosaicPublisher { await this.exportDataFromDuckDB(tables); } - await this.writeFiles(isInteractive, element); + await this.writeFiles( + isInteractive, + this.optimizations.includes(Optimizations.PRERENDER) ? element : undefined + ); } private processClients() { @@ -167,7 +171,7 @@ export class MosaicPublisher { * for queries to finish. */ private async activateInteractorsAndInputs(interactors: Set, inputs: Set) { - this.ctx.coordinator.manager._logQueries = true; + await this.ctx.coordinator.exec('CREATE SCHEMA IF NOT EXISTS mosaic'); for (const interactor of interactors) { if (isActivatable(interactor)) interactor.activate(); await this.waitForQueryToFinish(); @@ -203,7 +207,8 @@ export class MosaicPublisher { // process tables from DuckDB const db_tables = await this.ctx.coordinator.query('SHOW ALL TABLES', { cache: false, type: 'json' }); - if (db_tables.some((table: any) => table.name.startsWith('preagg_'))) { + if (this.optimizations.includes(Optimizations.PREAGREGATE) + && db_tables.some((table: any) => table.name.startsWith('preagg_'))) { this.ast!.data['schema'] = new SchemaCreateNode('schema', 'mosaic'); for (const table of db_tables) { if (table.name.startsWith('preagg_')) { @@ -222,23 +227,36 @@ export class MosaicPublisher { * Write out the index.js, index.html, and create data/ directory as needed. */ private async writeFiles(isInteractive: boolean, element?: HTMLElement | SVGElement) { - const cache = this.ctx.coordinator.manager.cache().export(); - const cacheFile = '.cache.arrow'; - if (cache) { - const cacheBytes = tableToIPC(tableFromArrays({ cache: [cache] }, { - types: { - cache: map(utf8(), binary()) - } - }), {})!; - fs.writeFileSync(path.join(this.outputPath, cacheFile), cacheBytes); + let preambleOptions: PreambleOptions = { + needsClientReady: this.optimizations.includes(Optimizations.PRERENDER), + cacheFile: undefined, + } + if (this.optimizations.includes(Optimizations.LOAD_CACHE)) { + const cache = this.ctx.coordinator.manager.cache().export(); + const cacheFile = '.cache.arrow'; + if (cache) { + const cacheBytes = tableToIPC(tableFromArrays({ cache: [cache] }, { + types: { + cache: map(utf8(), binary()) + } + }), {})!; + fs.writeFileSync(path.join(this.outputPath, cacheFile), cacheBytes); + preambleOptions.cacheFile = cacheFile; + } } const code = astToESM(this.ast!, { connector: 'wasm', imports: new Map([[VGPLOT, '* as vg'], [FLECHETTE, '{ tableFromIPC }']]), - preamble: preamble(this.optimize == 'most', cache ? cacheFile : undefined), + preamble: preamble(preambleOptions), + }); + const html = htmlTemplate({ + title: this.title, + css: templateCSS, + isInteractive, + needsClientReady: this.optimizations.includes(Optimizations.PRERENDER), + element }); - const html = htmlTemplate(isInteractive, this.title, element, templateCSS); fs.writeFileSync(path.join(this.outputPath, 'index.html'), html); if (isInteractive) { @@ -282,28 +300,18 @@ export class MosaicPublisher { const table = node.name; const file = node.file; const relevant_columns = tables[table]; - if (relevant_columns?.size === 0) { + if (relevant_columns?.size === 0 && this.optimizations.includes(Optimizations.DATASHAKE)) { this.logger.warn(`Skipping export of table: ${table}`); this.logger.warn(`No columns are being used from this table.`); continue; } let query: string; - switch (this.optimize) { - case 'minimal': - query = `COPY (SELECT * FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; - break; - case 'more': - case 'most': - if (relevant_columns === null) { - query = `COPY (SELECT * FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; - } else { - this.logger.warn(`Partially exporting table: ${table}`); - query = `COPY (SELECT ${Array.from(relevant_columns).join(', ')} FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; - } - break; - default: - throw new PublishError(`Invalid optimization level: ${this.optimize}`); + if (this.optimizations.includes(Optimizations.PROJECTION) && relevant_columns) { + this.logger.warn(`Partially exporting table: ${table}`); + query = `COPY (SELECT ${Array.from(relevant_columns).join(', ')} FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; + } else { + query = `COPY (SELECT * FROM ${table}) TO '${this.outputPath}/${file}' (FORMAT PARQUET)`; } if (table.startsWith('mosaic.preagg_')) { diff --git a/packages/publish/src/cli.ts b/packages/publish/src/cli.ts index 054397fee..8c5811a51 100755 --- a/packages/publish/src/cli.ts +++ b/packages/publish/src/cli.ts @@ -22,7 +22,7 @@ const parsed = yargs(hideBin(process.argv)) alias: 'o', type: 'string', describe: 'Level of optimizations', - choices: ['minimal', 'more', 'most'] as const, + choices: ['none', 'minimal', 'more', 'most'] as const, default: 'minimal', }, output: { @@ -50,7 +50,7 @@ const parsed = yargs(hideBin(process.argv)) const argv = { spec: parsed.spec as string, - optimize: parsed.optimize as 'minimal' | 'more' | 'most', + optimize: parsed.optimize as 'none' | 'minimal' | 'more' | 'most', output: parsed.output as string | undefined, title: parsed.title as string | undefined, }; diff --git a/packages/publish/src/util/Logger.ts b/packages/publish/src/util/Logger.ts index 3ea9b573d..d54d55506 100644 --- a/packages/publish/src/util/Logger.ts +++ b/packages/publish/src/util/Logger.ts @@ -26,6 +26,12 @@ export class Logger { logFunction(str.split('\n').map(line => prefix + line).join('\n')); } + debug(...args: unknown[]) { + if (this.level <= LogLevel.INFO) { + this.log(console.debug, chalk.cyanBright('[DEBUG] '), ...args); + } + } + info(...args: unknown[]) { if (this.level <= LogLevel.INFO) { this.log(console.log, chalk.blueBright('[INFO] '), ...args); diff --git a/packages/publish/src/util/constants.ts b/packages/publish/src/util/constants.ts index c7c6f3df1..1fa36faad 100644 --- a/packages/publish/src/util/constants.ts +++ b/packages/publish/src/util/constants.ts @@ -1,24 +1,35 @@ -export const htmlTemplate = (isInteractive: boolean, title: string, element?: HTMLElement | SVGElement, css = templateCSS) => ` +export type HTMLTemplateOptions = { + title: string; + isInteractive: boolean; + needsClientReady: boolean; + element?: HTMLElement | SVGElement; + css?: string; +}; +export const htmlTemplate = (options: HTMLTemplateOptions) => ` - ${title} + ${options.title}
- ${isInteractive ? '
' : ''} - ${element?.outerHTML ?? ''} + ${options.isInteractive ? '
' : ''} + ${options.element?.outerHTML ?? ''}
-${isInteractive ? ` +${options.isInteractive ? ` ` : ''} -${css} +${options.css} `; export const templateCSS = `