Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 100 additions & 16 deletions docs/md/how_to/javascript/importing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,12 @@ The exact syntax will vary slightly depending on the bundler.

### Memory64 (wasm64)

`@perspective-dev/server` also ships a WebAssembly Memory64 build of the
engine, `dist/wasm/perspective-server.memory64.wasm`, which raises the
engine's heap ceiling from 4GB to 16GB (at some engine performance cost).
`init_server` accepts both binaries at once — register each as a _thunk_ and
only the selected binary is ever downloaded. The wasm64 binary is used
whenever the browser supports Memory64; registering only the wasm32 binary
(as above) opts out.
`@perspective-dev/server` also ships a WebAssembly Memory64 build of the engine,
`dist/wasm/perspective-server.memory64.wasm`, which raises the engine's heap
ceiling from 4GB to 16GB (at some engine performance cost). `init_server`
accepts both binaries at once — register each as a _thunk_ and only the selected
binary is ever downloaded. The wasm64 binary is used whenever the browser
supports Memory64; registering only the wasm32 binary (as above) opts out.

```javascript
perspective.init_server({
Expand Down Expand Up @@ -138,18 +137,103 @@ Webpack config:

## Inline builds with a bundler

<span class="warning">Inline builds are deprecated and will be removed in a
future release.</span>
<span class="warning">Perspective no longer publishes prebuilt _inline_ bundles.
The `@perspective-dev/client/inline` and `@perspective-dev/viewer/inline` entry
points, and the `dist/esm/*.inline.js` files behind them, have been removed.
Produce an equivalent build with your bundler's asset inlining, as below.</span>

Perspective's _Inline_ Builds work by _inlining_ WebAssembly binary content as
a base64-encoded string. While inline builds work with most bundlers and _do
not_ require bootstrapping, there is an inherent file-size and boot-performance
penalty. Prefer your bundler's inlining features and Perspective ESM builds
where possible.
An _inline_ build embeds the WebAssembly binaries in the `.js` bundle as base64
instead of emitting them as separate assets, so nothing needs to be served
alongside the bundle. It costs bundle size and boot time — base64 is 33% larger
than the binary it encodes, and the browser must decode it before compiling — so
prefer the ESM builds above where you control asset hosting.

Only the bundler's `.wasm` loader changes; the bootstrapping code is identical
to the ESM builds.

### ESBuild

Use the `binary` loader in place of `file`. The imported value is a
`Uint8Array`, which `init_server()` and `init_client()` accept directly.

```javascript
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";

await Promise.all([
perspective.init_server(SERVER_WASM),
perspective_viewer.init_client(CLIENT_WASM),
]);
```

ESBuild config JSON to encode this asset as `binary`:

```javascript
{
// ...
"loader": {
// ...
".wasm": "binary"
}
}
```

### Webpack

Use `asset/inline` in place of `asset/resource`. The imported value is a `data:`
URL rather than a path, so read it into an `ArrayBuffer` first.

```javascript
import "@perspective-dev/viewer/dist/esm/perspective-viewer.inline.js";
import psp from "@perspective-dev/client/dist/esm/perspective.inline.js";
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";

const buffer = async (url) => await (await fetch(url)).arrayBuffer();

await Promise.all([
perspective.init_server(await buffer(SERVER_WASM)),
perspective_viewer.init_client(await buffer(CLIENT_WASM)),
]);
```

Webpack config:

```javascript
{
// ...
module: {
// ...
rules: [
// ...
{
test: /\.wasm$/,
type: "asset/inline"
},
]
},
experiments: {
// ...
asyncWebAssembly: false,
syncWebAssembly: false,
},
}
```

### Vite

Vite inlines assets below `build.assetsInlineLimit` as `data:` URLs. Raising the
limit past the size of the `.wasm` binaries inlines them, and the `?url`
bootstrapping from the ESM section applies unchanged. This threshold is a
build-wide setting and its interaction with `?url` imports has changed between
Vite major versions, so check the emitted bundle rather than assuming.

```javascript
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: "esnext",
assetsInlineLimit: 16 * 1024 * 1024,
},
});
```

## CDN builds
Expand Down
36 changes: 3 additions & 33 deletions packages/viewer-charts/src/ts/charts/cartesian/glyphs/lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from "../../../webgl/instanced-attrs";
import { compileProgram } from "../../../webgl/program-cache";
import { colorRangePivot } from "../../../theme/gradient";
import { formatTickValue, formatDateTickValue } from "../../../layout/ticks";
import { buildPointRowTooltipLines } from "../tooltip-lines";
import lineVert from "../../../shaders/line.vert.glsl";
import lineFrag from "../../../shaders/line.frag.glsl";

Expand Down Expand Up @@ -122,41 +122,11 @@ export class LineGlyph implements Glyph {

// helpers

async buildTooltipLines(
buildTooltipLines(
chart: CartesianChart,
flatIdx: number,
): Promise<string[][]> {
const grid: string[][] = [];
if (!chart._xData || !chart._yData) {
return grid;
}

if (chart._splitGroups.length > 0 && chart._seriesCapacity > 0) {
const seriesIdx = Math.floor(flatIdx / chart._seriesCapacity);
const sg = chart._splitGroups[seriesIdx];
if (sg) {
grid.push([sg.prefix]);
}
}

const xVal = chart._xData[flatIdx];
const yVal = chart._yData[flatIdx];

const xType = chart._columnTypes[chart._xLabel] || "";
const xIsDate = xType === "date" || xType === "datetime";
const xFormatted = xIsDate
? formatDateTickValue(xVal)
: formatTickValue(xVal);
grid.push([chart._xLabel || "Row", xFormatted]);

const yType = chart._columnTypes[chart._yLabel] || "";
const yIsDate = yType === "date" || yType === "datetime";
const yFormatted = yIsDate
? formatDateTickValue(yVal)
: formatTickValue(yVal);
grid.push([chart._yLabel, yFormatted]);

return grid;
return buildPointRowTooltipLines(chart, flatIdx);
}

tooltipOptions() {
Expand Down
2 changes: 1 addition & 1 deletion packages/viewer-charts/src/ts/charts/chart-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
sourceColumn,
type NumberFormatConfig,
type DateFormatConfig,
} from "@perspective-dev/viewer/src/ts/column-format.js";
} from "@perspective-dev/viewer/column-format";
import type { ColumnDataMap } from "../data/view-reader";
import { LazyRowFetcher } from "../data/lazy-row";
import {
Expand Down
2 changes: 1 addition & 1 deletion packages/viewer-charts/src/ts/event-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ export interface PerspectiveClickDetail {
panel?: string;
}

export { PerspectiveSelectDetail } from "@perspective-dev/viewer/src/ts/extensions.js";
export { PerspectiveSelectDetail } from "@perspective-dev/viewer/select-detail";
2 changes: 1 addition & 1 deletion packages/viewer-charts/src/ts/plugin/format-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import type {
DateFormatConfig,
NumberFormatConfig,
} from "@perspective-dev/viewer/src/ts/column-format.js";
} from "@perspective-dev/viewer/column-format";

/**
* The chart plugin's default per-column formats, declared to the viewer in
Expand Down
88 changes: 88 additions & 0 deletions packages/viewer-charts/test/ts/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,3 +764,91 @@ export function assertViewerQuiescent(
}
}
}

export async function readPinnedTooltip(page: Page): Promise<string[] | null> {
return await page.evaluate(() => {
const host = document.querySelector(
'perspective-viewer > [slot]:not([slot^="tab-"])',
);

const tip = host?.shadowRoot?.querySelector(".webgl-tooltip");
return tip
? [...tip.children].map((c) => (c.textContent ?? "").trim())
: null;
});
}

export function tooltipValue(
cells: string[],
label: string,
): string | undefined {
const i = cells.indexOf(label);
return i >= 0 ? cells[i + 1] : undefined;
}

export async function sweepPinnedTooltips(
page: Page,
step: { x: number; y: number } = { x: 0.02, y: 0.05 },
): Promise<string[][]> {
const box = await page.evaluate(() => {
const host = document.querySelector(
'perspective-viewer > [slot]:not([slot^="tab-"])',
);

if (!host) {
return null;
}

const r = host.getBoundingClientRect();
return { x: r.x, y: r.y, w: r.width, h: r.height };
});

if (!box) {
return [];
}

const seen = new Map<string, string[]>();
for (let fx = 0.05; fx <= 0.96; fx += step.x) {
for (let fy = 0.05; fy <= 0.96; fy += step.y) {
await page.mouse.move(box.x + box.w * fx, box.y + box.h * fy);
await page.mouse.click(box.x + box.w * fx, box.y + box.h * fy);
const cells = await readPinnedTooltip(page);
if (cells && cells.length > 0) {
seen.set(JSON.stringify(cells), cells);
}
}
}

return [...seen.values()];
}

export async function viewYearRange(
page: Page,
suffix: string,
): Promise<[number, number]> {
return await page.evaluate(async (s) => {
const viewer = document.querySelector("perspective-viewer")!;
const view = await (viewer as any).getView({ mode: "clone" });
try {
const columns = await view.to_columns();
const years: number[] = [];
for (const [name, values] of Object.entries(columns)) {
if (!name.endsWith(s)) {
continue;
}

for (const value of values as (number | null)[]) {
if (value === null || value === undefined) {
continue;
}

years.push(new Date(Number(value)).getFullYear());
}
}

return [Math.min(...years), Math.max(...years)] as [number, number];
} finally {
await view.delete();
}
}, suffix);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,56 @@
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import { test } from "../helpers.ts";
import { run_standard_tests } from "../helpers/standard_tests.ts";
import type { ViewerConfigUpdate } from "@perspective-dev/viewer";
import { expect, test } from "@perspective-dev/test";
import {
gotoBasic,
restoreChart,
sweepPinnedTooltips,
tooltipValue,
viewYearRange,
} from "./helpers";

async function get_contents(page) {
return await page.evaluate(async () => {
const viewer = document.querySelector(
"perspective-viewer perspective-viewer-plugin",
);
const DATETIME_X: ViewerConfigUpdate = {
columns: ["Order Date", "Sales"],
group_by: [],
split_by: ["Region"],
sort: [["Order Date", "asc"]],
} as ViewerConfigUpdate;

// Don't format - light DOM is CSV in a <pre> tag.
return viewer.innerHTML;
const YEAR = /\b(\d{4})\b/;

test.describe("Tooltip source values", () => {
test.beforeEach(async ({ page }) => {
await gotoBasic(page);
});
}

test.describe("Superstore Inline", () => {
test.beforeEach(async function init({ page }) {
await page.goto(
"/node_modules/@perspective-dev/viewer/test/html/superstore-inline.html",
);
for (const plugin of ["X/Y Scatter", "X/Y Line"]) {
test(`${plugin} pins dates and values from the source row`, async ({
page,
}) => {
await restoreChart(page, {
...DATETIME_X,
plugin,
} as ViewerConfigUpdate);

await page.evaluate(async () => {
while (!window["__TEST_PERSPECTIVE_READY__"]) {
await new Promise((x) => setTimeout(x, 10));
}
});
const [minYear, maxYear] = await viewYearRange(page, "Order Date");
expect(minYear).toBeGreaterThan(1970);

await page.evaluate(async () => {
await document.querySelector("perspective-viewer").restore({
plugin: "Debug",
});
});
});
const tooltips = await sweepPinnedTooltips(page);
expect(tooltips.length).toBeGreaterThan(4);

for (const cells of tooltips) {
const date = tooltipValue(cells, "Order Date");
const sales = tooltipValue(cells, "Sales");
expect(date).toBeDefined();
expect(sales).toBeDefined();

run_standard_tests("superstore inline", get_contents);
const year = Number(YEAR.exec(date!)?.[1]);
expect(year).toBeGreaterThanOrEqual(minYear - 1);
expect(year).toBeLessThanOrEqual(maxYear + 1);
expect(sales!.startsWith("-")).toBe(false);
}
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import column_config_schema, {
import plugin_config_schema from "../plugin/plugin_config_schema.js";
import datagridStyles from "../../../dist/css/perspective-viewer-datagrid.css";
import { format_raw } from "../data_listener/format_cell.js";
import { sourceColumn } from "@perspective-dev/viewer/src/ts/column-format.js";
import { sourceColumn } from "@perspective-dev/viewer/column-format";

import type { View, ViewWindow } from "@perspective-dev/client";
import type {
Expand Down
Loading
Loading