{"schemas":{"ViewerConfigUpdate":{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/ViewerConfigUpdate","definitions":{"ViewerConfigUpdate":{"type":"object","properties":{"version":{"type":["null","string"],"description":"The `@perspective-dev/viewer` version a saved config was written by, used to migrate older tokens. Omit it — the viewer stamps the current version on save."},"plugin":{"type":["null","string"],"description":"Name of the visualization plugin to switch to, from the set registered on the page (the `list_plugins` agent tool, or the plugin picker). Changing it changes what the view fields MEAN: `columns` is positional and every plugin reads the positions differently, and `group_by`/`split_by` draw different things per plugin — so read the new plugin's roles before writing `columns` for it."},"title":{"type":["null","string"],"description":"Panel title, shown in its tab. `null` restores the default title; omitting the field leaves the current one."},"table":{"type":["null","string"],"description":"Name of the `Table` to render, as hosted on this panel's `Client`. Rebinding an existing panel to another table keeps the rest of the config, so column names that do not exist in the new table will fail validation."},"theme":{"type":["null","string"],"description":"Theme NAME (e.g. `\"Pro Dark\"`) — not a CSS value. Valid names are the Perspective themes loaded on the page, re-scanned by `resetThemes()`. `null` selects the default."},"settings":{"type":["null","boolean"],"description":"Whether the settings sidebar is OPEN. Purely cosmetic chrome — it does not affect what the viewer renders, and it is element-level rather than per-panel."},"plugin_config":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"}}],"description":"Plugin-wide settings (as opposed to the per-column [`Self::columns_config`]). The viewer passes these through opaquely — their valid keys are defined by the ACTIVE plugin and vary by plugin and by state, so query them with the `get_style_schema` agent tool rather than guessing."},"columns_config":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"}}}],"description":"Per-column styling — formatting, colors, and other per-column controls — keyed by column name. Opaque to the viewer and plugin-defined like [`Self::plugin_config`]; `get_style_schema` reports the valid keys for a given column under the active plugin."},"group_by":{"type":"array","items":{"type":"string"},"description":"A group by _groups_ the dataset by the unique values of each column used as a group by - a close analogue in SQL to the `GROUP BY` statement. The underlying dataset is aggregated to show the values belonging to each group, and a total row is calculated for each group, showing the currently selected aggregated value (e.g. `sum`) of the column. Group by are useful for hierarchies, categorizing data and attributing values, i.e. showing the number of units sold based on State and City. In Perspective, group by are represented as an array of string column names to pivot, are applied in the order provided; For example, a group by of `[\"State\", \"City\", \"Postal Code\"]` shows the values for each Postal Code, which are grouped by City, which are in turn grouped by State."},"split_by":{"type":"array","items":{"type":"string"},"description":"A split by _splits_ the dataset by the unique values of each column used as a split by. The underlying dataset is not aggregated, and a new column is created for each unique value of the split by. Each newly created column contains the parts of the dataset that correspond to the column header, i.e. a `View` that has `[\"State\"]` as its split by will have a new column for each state. In Perspective, Split By are represented as an array of string column names to pivot."},"columns":{"type":"array","items":{"type":["string","null"]},"description":"The `columns` property specifies which columns should be included in the [`crate::View`]'s output. This allows users to show or hide a specific subset of columns, as well as control the order in which columns appear to the user. This is represented in Perspective as an array of string column names."},"filter":{"type":"array","items":{"$ref":"#/definitions/Filter"},"description":"The `filter` property specifies columns on which the query can be filtered, returning rows that pass the specified filter condition. This is analogous to the `WHERE` clause in SQL. There is no limit on the number of columns where `filter` is applied, but the resulting dataset is one that passes all the filter conditions, i.e. the filters are joined with an `AND` condition.\n\nPerspective represents `filter` as an array of arrays, with the values of each inner array being a string column name, a string filter operator, and a filter operand in the type of the column."},"sort":{"type":"array","items":{"$ref":"#/definitions/Sort"},"description":"The `sort` property specifies columns on which the query should be sorted, analogous to `ORDER BY` in SQL. A column can be sorted regardless of its data type, and sorts can be applied in ascending or descending order. Perspective represents `sort` as an array of arrays, with the values of each inner array being a string column name and a string sort direction. When `column-pivots` are applied, the additional sort directions `\"col asc\"` and `\"col desc\"` will determine the order of pivot columns groups.\n\n`sort` is the ONLY thing that orders a `View`'s rows — without it they keep the `Table`'s natural (insertion) order, which any consumer reading rows sequentially will reflect. Not to be confused with a window column's `order_by`, which orders rows WITHIN a window frame and does not reorder the `View`."},"expressions":{"$ref":"#/definitions/Expressions","description":"The `expressions` property specifies _new_ columns in Perspective that are created using existing column values or arbitary scalar values defined within the expression. In `<perspective-viewer>`, expressions are added using the \"New Column\" button in the side panel."},"windows":{"$ref":"#/definitions/Windows","description":"The `windows` property declares ordered, partitioned rolling computations (moving aggregates, cumulative sums) as _new_ columns keyed by output alias (`{\"name\": {...spec}}`, symmetric with `expressions`), analogous to SQL window functions. See [`crate::config::WindowSpec`]."},"aggregates":{"type":"object","additionalProperties":{"$ref":"#/definitions/Aggregate"},"description":"Aggregates perform a calculation over an entire column, and are displayed when one or more [Group By](#group-by) are applied to the `View`. Aggregates can be specified by the user, or Perspective will use the following sensible default aggregates based on column type:\n\n- \"sum\" for `integer` and `float` columns\n- \"count\" for all other columns\n\nPerspective provides a selection of aggregate functions that can be applied to columns in the `View` constructor using a dictionary of column name to aggregate function name.\n\nAn aggregate also determines the column's RESULT TYPE, which need not match the input: `\"count\"` yields an `integer` whatever it counts, so a `date` column left on the default `\"count\"` is an `integer` in the resulting `View` — no longer a date. Set an aggregate that preserves the type (e.g. `\"any\"`, `\"last\"`) when the original type matters, such as a date used as a chart axis."},"group_by_depth":{"type":"number"},"filter_op":{"$ref":"#/definitions/FilterReducer"},"group_rollup_mode":{"$ref":"#/definitions/GroupRollupMode"},"split_rollup_mode":{"$ref":"#/definitions/SplitRollupMode"}},"additionalProperties":false},"JsonValue":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"boolean"},{"type":"array","items":{"$ref":"#/definitions/JsonValue"}},{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"}},{"type":"null"}]},"Filter":{"type":"array","minItems":3,"items":[{"type":"string"},{"type":"string"},{"$ref":"#/definitions/FilterTerm"}],"maxItems":3},"FilterTerm":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/Scalar"}},{"$ref":"#/definitions/Scalar"}]},"Scalar":{"type":["number","string","boolean","null"],"description":"This type represents the ViewConfig serializable type, which must be JSON safe."},"Sort":{"type":"array","minItems":2,"items":[{"type":"string"},{"$ref":"#/definitions/SortDir"}],"maxItems":2},"SortDir":{"type":"string","enum":["none","desc","asc","col desc","col asc","desc abs","asc abs","col desc abs","col asc abs"]},"Expressions":{"type":"object","additionalProperties":{"type":"string"}},"Windows":{"type":"object","additionalProperties":{"$ref":"#/definitions/WindowSpec"},"description":"The window columns of a `ViewConfig`, keyed by output column alias - symmetric with `expressions` (`{\"name\": {...spec}}`). An alias must not collide with a `Table` column, expression alias, or another window's key."},"WindowSpec":{"type":"object","properties":{"column":{"type":"string","description":"The input column - either a real `Table` column or an expression alias from the same `ViewConfig`."},"aggregate":{"type":"string"},"partition_by":{"type":"array","items":{"type":"string"},"description":"Columns whose distinct value tuples partition the rows; empty partitions the whole `Table` as one group."},"order_by":{"anyOf":[{"$ref":"#/definitions/WindowSort"},{"type":"null"}],"description":"The `Table` column which orders each partition, and the direction. This orders rows WITHIN the window frame only — it does not reorder the `View`, which is what the view-level `sort` does."},"rows":{"type":["number","null"],"description":"A frame of the `rows` preceding each row, plus the row itself. Mutually exclusive with `range` and `cumulative`."},"range":{"type":["number","null"],"description":"A frame of the rows whose `order_by` value lies within `range` of each row's, requiring a numeric or temporal `order_by`. Mutually exclusive with `rows` and `cumulative`."},"cumulative":{"type":["boolean","null"],"description":"`true` frames all rows from the partition start through each row. Default can be omitted."},"offset":{"type":["number","null"],"description":"Row offset for `lag`/`lead` (default 1)."},"alpha":{"type":["number","null"],"description":"Smoothing factor in `(0, 1]` for `ema`."}},"required":["column","aggregate"],"additionalProperties":false,"description":"The serialized form of [`WindowSpec`]."},"WindowSort":{"type":"array","minItems":2,"items":[{"type":"string"},{"$ref":"#/definitions/WindowSortDir"}],"maxItems":2,"description":"The `Table` column which orders each partition, with its direction - serialized as a two-element array (`[\"ts\", \"desc\"]`), symmetric with the `ViewConfig`'s `sort` field."},"WindowSortDir":{"type":"string","enum":["asc","desc"],"description":"A window's order direction. A dedicated two-variant enum rather than [`crate::config::SortDir`] - the column-sort extras (`col asc`, `abs`, `none`) are meaningless inside a window frame and unrepresentable here."},"Aggregate":{"anyOf":[{"type":"string"},{"type":"array","minItems":2,"items":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"maxItems":2}]},"FilterReducer":{"type":"string","enum":["and","or"]},"GroupRollupMode":{"type":"string","enum":["rollup","flat","total"]},"SplitRollupMode":{"type":"string","enum":["flat","rollup"],"description":"The `split_by` corollary to [`GroupRollupMode`]. `Flat` (the default, matching this crate's historical behavior) emits only full-depth split combinations as columns; `Rollup` additionally emits grand-total and subtotal column groups in \"totals before\" order. There is no `Total` variant - an empty `split_by` already expresses a single grand-total column group."}}},"ViewerConfigInitial":{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/ViewerConfigInitial","definitions":{"ViewerConfigInitial":{"type":"object","properties":{"table":{"type":"string","description":"Name of the `Table` the new panel renders, as hosted on the `Client`. REQUIRED: a placed panel with no table binding would be permanently blank."},"version":{"type":"string","description":"The `@perspective-dev/viewer` version a saved config was written by. Omit it when creating a panel."},"plugin":{"type":"string","description":"Name of the visualization plugin, from the set registered on the page (the `list_plugins` agent tool, or the plugin picker). Decides what the view fields MEAN: `columns` is positional and every plugin reads the positions differently, and `group_by`/`split_by` draw different things per plugin. Absent uses the default plugin."},"title":{"type":"string","description":"Panel title, shown in its tab. Absent renders the default title."},"theme":{"type":"string","description":"Theme NAME (e.g. `\"Pro Dark\"`) — not a CSS value. Valid names are the Perspective themes loaded on the page. Absent uses the default."},"plugin_config":{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"},"description":"Plugin-wide settings (as opposed to the per-column [`Self::columns_config`]). Opaque to the viewer and defined by the ACTIVE plugin; query the valid keys with `get_style_schema` rather than guessing."},"columns_config":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"}},"description":"Per-column styling — formatting, colors, and other per-column controls — keyed by column name. Plugin-defined like [`Self::plugin_config`]; see `get_style_schema`."},"group_by":{"type":"array","items":{"type":"string"},"description":"A group by _groups_ the dataset by the unique values of each column used as a group by - a close analogue in SQL to the `GROUP BY` statement. The underlying dataset is aggregated to show the values belonging to each group, and a total row is calculated for each group, showing the currently selected aggregated value (e.g. `sum`) of the column. Group by are useful for hierarchies, categorizing data and attributing values, i.e. showing the number of units sold based on State and City. In Perspective, group by are represented as an array of string column names to pivot, are applied in the order provided; For example, a group by of `[\"State\", \"City\", \"Postal Code\"]` shows the values for each Postal Code, which are grouped by City, which are in turn grouped by State."},"split_by":{"type":"array","items":{"type":"string"},"description":"A split by _splits_ the dataset by the unique values of each column used as a split by. The underlying dataset is not aggregated, and a new column is created for each unique value of the split by. Each newly created column contains the parts of the dataset that correspond to the column header, i.e. a `View` that has `[\"State\"]` as its split by will have a new column for each state. In Perspective, Split By are represented as an array of string column names to pivot."},"columns":{"type":"array","items":{"type":["string","null"]},"description":"The `columns` property specifies which columns should be included in the [`crate::View`]'s output. This allows users to show or hide a specific subset of columns, as well as control the order in which columns appear to the user. This is represented in Perspective as an array of string column names."},"filter":{"type":"array","items":{"$ref":"#/definitions/Filter"},"description":"The `filter` property specifies columns on which the query can be filtered, returning rows that pass the specified filter condition. This is analogous to the `WHERE` clause in SQL. There is no limit on the number of columns where `filter` is applied, but the resulting dataset is one that passes all the filter conditions, i.e. the filters are joined with an `AND` condition.\n\nPerspective represents `filter` as an array of arrays, with the values of each inner array being a string column name, a string filter operator, and a filter operand in the type of the column."},"sort":{"type":"array","items":{"$ref":"#/definitions/Sort"},"description":"The `sort` property specifies columns on which the query should be sorted, analogous to `ORDER BY` in SQL. A column can be sorted regardless of its data type, and sorts can be applied in ascending or descending order. Perspective represents `sort` as an array of arrays, with the values of each inner array being a string column name and a string sort direction. When `column-pivots` are applied, the additional sort directions `\"col asc\"` and `\"col desc\"` will determine the order of pivot columns groups.\n\n`sort` is the ONLY thing that orders a `View`'s rows — without it they keep the `Table`'s natural (insertion) order, which any consumer reading rows sequentially will reflect. Not to be confused with a window column's `order_by`, which orders rows WITHIN a window frame and does not reorder the `View`."},"expressions":{"$ref":"#/definitions/Expressions","description":"The `expressions` property specifies _new_ columns in Perspective that are created using existing column values or arbitary scalar values defined within the expression. In `<perspective-viewer>`, expressions are added using the \"New Column\" button in the side panel."},"windows":{"$ref":"#/definitions/Windows","description":"The `windows` property declares ordered, partitioned rolling computations (moving aggregates, cumulative sums) as _new_ columns keyed by output alias (`{\"name\": {...spec}}`, symmetric with `expressions`), analogous to SQL window functions. See [`crate::config::WindowSpec`]."},"aggregates":{"type":"object","additionalProperties":{"$ref":"#/definitions/Aggregate"},"description":"Aggregates perform a calculation over an entire column, and are displayed when one or more [Group By](#group-by) are applied to the `View`. Aggregates can be specified by the user, or Perspective will use the following sensible default aggregates based on column type:\n\n- \"sum\" for `integer` and `float` columns\n- \"count\" for all other columns\n\nPerspective provides a selection of aggregate functions that can be applied to columns in the `View` constructor using a dictionary of column name to aggregate function name.\n\nAn aggregate also determines the column's RESULT TYPE, which need not match the input: `\"count\"` yields an `integer` whatever it counts, so a `date` column left on the default `\"count\"` is an `integer` in the resulting `View` — no longer a date. Set an aggregate that preserves the type (e.g. `\"any\"`, `\"last\"`) when the original type matters, such as a date used as a chart axis."},"group_by_depth":{"type":"number"},"filter_op":{"$ref":"#/definitions/FilterReducer"},"group_rollup_mode":{"$ref":"#/definitions/GroupRollupMode"},"split_rollup_mode":{"$ref":"#/definitions/SplitRollupMode"}},"required":["table"],"additionalProperties":false,"description":"The initial configuration of a NEW panel (`addPanel`, `restore`'s panel-creating upsert, `restoreWorkspace` `panels` entries). Unlike [`ViewerConfigUpdate`] — a patch against existing state — creation has no prior state: `table` is REQUIRED (a placed panel without a table binding would be permanently blank), absent fields mean \"default\" rather than \"leave unchanged\" (so no [`OptionalUpdate`] tri-state), and there is no `settings` field (element-level, not per-panel)."},"JsonValue":{"anyOf":[{"type":"number"},{"type":"string"},{"type":"boolean"},{"type":"array","items":{"$ref":"#/definitions/JsonValue"}},{"type":"object","additionalProperties":{"$ref":"#/definitions/JsonValue"}},{"type":"null"}]},"Filter":{"type":"array","minItems":3,"items":[{"type":"string"},{"type":"string"},{"$ref":"#/definitions/FilterTerm"}],"maxItems":3},"FilterTerm":{"anyOf":[{"type":"array","items":{"$ref":"#/definitions/Scalar"}},{"$ref":"#/definitions/Scalar"}]},"Scalar":{"type":["number","string","boolean","null"],"description":"This type represents the ViewConfig serializable type, which must be JSON safe."},"Sort":{"type":"array","minItems":2,"items":[{"type":"string"},{"$ref":"#/definitions/SortDir"}],"maxItems":2},"SortDir":{"type":"string","enum":["none","desc","asc","col desc","col asc","desc abs","asc abs","col desc abs","col asc abs"]},"Expressions":{"type":"object","additionalProperties":{"type":"string"}},"Windows":{"type":"object","additionalProperties":{"$ref":"#/definitions/WindowSpec"},"description":"The window columns of a `ViewConfig`, keyed by output column alias - symmetric with `expressions` (`{\"name\": {...spec}}`). An alias must not collide with a `Table` column, expression alias, or another window's key."},"WindowSpec":{"type":"object","properties":{"column":{"type":"string","description":"The input column - either a real `Table` column or an expression alias from the same `ViewConfig`."},"aggregate":{"type":"string"},"partition_by":{"type":"array","items":{"type":"string"},"description":"Columns whose distinct value tuples partition the rows; empty partitions the whole `Table` as one group."},"order_by":{"anyOf":[{"$ref":"#/definitions/WindowSort"},{"type":"null"}],"description":"The `Table` column which orders each partition, and the direction. This orders rows WITHIN the window frame only — it does not reorder the `View`, which is what the view-level `sort` does."},"rows":{"type":["number","null"],"description":"A frame of the `rows` preceding each row, plus the row itself. Mutually exclusive with `range` and `cumulative`."},"range":{"type":["number","null"],"description":"A frame of the rows whose `order_by` value lies within `range` of each row's, requiring a numeric or temporal `order_by`. Mutually exclusive with `rows` and `cumulative`."},"cumulative":{"type":["boolean","null"],"description":"`true` frames all rows from the partition start through each row. Default can be omitted."},"offset":{"type":["number","null"],"description":"Row offset for `lag`/`lead` (default 1)."},"alpha":{"type":["number","null"],"description":"Smoothing factor in `(0, 1]` for `ema`."}},"required":["column","aggregate"],"additionalProperties":false,"description":"The serialized form of [`WindowSpec`]."},"WindowSort":{"type":"array","minItems":2,"items":[{"type":"string"},{"$ref":"#/definitions/WindowSortDir"}],"maxItems":2,"description":"The `Table` column which orders each partition, with its direction - serialized as a two-element array (`[\"ts\", \"desc\"]`), symmetric with the `ViewConfig`'s `sort` field."},"WindowSortDir":{"type":"string","enum":["asc","desc"],"description":"A window's order direction. A dedicated two-variant enum rather than [`crate::config::SortDir`] - the column-sort extras (`col asc`, `abs`, `none`) are meaningless inside a window frame and unrepresentable here."},"Aggregate":{"anyOf":[{"type":"string"},{"type":"array","minItems":2,"items":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"maxItems":2}]},"FilterReducer":{"type":"string","enum":["and","or"]},"GroupRollupMode":{"type":"string","enum":["rollup","flat","total"]},"SplitRollupMode":{"type":"string","enum":["flat","rollup"],"description":"The `split_by` corollary to [`GroupRollupMode`]. `Flat` (the default, matching this crate's historical behavior) emits only full-depth split combinations as columns; `Rollup` additionally emits grand-total and subtotal column groups in \"totals before\" order. There is no `Total` variant - an empty `split_by` already expresses a single grand-total column group."}}}},"chunks":[{"title":"FAQ » Installation » Python installation fails on Windows","path":"FAQ.md","text":"Python wheels are published for supported Python versions and platforms. On\nWindows, ensure you have a compatible Python version and architecture. Install\nwith:\n\n```bash\npip install perspective-python\n```\n\nIf you encounter C++ binding errors or link errors, make sure you are using a\nsupported Python version and that your `pip` is up to date. Pre-built wheels\neliminate the need for a C++ compiler in most cases.\n\n<!-- _Related: [#928](https://github.com/perspective-dev/perspective/issues/928),\n[#1325](https://github.com/perspective-dev/perspective/issues/1325),\n[#1025](https://github.com/perspective-dev/perspective/issues/1025)_ -->"},{"title":"FAQ » Installation » Python import perspective fails with ImportError or undefined symbol","path":"FAQ.md","text":"This typically happens when the C++ shared library (`libpsp.so`) cannot be found\nor was built against a different Python version. Ensure your Python version\nmatches the installed wheel. On Linux, verify that required system libraries are\npresent. If you see errors about `libpsp.so` or undefined symbols, try\nreinstalling in a clean virtual environment.\n\n<!-- _Related: [#937](https://github.com/perspective-dev/perspective/issues/937),\n[#1120](https://github.com/perspective-dev/perspective/issues/1120),\n[#1216](https://github.com/perspective-dev/perspective/issues/1216),\n[#1332](https://github.com/perspective-dev/perspective/issues/1332)_ -->"},{"title":"FAQ » Installation » Python installation fails on macOS","path":"FAQ.md","text":"On Apple Silicon (M1/M2/M3), make sure you are using a native ARM Python build,\nnot one running under Rosetta. The published wheels include `aarch64` variants\nfor supported platforms.\n\n<!-- _Related: [#938](https://github.com/perspective-dev/perspective/issues/938),\n[#1170](https://github.com/perspective-dev/perspective/issues/1170)_ -->\n\nFAQ » Installation » How do I install Perspective in a Docker container?\n\nPerspective's Python wheels are built against `manylinux_2_28` containers (see\n[`.github/workflows/build.yaml`](../../.github/workflows/build.yaml)), so they\nare compatible with most Linux distributions based on glibc 2.28+ (e.g., Debian\n10+, Ubuntu 20.04+, RHEL 8+). Use a compatible base image:\n\n```dockerfile\nFROM python:3.12-slim\nRUN pip install perspective-python\n```\n\nAlpine Linux uses musl instead of glibc and is **not** compatible with the\npublished wheels.\n\n<!-- _Related: [#1201](https://github.com/perspective-dev/perspective/issues/1201)_ -->"},{"title":"FAQ » JavaScript Bundling » How do I use Perspective with Vite, Webpack, or esbuild?","path":"FAQ.md","text":"Perspective no longer exports bundler plugins. Instead, you must manually\nbootstrap the WASM binaries using your bundler's asset handling. See\n[Importing with or without a bundler](./how_to/javascript/importing.md) for\ncomplete examples for Vite, Webpack, esbuild, CDN, and inline builds.\n\n<!-- _Related: [#1734](https://github.com/perspective-dev/perspective/issues/1734),\n[#2725](https://github.com/perspective-dev/perspective/issues/2725),\n[#857](https://github.com/perspective-dev/perspective/issues/857),\n[#1497](https://github.com/perspective-dev/perspective/issues/1497),\n[#1655](https://github.com/perspective-dev/perspective/issues/1655)_ -->"},{"title":"FAQ » Framework Integration » How do I use Perspective with React?","path":"FAQ.md","text":"Perspective provides a dedicated\n[React component](./how_to/javascript/react.md). You must also still initialize\nPerspective's WebAssembly as per your bundler — see\n[Importing with or without a bundler](./how_to/javascript/importing.md).\n\n<!-- _Related: [#865](https://github.com/perspective-dev/perspective/issues/865),\n[#931](https://github.com/perspective-dev/perspective/issues/931),\n[#3023](https://github.com/perspective-dev/perspective/discussions/3023)_ -->\n\nFAQ » Framework Integration » How do I use Perspective with Next.js?\n\nPerspective relies on Web Workers and WASM, which require client-side rendering.\nUse dynamic imports with `ssr: false` in Next.js to load Perspective components\nonly on the client.\n\n<!-- _Related:\n[#2947](https://github.com/perspective-dev/perspective/discussions/2947),\n[#2181](https://github.com/perspective-dev/perspective/discussions/2181)_ -->"},{"title":"FAQ » Framework Integration » How do I use Perspective with Vue.js/Angular/etc?","path":"FAQ.md","text":"As a standard Web Component, `<perspective-viewer>` works in most JavaScript web\nframeworks directly via standard HTML/DOM APIs, but does not have dedicated\nintegration libraries for these frameworks.\n\n<!-- _Related:\n[#2787](https://github.com/perspective-dev/perspective/discussions/2787)_ -->\n\nFAQ » Expressions » How do I create computed/expression columns?\n\nUse the [`expressions`](./explanation/view/config/expressions.md) config option\nin your `View` to define new columns with ExprTK syntax, which must then be\n_used_ somewhere else in your config (like `columns`) to actually be visible &\ncalculated. In `<perspective-viewer>`, expression columns can be created from\nthe UI column sidebar by clicking the \"New Column\" button.\n\n<!-- _Related: [#1981](https://github.com/perspective-dev/perspective/issues/1981),\n[#2148](https://github.com/perspective-dev/perspective/issues/2148),\n[#1493](https://github.com/perspective-dev/perspective/issues/1493)_ -->"},{"title":"FAQ » Expressions » Can I reference one expression column from another?","path":"FAQ.md","text":"No, you must duplicate calculations that are shared between expression columns.\n\n<!-- _Related: [#2148](https://github.com/perspective-dev/perspective/issues/2148)_ -->\n\nFAQ » Expressions » Can I do date arithmetic in expressions?\n\nYes, but they must be converted to `float` values first (`integer` is an `i32`\nwhich is too small). See\n[Expressions](./explanation/view/config/expressions.md).\n\n<!-- _Related: [#3026](https://github.com/perspective-dev/perspective/issues/3026),\n[#1768](https://github.com/perspective-dev/perspective/issues/1768)_ -->"},{"title":"FAQ » Expressions » Can I do rolling sums or cumulative calculations?","path":"FAQ.md","text":"Yes — use [Window Columns](./explanation/view/config/windows.md), the\n`windows` property of a `View` config. These are ordered, partitioned rolling\ncomputations analogous to SQL window functions, declared per-`View` like\nexpression columns:\n\n```javascript\nconst view = await table.view({\n    columns: [\"Cumulative Sales\"],\n    windows: {\n        \"Cumulative Sales\": {\n            column: \"Sales\",\n            aggregate: \"sum\",\n            order_by: [\"Order Date\", \"asc\"],\n            cumulative: true,\n        },\n    },\n});\n```\n\nWindow Columns are supported by Perspective's built-in engine, by the DuckDB,\nClickHouse and Polars [Virtual Servers](./explanation/virtual_servers.md), and\nby the `<perspective-viewer>` UI. They update incrementally as the `Table`\nupdates.\n\n<!-- _Related: [#504](https://github.com/perspective-dev/perspective/issues/504),\n[#2600](https://github.com/perspective-dev/perspective/discussions/2600),\n[#2624](https://github.com/perspective-dev/perspective/issues/2624)_ -->"},{"title":"FAQ » Filters » Can I compose filters with OR logic?","path":"FAQ.md","text":"Perspective\n[filters](./explanation/view/config/selection_and_ordering.md#filter) are\ncomposed with AND logic by default. As an alternative, you can use\n[expression columns](./explanation/view/config/expressions.md) to create a\nboolean column that encodes your OR logic (or any arbitrary multi-column\npredicate), then filter on that column:\n\n```javascript\nconst view = await table.view({\n    expressions: {\n        or_filter:\n            \"if (\\\"State\\\" == 'Texas') true; else if (\\\"State\\\" == 'California') true; else false\",\n    },\n    filter: [[\"or_filter\", \"==\", true]],\n});\n```\n\n<!-- _Related: [#1192](https://github.com/perspective-dev/perspective/issues/1192)_ -->"},{"title":"FAQ » Filters » How do I update filters programmatically?","path":"FAQ.md","text":"Set the [`filter`](./explanation/view/config/selection_and_ordering.md#filter)\nproperty on a `View` config, or use the `<perspective-viewer>`\n[`.restore()`](./how_to/javascript/save_restore.md) method to update filters at\nruntime.\n\n<!-- _Related: [#935](https://github.com/perspective-dev/perspective/issues/935)_ -->\n\nFAQ » Filters » Does date filtering support ranges?\n\nDate columns can be\n[filtered](./explanation/view/config/selection_and_ordering.md#filter) with\ncomparison operators (`>`, `<`, `>=`, `<=`) to achieve range-based filtering.\nApply two filters on the same date column for a range.\n\n<!-- _Related:\n[#3100](https://github.com/perspective-dev/perspective/discussions/3100),\n[#2023](https://github.com/perspective-dev/perspective/issues/2023)_ -->"},{"title":"FAQ » Notebooks » PerspectiveWidget is not loading","path":"FAQ.md","text":"`PerspectiveWidget` is an [AnyWidget](https://anywidget.dev), shipped entirely\ninside the `perspective-python` wheel. There is no separate JupyterLab\nextension to install or version-match for the widget, so\n`jupyter labextension list` is not where to look.\n\nInstall the `jupyter` extra, which pulls in `anywidget`:\n\n```bash\npip install \"perspective-python[jupyter]\"\n```\n\nThen restart the kernel — and, for JupyterLab, reload the browser page. See\nthe [`PerspectiveWidget` guide](./how_to/python/jupyterlab.md).\n\n<!-- _Related: [#1392](https://github.com/perspective-dev/perspective/issues/1392),\n[#2059](https://github.com/perspective-dev/perspective/issues/2059),\n[#2307](https://github.com/perspective-dev/perspective/issues/2307)_ -->"},{"title":"FAQ » Notebooks » Does PerspectiveWidget work outside JupyterLab?","path":"FAQ.md","text":"Yes. Because the widget is an AnyWidget bundled into the wheel rather than a\nJupyterLab labextension, it runs in any AnyWidget-compatible host —\nJupyterLab, classic Jupyter Notebook, **VSCode notebooks**, Google Colab and\nMarimo — with no per-host install step.\n\nThe separate `@perspective-dev/jupyterlab` package is now _optional_ and\nprovides only the \"Open With → Perspective\" file renderers for `csv`, `json`\nand `arrow` files in JupyterLab.\n\n<!-- _Related: [#2783](https://github.com/perspective-dev/perspective/issues/2783),\n[#3042](https://github.com/perspective-dev/perspective/issues/3042),\n[#3056](https://github.com/perspective-dev/perspective/issues/3056)_ -->"},{"title":"FAQ » Memory and Performance » Perspective has a memory leak","path":"FAQ.md","text":"Maybe, but please review the\n[Cleaning up resources](./how_to/javascript/deleting.md) docs carefully before\nopening an Issue reporting it (and of course review\n[`CONTRIBUTING.md`](https://github.com/perspective-dev/perspective/blob/master/CONTRIBUTING.md)\nbefore opening _any_ Issue). Ensure you call `.delete()` on Views, Tables, and\n`<perspective-viewer>` instances when they are no longer needed, in reverse\ndependency order.\n\n<!-- _Related: [#1037](https://github.com/perspective-dev/perspective/issues/1037),\n[#1723](https://github.com/perspective-dev/perspective/issues/1723),\n[#3035](https://github.com/perspective-dev/perspective/issues/3035),\n[#1329](https://github.com/perspective-dev/perspective/issues/1329)_ -->"},{"title":"FAQ » Memory and Performance » How many rows can Perspective's built-in engine handle?","path":"FAQ.md","text":"Perspective is designed for large datasets and can handle millions of rows\ndepending on the number of columns and available memory. Performance also\nsignificantly depends on column types (`\"string\"` being slower and larger than\nother types due to dictionary interning).\n\nFor larger datasets or out-of-memory virtualized datasets, see\n[Virtual Servers](./explanation/virtual_servers.md).\n\n<!-- _Related: [#341](https://github.com/perspective-dev/perspective/issues/341),\n[#1719](https://github.com/perspective-dev/perspective/issues/1719),\n[#1089](https://github.com/perspective-dev/perspective/issues/1089)_ -->"},{"title":"FAQ » Memory and Performance » How do I control threading in perspective-python?","path":"FAQ.md","text":"The Python library uses a thread pool internally. For advanced threading\ncontrol, consult the\n[multithreading documentation](./how_to/python/multithreading.md).\n\n<!-- _Related: [#1145](https://github.com/perspective-dev/perspective/issues/1145),\n[#1313](https://github.com/perspective-dev/perspective/issues/1313)_ -->\n\nFAQ » Theming and Styling » How do I enable dark theme?\n\nImport `themes.css` (see [Theming](./how_to/javascript/theming.md)) and set the\ntheme via `restore()`:\n\n```javascript\nawait viewer.restore({ theme: \"Pro Dark\" });\n```\n\nOr import just the dark theme directly:\n`import \"@perspective-dev/viewer/dist/css/pro-dark.css\";`\n\n<!-- _Related: [#950](https://github.com/perspective-dev/perspective/issues/950),\n[#882](https://github.com/perspective-dev/perspective/issues/882)_ -->"},{"title":"FAQ » Theming and Styling » Can I create a custom cell renderer for the datagrid?","path":"FAQ.md","text":"The datagrid plugin supports custom styling via\n[`column_config`](https://perspective-dev.github.io/viewer/types/src_ts_ts-rs_ColumnConfigValues.ts.ColumnConfigValues.html)\nand CSS custom properties, but custom cell renderers require building a custom\nplugin.\n\n<!-- _Related: [#1508](https://github.com/perspective-dev/perspective/issues/1508)_ -->\n\nFAQ » Theming and Styling » How do I customize chart colors?\n\nChart colors can be customized via\n[CSS custom properties](./how_to/javascript/theming.md#custom-themes) on the\n`<perspective-viewer>` element.\n\n<!-- _Related:\n[#2810](https://github.com/perspective-dev/perspective/discussions/2810),\n[#2859](https://github.com/perspective-dev/perspective/discussions/2859),\n[#2000](https://github.com/perspective-dev/perspective/discussions/2000)_ -->"},{"title":"FAQ » Streaming and Real-Time Updates » How do I stream data into a Perspective table?","path":"FAQ.md","text":"Use [`table.update()`](./explanation/table/update_and_remove.md) to push new\ndata incrementally. For [indexed](./explanation/table/options.md) tables,\nupdates with matching index values will replace existing rows.\n\n<!-- _Related: [#1133](https://github.com/perspective-dev/perspective/issues/1133),\n[#1054](https://github.com/perspective-dev/perspective/issues/1054)_ -->\n\nFAQ » Streaming and Real-Time Updates » table.update() raises \"No Running Event Loop\"\n\nPerspective 3+ is now threadsafe by default and no longer requires special loop\nintegration.\n\n<!-- _Related:\n[#2801](https://github.com/perspective-dev/perspective/discussions/2801)_ -->"},{"title":"FAQ » Streaming and Real-Time Updates » How do I listen for data updates?","path":"FAQ.md","text":"Use `view.on_update()` to register a callback that fires when the underlying\ntable data changes. See [Listening for events](./how_to/javascript/events.md)\nand [Advanced View Operations](./explanation/view/advanced.md#update-callbacks).\n\n<!-- _Related: [#1152](https://github.com/perspective-dev/perspective/issues/1152),\n[#2912](https://github.com/perspective-dev/perspective/discussions/2912)_ -->\n\nFAQ » Server Architecture » What is the difference between Client-only, Client/Server, and Server-only modes?\n\n- **Client-only**: The Perspective engine runs entirely in the browser via WASM.\n  Best for small to medium datasets.\n- **Client/Server (replicated)**: Data is hosted on a server and replicated to\n  the client. The client has a full copy and performs queries locally.\n- **Server-only**: All queries are executed on the server. The client only\n  renders results. Best for very large datasets.\n\nSee [Data Architecture](./explanation/architecture.md) for detailed explanations\nof each mode.\n\n<!-- _Related:\n[#2916](https://github.com/perspective-dev/perspective/discussions/2916)_ -->"},{"title":"FAQ » Server Architecture » Is the WebSocket Perspective Server safe to expose to untrusted clients?","path":"FAQ.md","text":"No. The WebSocket `Server` is not a security boundary. Every connected `Client`\nis treated as the author of the queries it submits, and is permitted to create\nand delete `Table`/`View` resources, author arbitrary\n[expression columns](./explanation/view/config/expressions.md), and — for\n[Virtual Server](./explanation/virtual_servers.md) backends like DuckDB or\nClickHouse — author SQL fragments executed under the configured database\nrole. The bundled WebSocket adapters\n(`tornado.py`/`aiohttp.py`/`starlette.py`/`WebSocketServer`) are reference\nintegrations and do not authenticate, authorize, or enforce origin policy.\n\nWebSocket Deployments that need per-user isolation must put an authenticating\nproxy in front of the `Server`, run a least-privileged database role for any\n`Virtual Server` backend, and/or isolate users into separate `Server`\ninstances. See [`SECURITY.md`](../../SECURITY.md) for the full threat model\nand deployment guidance.\n\nObviously, none of this applies to WASM DBs like Perspective and DuckDB."},{"title":"FAQ » Server Architecture » Does Perspective sanitize SQL Virtual Servers?","path":"FAQ.md","text":"No, by design. [Virtual Server](./explanation/virtual_servers.md) backends\ninterpolate client-supplied `view_id`, `table_id`, `column_name`, expression\nstrings, and filter operators directly into SQL templates without\nparameterization or whitelist validation. The `Client` is the author of the\nqueries — there is no privilege boundary inside the engine for sanitization\nto enforce. If your deployment needs to restrict the SQL surface area exposed\nto a `Client`, the supported boundary is the database role the `Virtual Server`\nis configured with (read-only etc), or better complete isolation via WASM\nbackend."},{"title":"FAQ » Server Architecture » How do I set up WebSocket authentication?","path":"FAQ.md","text":"The [`WebSocketServer`](./how_to/javascript/nodejs_server.md) does not include\nbuilt-in authentication. Implement authentication at the transport layer (e.g.,\nvia middleware in your HTTP server) before the WebSocket upgrade. For more\ncomplex needs, `WebSocketServer` is a simple example server based on the\n`node:http` module which can serve as a starting point for a custom server.\n\n<!-- _Related:\n[#2788](https://github.com/perspective-dev/perspective/discussions/2788)_ -->\n\nFAQ » Server Architecture » Can I bind Perspective to a database?\n\nPerspective supports [Virtual Servers](./explanation/virtual_servers.md) that\nproxy queries to external data sources, with built-in implementations for e.g.\n[DuckDB](./how_to/javascript/virtual_server/duckdb.md).\n\n<!-- _Related: [#1255](https://github.com/perspective-dev/perspective/issues/1255),\n[#1361](https://github.com/perspective-dev/perspective/discussions/1361)_ -->"},{"title":"FAQ » Aggregation » Can I apply multiple aggregates to the same column?","path":"FAQ.md","text":"Yes, by creating a duplicate/alias for your column via\n[`expressions`](./explanation/view/config/expressions.md):\n\n```javascript\nawait viewer.restore({\n    columns: [\"Sales\", \"Sales 2\"],\n    expresions: { \"Sales 2\": '\"Sales\"' },\n    aggregate: {\n        Sales: \"sum\",\n        \"Sales 2\": \"avg\",\n    },\n});\n```\n\n<!-- _Related: [#272](https://github.com/perspective-dev/perspective/issues/272)_ -->\n\nFAQ » Aggregation » Can I compute a ratio between aggregated columns?\n\nUse [expression columns](./explanation/view/config/expressions.md) on an\naggregated View to compute ratios. Define an expression that divides one column\nby another.\n\n<!-- _Related:\n[#2994](https://github.com/perspective-dev/perspective/discussions/2994),\n[#3096](https://github.com/perspective-dev/perspective/discussions/3096)_ -->"},{"title":"FAQ » Data Loading and Arrow » How do I load Apache Arrow data into Perspective?","path":"FAQ.md","text":"Perspective natively accepts\n[Apache Arrow format](./explanation/table/loading_data.md). Pass an\n`ArrayBuffer` containing Arrow IPC data directly to `table()` or\n`table.update()`.\n\n<!-- _Related: [#1157](https://github.com/perspective-dev/perspective/issues/1157),\n[#929](https://github.com/perspective-dev/perspective/issues/929)_ -->\n\nFAQ » Data Loading and Arrow » What data formats does Perspective accept?\n\nPerspective accepts (see [Loading data](./explanation/table/loading_data.md)):\n\n- **JavaScript**: JSON (row-oriented or column-oriented objects), CSV strings,\n  Apache Arrow `ArrayBuffer`\n- **Python**: `dict`, `list`, `pandas.DataFrame`, `pyarrow.Table`, CSV strings,\n  Apache Arrow bytes\n\n<!-- _Related: [#929](https://github.com/perspective-dev/perspective/issues/929),\n[#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->"},{"title":"FAQ » Data Loading and Arrow » CSV update fails but CSV creation works","path":"FAQ.md","text":"When updating a table created with a schema, ensure the CSV column names and\ntypes match the schema exactly. Mismatched column names or types will cause\nupdate failures.\n\n<!-- _Related: [#2524](https://github.com/perspective-dev/perspective/issues/2524)_ -->\n\nFAQ » Export » Can I export the viewer to HTML, PNG or PDF?\n\nHTML export is available via `viewer.export({ method: \"html\" })`. For an\nimage, use `{ method: \"plugin\" }`, which asks the plugin to render itself —\nthis produces a PNG for chart plugins (and text for the datagrid). For PDF,\nrender the viewer and use browser or headless browser screenshot capabilities.\n\n<!-- _Related: [#2836](https://github.com/perspective-dev/perspective/issues/2836),\n[#2770](https://github.com/perspective-dev/perspective/discussions/2770),\n[#2772](https://github.com/perspective-dev/perspective/issues/2772)_ -->"},{"title":"FAQ » Export » Can I export data to Excel?","path":"FAQ.md","text":"Perspective does not have built-in Excel export. Export data via\n`view.to_csv()`, `view.to_json()`, or `view.to_arrow()` (see\n[Serializing data](./how_to/javascript/serializing.md)) and convert to Excel\nusing a library like `xlsx` (JavaScript) or `openpyxl` (Python).\n\n<!-- _Related:\n[#2738](https://github.com/perspective-dev/perspective/discussions/2738)_ -->\n\nFAQ » Export » How do I copy data from a cell or row?\n\nUse one of the `-selected` export methods, which operate on the current\nselection. To place it on the clipboard:\n\n```javascript\nawait viewer.copy({ method: \"csv-selected\" });\n```\n\n... or to get it as a string, `await viewer.export({ method: \"csv-selected\" })`.\n`json-selected` and `arrow-selected` are also available.\n\n<!-- _Related: [#2765](https://github.com/perspective-dev/perspective/issues/2765),\n[#2356](https://github.com/perspective-dev/perspective/discussions/2356)_ -->"},{"title":"FAQ » Table Operations » table.remove() does not update the viewer","path":"FAQ.md","text":"The [`remove()`](./explanation/table/update_and_remove.md) method requires an\n[indexed](./explanation/table/options.md) table. Ensure your table was created\nwith an `index` option, and pass the index values to remove.\n\n<!-- _Related: [#1597](https://github.com/perspective-dev/perspective/issues/1597),\n[#2293](https://github.com/perspective-dev/perspective/issues/2293)_ -->\n\nFAQ » Viewer Configuration » How do I save and restore the viewer state?\n\nUse\n[`viewer.save()` and `viewer.restore()`](./how_to/javascript/save_restore.md) to\nserialize and deserialize the full viewer configuration.\n\n<!-- _Related: [#1501](https://github.com/perspective-dev/perspective/issues/1501),\n[#1560](https://github.com/perspective-dev/perspective/issues/1560)_ -->"},{"title":"FAQ » Viewer Configuration » Can I hide the configuration panel?","path":"FAQ.md","text":"The settings panel can be toggled programmatically via\n`await viewer.restore({ settings: false })`.\n\n<!-- _Related:\n[#2581](https://github.com/perspective-dev/perspective/discussions/2581),\n[#1085](https://github.com/perspective-dev/perspective/issues/1085)_ -->\n\nFAQ » Viewer Configuration » Can I collapse row groups by default?\n\nRow group can be closed imperatively via\n[`view.set_depth()`](./explanation/view/advanced.md). Expansion state is not\npersisted or configurable via the `save`/`restore` API currently.\n\n<!-- _Related:\n[#2695](https://github.com/perspective-dev/perspective/discussions/2695),\n[#2861](https://github.com/perspective-dev/perspective/issues/2861)_ -->"},{"title":"FAQ » Internationalization » Can I change the UI language?","path":"FAQ.md","text":"Perspective's UI text is defined via CSS variables, which can be customized per\ntheme. See the\n[Icons and Translation](./how_to/javascript/theming.md#icons-and-translation)\nsection of the theming guide for details.\n\n<!-- _Related: [#1934](https://github.com/perspective-dev/perspective/issues/1934),\n[#2358](https://github.com/perspective-dev/perspective/issues/2358)_ -->\n\nFAQ » Rust » How do I build Perspective from Rust?\n\nSee the [Getting Started](./how_to/rust.md) guide for Rust. The Rust crate wraps\nthe C++ engine and requires a C++ toolchain. You need `cmake` installed and on\nyour path to build the engine.\n\n<!-- _Related:\n[#3121](https://github.com/perspective-dev/perspective/discussions/3121),\n[#3080](https://github.com/perspective-dev/perspective/discussions/3080),\n[#2684](https://github.com/perspective-dev/perspective/discussions/2684)_ -->"},{"title":"FAQ » Miscellaneous » Can I use Perspective without <perspective-viewer>?","path":"FAQ.md","text":"Yes. The `perspective` library (data engine) can be used independently for\nserver-side data processing without any UI. Use\n[`table()` and `view()`](./how_to/javascript/worker.md) directly to query data.\n\n<!-- _Related:\n[#2933](https://github.com/perspective-dev/perspective/discussions/2933),\n[#2644](https://github.com/perspective-dev/perspective/discussions/2644)_ -->\n\nFAQ » Miscellaneous » Can I use Perspective in Pyodide?\n\nYes. Perspective publishes Emscripten wheels to PyPI under\n[PEP 783](https://peps.python.org/pep-0783/), so `perspective-python` can be\ninstalled by Pyodide's own package resolution — there is no need to download\nand host a wheel yourself.\n\nEmscripten wheels are ABI-tied to a specific Emscripten version, and thus to\nthe Pyodide versions built against it. If resolution fails, check that your\nPyodide version matches a published wheel tag.\n\n<!-- _Related: [#3186](https://github.com/perspective-dev/perspective/issues/3186),\n[#2880](https://github.com/perspective-dev/perspective/discussions/2880)_ -->"},{"title":"FAQ » Miscellaneous » How do I handle row selection events?","path":"FAQ.md","text":"Listen for\n[`perspective-click` and `perspective-select`](./how_to/javascript/events.md)\nevents on the `<perspective-viewer>` element.\n\n<!-- _Related:\n[#2589](https://github.com/perspective-dev/perspective/discussions/2589),\n[#1076](https://github.com/perspective-dev/perspective/issues/1076)_ -->"},{"title":"api_reference » API Reference","path":"api_reference.md","text":"Perspective's complete API is hosted on `docs.rs`:\n\n- Python API\n    - [`perspective`](https://perspective-dev.github.io/python/index.html)\n    - [`perspective.widget`](https://perspective-dev.github.io/python/perspective/widget.html)\n    - [`perspective.handlers.aiohttp`](https://perspective-dev.github.io/python/perspective/handlers/aiohttp.html)\n    - [`perspective.handlers.starlette`](https://perspective-dev.github.io/python/perspective/handlers/starlette.html)\n    - [`perspective.handlers.tornado`](https://perspective-dev.github.io/python/perspective/handlers/tornado.html)\n- JavaScript API\n    - [`@perspective-dev/client` Browser](https://perspective-dev.github.io/browser/modules/src_ts_perspective.browser.ts.html)\n    - [`@perspective-dev/client` Node.js](https://perspective-dev.github.io/node/modules/src_ts_perspective.node.ts.html)\n    - [`@perspective-dev/viewer`](https://perspective-dev.github.io/viewer/modules/perspective-viewer.html)\n    - [`@perspective-dev/react`](https://perspective-dev.github.io/react/index.html)\n- Rust API\n    - [`perspective`](https://docs.rs/perspective/latest/perspective/)\n    - [`perspective-client`](https://docs.rs/perspective-client/latest/perspective_client/)\n    - [`perspective-server`](https://docs.rs/perspective-server/latest/perspective_server/)\n    - [`perspective-python`](https://docs.rs/perspective-python/latest/perspective_python/)\n    - [`perspective-js`](https://docs.rs/perspective-js/latest/perspective_js/)\n    - [`perspective-viewer`](https://docs.rs/perspective-viewer/latest/perspective_viewer/)"},{"title":"client_only » Client-only","path":"client_only.md","text":"<img src=\"./architecture.sub1.svg\" />\n\n_For static datasets, datasets provided by the user, and simple server-less and\nread-only web applications._\n\nIn this design, Perspective is run as a client Browser WebAssembly library, the\ndataset is downloaded entirely to the client and all calculations and UI\ninteractions are performed locally. Interactive performance is very good, using\nWebAssembly engine for near-native runtime plus WebWorker isolation for parallel\nrendering within the browser. Operations like scrolling and creating new views\nare responsive. However, the entire dataset must be downloaded to the client.\nPerspective is not a typical browser component, and datset sizes of 1gb+ in\nApache Arrow format will load fine with good interactive performance!\n\nHorizontal scaling is a non-issue, since here is no concurrent state to scale,\nand only uses client-side computation via WebAssembly client. Client-only\nperspective can support as many concurrent users as can download the web\napplication itself. Once the data is loaded, no server connection is needed and\nall operations occur in the client browser, imparting no additional runtime cost\non the server beyond initial load. This also means updates and edits are local\nto the browser client and will be lost when the page is refreshed, unless\notherwise persisted by your application.\n\nAs the client-only design starts with creating a client-side Perspective\n`Table`, data can be provided by any standard web service in any Perspective\ncompatible format (JSON, CSV or Apache Arrow)."},{"title":"client_only » Client-only » Javascript client","path":"client_only.md","text":"```javascript\nconst worker = await perspective.worker();\nconst table = await worker.table(csv);\n\nconst viewer = document.createElement(\"perspective-viewer\");\ndocument.body.appendChild(viewer);\nawait viewer.load(table);\n```"},{"title":"client_server » Client/Server replicated","path":"client_server.md","text":"<img src=\"./architecture.sub2.svg\" />\n\n_For medium-sized, real-time, synchronized and/or editable data sets with many\nconcurrent users._\n\nThe dataset is instantiated in-memory with a Python or Node.js Perspective\nserver, and web applications create duplicates of these tables in a local\nWebAssembly client in the browser, synchonized efficiently to the server via\nApache Arrow. This design scales well with additional concurrent users, as\nbrowsers only need to download the initial data set and subsequent update\ndeltas, while operations like scrolling, pivots, sorting, etc. are performed on\nthe client.\n\nPython servers can make especially good use of additional threads, as\nPerspective will release the GIL for almost all operations. Interactive\nperformance on the client is very good and identical to client-only\narchitecture. Updates and edits are seamlessly synchonized across clients via\ntheir virtual server counterparts using websockets and Apache Arrow."},{"title":"client_server » Client/Server replicated » Python and Tornado server","path":"client_server.md","text":"```python\nfrom perspective import Server, PerspectiveTornadoHandler\n\nserver = Server()\nclient = server.new_local_client()\nclient.table(csv, name=\"my_table\")\nroutes = [(\n    r\"/websocket\",\n    perspective.handlers.tornado.PerspectiveTornadoHandler,\n    {\"perspective_server\": server},\n)]\n\napp = tornado.web.Application(routes)\napp.listen(8080)\nloop = tornado.ioloop.IOLoop.current()\nloop.start()\n```\n\nclient_server » Client/Server replicated » Javascript client\n\nPerspective's websocket client interfaces with the Python server, then\n_replicates_ the server-side Table.\n\n```javascript\nconst websocket = await perspective.websocket(\"ws://localhost:8080\");\nconst server_table = await websocket.open_table(\"my_table\");\nconst server_view = await server_table.view();\n\nconst worker = await perspective.worker();\nconst client_table = await worker.table(server_view);\n\nconst viewer = document.createElement(\"perspective-viewer\");\ndocument.body.appendChild(viewer);\nawait viewer.load(client_table);\n```"},{"title":"server_only » Server-only","path":"server_only.md","text":"<img src=\"./architecture.sub3.svg\" />\n\n_For extremely large datasets with a small number of concurrent users._\n\nThe dataset is instantiated in-memory with a Python or Node.js server, and web\napplications connect virtually. Has very good initial load performance, since no\ndata is downloaded. Group-by and other operations will run column-parallel if\nconfigured.\n\nBut interactive performance is poor, as every user interaction must page the\nserver to render. Operations like scrolling are not as responsive and can be\nimpacted by network latency. Web applications must be \"always connected\" to the\nserver via WebSocket. Disconnecting will prevent any interaction, scrolling,\netc. of the UI. Does not use WebAssembly.\n\nEach connected browser will impact server performance as long as the connection\nis open, which in turn impacts interactive performance of every client. This\nultimately limits the horizontal scalabity of this architecture. Since each\nclient reads the perspective `Table` virtually, changes like edits and updates\nare automatically reflected to all clients and persist across browser refresh.\nUsing the same Python server as the previous design, we can simply skip the\nintermediate WebAssembly `Table` and pass the virtual table directly to `load()`\n\n```javascript\nconst websocket = await perspective.websocket(\"ws://localhost:8080\");\nconst server_table = await websocket.open_table(\"my_table\");\n\nconst viewer = document.createElement(\"perspective-viewer\");\ndocument.body.appendChild(viewer);\nawait viewer.load(server_table);\n```"},{"title":"architecture » Data Architecture","path":"architecture.md","text":"Application developers can choose from\n[Client (WebAssembly)](./architecture/client_only.md),\n[Server (Python/Node)](./architecture/server_only.md) or\n[Client/Server Replicated](./architecture/client_server.md) designs to bind\ndata, and a web application can use one or a mix of these designs as needed. By\nserializing to Apache Arrow, tables are duplicated and synchronized across\nruntimes efficiently.\n\nPerspective is a multi-language platform. The examples in this section use\nPython and JavaScript as an example, but the same general principles apply to\nany `Client`/`Server` combination.\n\n<img src=\"./architecture/architecture.svg\" />"},{"title":"join_types » Join Types","path":"join_types.md","text":"`Client::join` supports three join types, specified via the `join_type` option.\nThe default is `\"inner\"`.\n\njoin_types » Join Types » Inner Join (default)\n\nAn inner join includes only rows where the key column exists in _both_ source\ntables. Rows from either table that have no match in the other are excluded.\n\njoin_types » Join Types » Left Join\n\nA left join includes all rows from the left table. For left rows that have no\nmatch in the right table, right-side columns are filled with `null`.\n\njoin_types » Join Types » Outer Join\n\nAn outer join includes all rows from both tables. Unmatched rows on either side\nhave their missing columns filled with `null`.\n\n| `join_type` | Left-only rows | Right-only rows |\n| ----------- | -------------- | --------------- |\n| `\"inner\"`   | excluded       | excluded        |\n| `\"left\"`    | included       | excluded        |\n| `\"outer\"`   | included       | included        |"},{"title":"options » Join Options » on — Join Key Column","path":"options.md","text":"The `on` parameter specifies the column name used to match rows between the left\nand right tables. This column must exist in the left table and, by default, must\nalso exist in the right table with the same name and compatible type.\n\nThe join key column becomes the index of the resulting table.\n\noptions » Join Options » right_on — Different Right Key Column\n\nWhen the join key has a different name in the right table, use `right_on` to\nspecify the right table's column name. The left table's column name (`on`) is\nused in the output schema; the right key column is excluded from the result.\n\nThe `on` and `right_on` columns must have compatible types. An error is thrown\nif the types do not match."},{"title":"options » Join Options » join_type — Join Type","path":"options.md","text":"Controls which rows are included in the result. See\n[Join Types](./join_types.md) for details.\n\n| Value       | Behavior                                              |\n| ----------- | ----------------------------------------------------- |\n| `\"inner\"`   | Only rows with matching keys in both tables (default) |\n| `\"left\"`    | All left rows; unmatched right columns are `null`     |\n| `\"outer\"`   | All rows from both tables; unmatched columns are `null` |\n\noptions » Join Options » name — Table Name\n\nAn optional name for the resulting joined table. If omitted, a random name is\ngenerated. This name is used to identify the table in the server's hosted table\nregistry."},{"title":"reactivity » Reactivity and Constraints » Reactive Updates","path":"reactivity.md","text":"Joined tables are fully reactive. When either source table receives an\n`update()`, the join is automatically recomputed and any `View` created from the\njoined table will reflect the new data. This includes:\n\n- Updates that modify existing rows in either source table.\n- New rows added to either source table that create new matches.\n- Chained joins — if a joined table is itself used as input to another join,\n  updates propagate through the entire chain.\n\nreactivity » Reactivity and Constraints » Duplicate Keys\n\nLike SQL, `join()` produces a cross-product for each matching key value. When\nmultiple rows in the left table share the same key, each is paired with every\nmatching row in the right table (and vice versa). The number of output rows for\na given key is `left_count × right_count`.\n\nThis behavior depends on whether the source tables are _indexed_:\n\n- **Unindexed tables** (no `index` option) — rows are appended, so duplicate\n  keys accumulate naturally. Each `update()` appends new rows, which may\n  introduce additional duplicates.\n- **Indexed tables** (`index` set to the join key) — each key appears at most\n  once per table, so the join produces at most one row per key. Updates replace\n  existing rows in-place rather than appending."},{"title":"reactivity » Reactivity and Constraints » Read-Only","path":"reactivity.md","text":"Joined tables are read-only. Calling `update()`, `remove()`, `clear()`, or\n`replace()` on a joined table will throw an error. Data can only change\nindirectly, by updating the source tables.\n\nreactivity » Reactivity and Constraints » Column Name Conflicts\n\nThe left and right tables must not have overlapping column names (other than the\njoin key). If a non-key column name appears in both tables, `join()` throws an\nerror. Rename columns in your source data or use `View` expressions to avoid\nconflicts."},{"title":"reactivity » Reactivity and Constraints » Source Table Deletion","path":"reactivity.md","text":"A source table cannot be deleted while a joined table depends on it. You must\ndelete the joined table first, then delete the source tables."},{"title":"join » Join","path":"join.md","text":"`Client::join` creates a read-only `Table` by joining two source tables on a\nshared key column. The `left` and `right` arguments can be `Table` objects or\nstring table names (as returned by `get_hosted_table_names()`). The resulting\ntable is _reactive_: whenever either source table is updated, the join is\nautomatically recomputed and any `View` derived from the joined table will\nupdate accordingly.\n\nJoined tables support the full `View` API — you can apply `group_by`,\n`split_by`, `sort`, `filter`, `expressions`, and all other `View` operations on\nthe result, just as you would with any other `Table`."},{"title":"python » What is perspective-python","path":"python.md","text":"Perspective for Python uses the exact same C++ data engine used by the\n[WebAssembly version](https://docs.rs/perspective-js/latest/perspective_js/) and\n[Rust version](https://docs.rs/crate/perspective/latest). The library consists\nof many of the same abstractions and API as in JavaScript, as well as\nPython-specific data loading support for [NumPy](https://numpy.org/),\n[Pandas](https://pandas.pydata.org/) (and\n[Apache Arrow](https://arrow.apache.org/), as in JavaScript).\n\nAdditionally, `perspective-python` provides a session manager suitable for\nintegration into server systems such as\n[Tornado websockets](https://www.tornadoweb.org/en/stable/websocket.html),\n[AIOHTTP](https://docs.aiohttp.org/en/stable/web_quickstart.html#websockets), or\n[Starlette](https://www.starlette.io/websockets/)/[FastAPI](https://fastapi.tiangolo.com/advanced/websockets/),\nwhich allows fully _virtual_ Perspective tables to be interacted with by\nmultiple `<perspective-viewer>` in a web browser. You can also interact with a\nPerspective table from python clients, and to that end client libraries are\nimplemented for both Tornado and AIOHTTP."},{"title":"python » What is perspective-python » Example","path":"python.md","text":"A simple example which loads an [Apache Arrow](https://arrow.apache.org/) and\ncomputes a \"Group By\" operation, returning a new Arrow.\n\n```python\nfrom perspective import Server\n\nclient = Server().new_local_client()\ntable = client.table(arrow_bytes_data)\nview = table.view(group_by = [\"CounterParty\", \"Security\"])\narrow = view.to_arrow()\n```\n\n[More Examples](https://github.com/perspective-dev/perspective/tree/master/examples)\nare available on GitHub.\n\npython » What is perspective-python » What's included\n\nThe `perspective` module exports several tools:\n\n- `Server` the constructor for a new instance of the Perspective data engine.\n- The `perspective.widget` module exports `PerspectiveWidget`, the JupyterLab\n  widget for interactive visualization in a notebook cell.\n- The `perspective.handlers` modules exports web frameworks handlers that\n  interface with a `perspective-client` in JavaScript.\n    - `perspective.handlers.tornado.PerspectiveTornadoHandler` for\n      [Tornado](https://www.tornadoweb.org/)\n    - `perspective.handlers.starlette.PerspectiveStarletteHandler` for\n      [Starlette](https://www.starlette.io/) and\n      [FastAPI](https://fastapi.tiangolo.com)\n    - `perspective.handlers.aiohttp.PerspectiveAIOHTTPHandler` for\n      [AIOHTTP](https://docs.aiohttp.org),"},{"title":"python » What is perspective-python » What's included » Virtual UI server","path":"python.md","text":"As `<perspective-viewer>` or any other Perspective `Client` will only consume\nthe data necessary to render the current screen (or whatever else was requested\nvia the API), this runtime mode allows large datasets without the need to copy\nthem entirely to the Browser, at the expense of network latency on UI\ninteraction/API calls.\n\npython » What is perspective-python » What's included » Notebooks\n\n`PerspectiveWidget` is an [AnyWidget](https://anywidget.dev) that implements\nthe same API as `<perspective-viewer>`, and runs such a viewer in either\nserver or client (via WebAssembly) mode.\n\nThe widget is bundled entirely inside the `perspective-python` wheel, so\nthere is no per-host extension to install. It runs identically in\n[JupyterLab](https://jupyterlab.readthedocs.io/en/stable/), classic Jupyter\nNotebook, VSCode notebooks, Google Colab and Marimo. Install the `jupyter`\nextra to pull in `anywidget`:\n\n```bash\npip install \"perspective-python[jupyter]\"\n```\n\nSeparately, the _optional_ `@perspective-dev/jupyterlab` package provides\nconvenient builtin viewers for `csv`, `json`, or `arrow` files in JupyterLab.\nWith it installed, right-click a file of one of these types and choose the\nappropriate `Perspective` option from the context menu."},{"title":"clear_and_replace » Table::clear and Table::replace","path":"clear_and_replace.md","text":"Calling `Table::clear` will remove all data from the underlying `Table`. Calling\n`Table::replace` with new data will clear the `Table`, and update it with a new\ndataset that conforms to Perspective's data types and the existing schema on the\n`Table`.\n\n<div class=\"javascript\">\n\n```javascript\ntable.clear();\ntable.replace(json);\n```\n\n</div>\n<div class=\"python\">\n\n```python\ntable.clear()\ntable.replace(df)\n```\n\n</div>"},{"title":"constructing_schema » Construct a Table","path":"constructing_schema.md","text":"Examples of constructing an empty `Table` from a schema.\n\n<div class=\"javascript\">\n\nJavaScript:\n\n```javascript\nvar schema = {\n    x: \"integer\",\n    y: \"string\",\n    z: \"boolean\",\n};\n\nconst table2 = await worker.table(schema);\n```\n\n</div>\n<div class=\"python\">\n\nPython:\n\n```python\nfrom datetime import date, datetime\n\nschema = {\n    \"x\": \"integer\",\n    \"y\": \"string\",\n    \"z\": \"boolean\",\n}\n\ntable2 = perspective.table(schema)\n```\n\n</div>\n<div class=\"rust\">\n\nRust:\n\n```rust\nlet data = TableData::Schema(vec![(\" a\".to_string(), ColumnType::FLOAT)]);\nlet options = TableInitOptions::default();\nlet table = client.table(data.into(), options).await?;\n```\n\n</div>"},{"title":"loading_data » Loading data","path":"loading_data.md","text":"A `Table` may also be created-or-updated by data in CSV,\n[Apache Arrow](https://arrow.apache.org/), JSON row-oriented or JSON\ncolumn-oriented formats. In addition to these, `perspective-python` additionally\nsupports `pyarrow.Table`, `polars.DataFrame` and `pandas.DataFrame` objects\ndirectly. These formats are otherwise identical to the built-in formats and\ndon't exhibit any additional support or type-awareness; e.g., `pandas.DataFrame`\nsupport is _just_ `pyarrow.Table.from_pandas` piped into Perspective's Arrow\nreader.\n\n`Client::table` and `Table::update` perform _coercion_ on their input for all\ninput formats _except_ Arrow (which comes with its own schema and has no need\nfor coercion). `\"date\"` and `\"datetime\"` column types do not have native JSON\nrepresentations, so these column types _cannot_ be inferred from JSON input.\nInstead, for columns of these types for JSON input, a `Table` must first be\nconstructed with a _schema_. Next, call `Table::update` with the JSON input -\nPerspective's JSON reader may _coerce_ a `date` or `datetime` from these native\nJSON types:\n\n- `integer` as milliseconds-since-epoch.\n- `string` as a any of Perspective's built-in date format formats.\n- JavaScript `Date` and Python `datetime.date` and `datetime.datetime` are _not_\n  supported directly. However, in JavaScript `Date` types are automatically\n  coerced to correct `integer` timestamps by default when converted to JSON."},{"title":"loading_data » Loading data » Apache Arrow","path":"loading_data.md","text":"The most efficient way to load data into Perspective, encoded as\n[Apache Arrow IPC format](https://arrow.apache.org/docs/python/ipc.html). In\nJavaScript:\n\n```javascript\nconst resp = await fetch(\n    \"https://cdn.jsdelivr.net/npm/superstore-arrow/superstore.lz4.arrow\",\n);\n\nconst arrow = await resp.arrayBuffer();\n```\n\nApache Arrow input do not support type coercion, preferring Arrow's internal\nself-describing schema.\n\nloading_data » Loading data » CSV\n\nPerspective relies on Apache Arrow's CSV parser, and as such uses mostly the\nsame column-type inference logic as Arrow itself would use for parsing CSV."},{"title":"loading_data » Loading data » Row Oriented JSON","path":"loading_data.md","text":"Row-oriented JSON is in the form of a list of objects. Each object in the list\ncorresponds to a row in the table. For example:\n\n```json\n[\n    { \"a\": 86, \"b\": false, \"c\": \"words\" },\n    { \"a\": 0, \"b\": true, \"c\": \"\" },\n    { \"a\": 12345, \"b\": false, \"c\": \"here\" }\n]\n```\n\nloading_data » Loading data » Column Oriented JSON\n\nColumn-Oriented JSON comes in the form of an object of lists. Each key of the\nobject is a column name, and each element of the list is the corresponding value\nin the row.\n\n```json\n{\n    \"a\": [86, 0, 12345],\n    \"b\": [false, true, false],\n    \"c\": [\"words\", \"\", \"here\"]\n}\n```"},{"title":"loading_data » Loading data » NDJSON","path":"loading_data.md","text":"[NDJSON](https://github.com/ndjson/ndjson-spec) (sometimes also referred to as\nJSONL) is a streaming-friendly format where each line is a valid JSON object,\nseparated by newlines. It is commonly used in data streaming and messaging\nqueues.\n\n```json\n{ \"a\": 86, \"b\": false, \"c\": \"words\" }\n{ \"a\": 0, \"b\": true, \"c\": \"\" }\n{ \"a\": 12345, \"b\": false, \"c\": \"here\" }\n```"},{"title":"options » Index and Limit","path":"options.md","text":"<div class=\"warning\">`limit` cannot be used in conjunction with `index`.</div>\n\nInitializing a `Table` with an `index` tells Perspective to treat a column as\nthe primary key, allowing in-place updates of rows. Only a single column (of any\ntype) can be used as an `index`. Indexed `Table` instances allow:\n\n-   In-place _updates_ whenever a new row shares an `index` values with an\n    existing row\n-   _Partial updates_ when a data batch omits some column.\n-   _Removes_ to delete a row by `index`.\n\nTo create an indexed `Table`, provide the `index` property with a string column\nname to be used as an index:\n\n<div class=\"javascript\">\n\nJavaScript:\n\n```javascript\nconst indexed_table = await perspective.table(data, { index: \"a\" });\n```\n\n</div>\n<div class=\"python\">\n\nPython\n\n```python\nindexed_table = perspective.Table(data, index=\"a\");\n```\n\n</div>\n\nInitializing a `Table` with a `limit` sets the total number of rows the `Table`\nis allowed to have. When the `Table` is updated, and the resulting size of the\n`Table` would exceed its `limit`, rows that exceed `limit` overwrite the oldest\nrows in the `Table`. To create a `Table` with a `limit`, provide the `limit`\nproperty with an integer indicating the maximum rows:\n\n<div class=\"javascript\">\n\nJavaScript:\n\n```javascript\nconst limit_table = await perspective.table(data, { limit: 1000 });\n```\n\n</div>\n<div class=\"python\">\n\nPython:\n\n```python\nlimit_table = perspective.Table(data, limit=1000);\n```\n\n</div>"},{"title":"schema » Schema and column types","path":"schema.md","text":"The mapping of a `Table`'s column names to data types is referred to as a\n`schema`. Each column has a unique name and a single data type, one of\n\n-   `float`\n-   `integer`\n-   `boolean`\n-   `date`\n-   `datetime`\n-   `string`\n\nA `Table` schema is fixed at construction, either by explicitly passing a schema\ndictionary to the `Client::table` method, or by passing _data_ to this method\nfrom which the schema is _inferred_ (if CSV or JSON format) or inherited (if\nArrow).\n\nschema » Schema and column types » Arrow type mapping\n\nPerspective's six column types are narrower than Arrow's type system, so Arrow\ninput is mapped on ingest:\n\n| Arrow type | Perspective type |\n| --- | --- |\n| `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, `uint32`, `uint64` | `integer` |\n| `float`, `double` | `float` |\n| `decimal`, `decimal128` | `float` |\n| `bool` | `boolean` |\n| `date32`, `date64` | `date` |\n| `timestamp` | `datetime` |\n| `time32`, `time64` | `integer` |\n| `utf8`, `large_utf8`, `binary`, `dictionary`, `list`, `null` | `string` |\n\nTwo mappings are worth calling out:\n\n- Arrow `decimal` columns become `float` — a `DECIMAL` value of `3.14` reads\n  as `3.14`, not as its unscaled integer representation.\n- Arrow `time32`/`time64` (a time-of-day with no date component) becomes\n  `integer`, not `datetime`. Use a `timestamp` column for a true `datetime`.\n\nArrow types not listed above — including `decimal256` and the nested types —\nare rejected with an error rather than silently coerced.\n\nArrow input is fully validated before its buffers are read. A malformed IPC\npayload — bad offsets, out-of-range dictionary indices, inconsistent chunk\nlengths — is rejected with an error rather than producing corrupt data."},{"title":"schema » Schema and column types » Type inference","path":"schema.md","text":"When passing CSV or JSON data to the `Client::table` constructor, the type of\neach column is inferred automatically. In some cases, the inference algorithm\nmay not return exactly what you'd like. For example, a column may be interpreted\nas a `datetime` when you intended it to be a `string`, or a column may have no\nvalues at all (yet), as it will be updated with values from a real-time data\nsource later on. In these cases, create a `table()` with a _schema_.\n\nOnce the `Table` has been created, further `Table::update` calls will perform\nlimited type _coercion_ based on the schema. While _coercion_ works similarly to\n_inference_, in that input data may be parsed based on the expected column type,\n`Table::update` will not _change_ the column's type further. For example, a\nnumber literal `1234` would be _inferred_ as an `\"integer\"`, but _in the context\nof an `Table::update` call on a known `\"string\"` column_, this will be parsed as\nthe _string_ `\"1234\"`."},{"title":"schema » Schema and column types » date and datetime inference","path":"schema.md","text":"Various string representations of `date` and `datetime` format columns can be\n_inferred_ as well _coerced_ from strings if they match one of Perspective's\ninternal known datetime parsing formats, for example\n[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) (which is also the format\nPerspective will _output_ these types for CSV)."},{"title":"update_and_remove » Table::update and Table::remove","path":"update_and_remove.md","text":"Once a `Table` has been created, it can be updated with new data conforming to\nthe `Table`'s schema. `Table::update` supports the same data formats as\n`Client::table`, minus _schema_.\n\n<div class=\"javascript\">\n\n```javascript\nconst schema = {\n    a: \"integer\",\n    b: \"float\",\n};\n\nconst table = await perspective.table(schema);\ntable.update(new_data);\n```\n\n</div>\n<div class=\"python\">\n\n```python\nschema = {\"a\": \"integer\", \"b\": \"float\"}\n\ntable = perspective.Table(schema)\ntable.update(new_data)\n```\n\n</div>\n\nWithout an `index` set, calls to `update()` _append_ new data to the end of the\n`Table`. Otherwise, Perspective allows\n[_partial updates_ (in-place)](#index-and-limit) using the `index` to determine\nwhich rows to update:\n\n<div class=\"javascript\">\n\n```javascript\nindexed_table.update({ id: [1, 4], name: [\"x\", \"y\"] });\n```\n\n</div>\n<div class=\"python\">\n\n```python\nindexed_table.update({\"id\": [1, 4], \"name\": [\"x\", \"y\"]})\n```\n\n</div>\n\nAny value on a `Client::table` can be unset using the value `null` in JSON or\nArrow input formats. Values may be unset on construction, as any `null` in the\ndataset will be treated as an unset value. `Table::update` calls do not need to\nprovide _all columns_ in the `Table`'s schema; missing columns will be omitted\nfrom the `Table`'s updated rows.\n\n<div class=\"javascript\">\n\n```javascript\ntable.update([{ x: 3, y: null }]); // `z` missing\n```\n\n</div>\n<div class=\"python\">\n\n```python\ntable.update([{\"x\": 3, \"y\": None}])  # `z` missing\n```\n\n</div>\n\nRows can also be removed from an indexed `Table`, by calling `Table::remove`\nwith an array of index values:\n\n<div class=\"javascript\">\n\n```javascript\nindexed_table.remove([1, 4]);\n```\n\n</div>\n<div class=\"python\">\n\n```python\nindexed_table.remove([1, 4])\n```\n\n</div>"},{"title":"table » Table","path":"table.md","text":"`Table` is Perspective's columnar data frame, analogous to a Pandas `DataFrame`\nor Apache Arrow, supporting append & in-place updates, removal by index, and\nupdate notifications.\n\nA `Table` contains columns, each of which have a unique name, are strongly and\nconsistently typed, and contains rows of data conforming to the column's type.\nEach column in a `Table` must have the same number of rows, though not every row\nmust contain data; null-values are used to indicate missing values in the\ndataset. The schema of a `Table` is _immutable after creation_, which means the\ncolumn names and data types cannot be changed after the `Table` has been\ncreated. Columns cannot be added or deleted after creation either, but a `View`\ncan be used to select an arbitrary set of columns from the `Table`."},{"title":"advanced » Advanced View Operations","path":"advanced.md","text":"Beyond the standard query configuration, `View` provides additional methods for\ninteracting with hierarchical results and introspecting data.\n\nadvanced » Advanced View Operations » Tree Hierarchy Operations\n\nWhen a `View` has `group_by` applied, the results form a tree hierarchy.\nPerspective provides methods to control which levels of the tree are expanded or\ncollapsed:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({ group_by: [\"Region\", \"Country\", \"City\"] });\n\n// Collapse the tree at row index 5\nawait view.collapse(5);\n\n// Expand the tree at row index 5\nawait view.expand(5);\n\n// Set the expansion depth (0 = fully collapsed, 1 = first level, etc.)\nawait view.set_depth(1);\n```\n\n</div>\n<div class=\"python\">\n\nUsing the sync API\n\n```python\nview = table.view(group_by=[\"Region\", \"Country\", \"City\"])\n\nview.collapse(5)\nview.expand(5)\nview.set_depth(1)\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    group_by: Some(vec![\"Region\".into(), \"Country\".into(), \"City\".into()]),\n    ..ViewConfigUpdate::default()\n})).await?;\n\nview.collapse(5).await?;\nview.expand(5).await?;\nview.set_depth(1).await?;\n```\n\n</div>\n\n<span class=\"warning\">Perspective's built-in engine is lazy — aggregates for\ncollapsed rows are not recalculated when the underlying `Table` is updated.\nUpdates are only computed for rows that are currently visible (expanded). When a\ncollapsed row is later expanded, its aggregates are calculated at that\npoint.</span>"},{"title":"advanced » Advanced View Operations » Column Range Queries","path":"advanced.md","text":"`View::get_min_max` returns the minimum and maximum values for a given column,\nwhich is useful for setting up scales in custom visualizations:\n\n<div class=\"javascript\">\n\n```javascript\nconst [min, max] = await view.get_min_max(\"Sales\");\n```\n\n</div>\n<div class=\"python\">\n\n```python\nmin_val, max_val = view.get_min_max(\"Sales\")\n```\n\n</div>\n\nadvanced » Advanced View Operations » Expression Validation\n\nBefore creating a `View` with expressions, you can validate them against the\ntable's schema using `Table::validate_expressions`. This returns information\nabout which expressions are valid and their inferred types:\n\n<div class=\"javascript\">\n\n```javascript\nconst result = await table.validate_expressions({\n    expr1: '\"Sales\" + \"Profit\"',\n    expr2: \"invalid_column + 1\",\n});\n// result.expression_schema contains valid expressions and their types\n// result.errors contains invalid expressions and error messages\n```\n\n</div>\n<div class=\"python\">\n\n```python\nresult = table.validate_expressions(['\"Sales\" + \"Profit\"', 'invalid + 1'])\n```\n\n</div>"},{"title":"advanced » Advanced View Operations » View Dimensions","path":"advanced.md","text":"`View::dimensions` returns the number of rows and columns in the current view,\nincluding information about group-by header rows:\n\n<div class=\"javascript\">\n\n```javascript\nconst dims = await view.dimensions();\n// { num_view_rows, num_view_columns, num_table_rows, num_table_columns, ... }\n```\n\n</div>\n<div class=\"python\">\n\n```python\ndims = view.dimensions()\n```\n\n</div>\n\nadvanced » Advanced View Operations » View Configuration Introspection\n\n`View::get_config` returns the full configuration used to create the view:\n\n<div class=\"javascript\">\n\n```javascript\nconst config = await view.get_config();\n// { group_by: [...], split_by: [...], sort: [...], filter: [...], ... }\n```\n\n</div>\n<div class=\"python\">\n\n```python\nconfig = view.get_config()\n```\n\n</div>"},{"title":"advanced » Advanced View Operations » Update Callbacks","path":"advanced.md","text":"Register a callback to be notified whenever the underlying `Table` is updated\nand the `View` has been recalculated:\n\n<div class=\"javascript\">\n\n```javascript\nview.on_update(\n    (updated) => {\n        console.log(\"View updated\", updated.port_id);\n    },\n    { mode: \"row\" },\n);\n\n// Later, remove the callback\nview.remove_update(callback);\n```\n\n</div>\n<div class=\"python\">\n\n```python\ndef on_update(port_id, delta):\n    print(\"View updated\", port_id)\n\nview.on_update(on_update, mode=\"row\")\nview.remove_update(on_update)\n```\n\n</div>\n\nWhen `mode` is set to `\"row\"`, the callback receives a delta of only the rows\nthat changed (as Apache Arrow), which is useful for efficiently synchronizing\ntables across clients."},{"title":"advanced » Advanced View Operations » Flattening a View into a Table","path":"advanced.md","text":"A [`Table`] can be constructed on a [`Table::view`] instance, which will return\na new [`Table`] based on the [`Table::view`]'s dataset, and all future updates\nthat affect the [`Table::view`] will be forwarded to the new [`Table`]. This is\nparticularly useful for implementing a\n[Client/Server Replicated](../architecture/client_server.md) design, as it\nhandles the `View` serialization and `on_update` forwarding for you. This\npattern is available in JavaScript, Python and Rust.\n\n<div class=\"javascript\">\n\n```javascript\nconst worker = await perspective.worker();\nconst table = await worker.table(data);\nconst view = await table.view({ filter: [[\"State\", \"==\", \"Texas\"]] });\nconst table2 = await worker.table(view);\ntable.update([{ State: \"Texas\", City: \"Austin\" }]);\n```\n\n</div>\n<div class=\"python\">\n\n```python\ntable = client.table(data)\nview = table.view(filter=[[\"State\", \"==\", \"Texas\"]])\ntable2 = client.table(view)\ntable.update([{\"State\": \"Texas\", \"City\": \"Austin\"}])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet opts = TableInitOptions::default();\nlet data = TableData::Update(UpdateData::Csv(\"x,y\\n1,2\\n3,4\".into()));\nlet table = client.table(data, opts).await?;\nlet view = table.view(None).await?;\nlet table2 = client.table(TableData::View(view)).await?;\ntable.update(data).await?;\n```\n\n</div>"},{"title":"expressions » Expressions","path":"expressions.md","text":"The `expressions` property specifies _new_ columns in Perspective that are\ncreated using existing column values or arbitrary scalar values defined within\nthe expression. In `<perspective-viewer>`, expressions are added using the \"New\nColumn\" button in the side panel.\n\nExpressions are strings parsed by Perspective's expression engine (based on\n[ExprTK](https://github.com/ArashPartow/exprtk)). Column names are referenced by\nwrapping them in double quotes, e.g. `\"Sales\"`:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    expressions: {\n        \"Profit Ratio\": '\"Profit\" / \"Sales\"',\n    },\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(expressions={'Profit Ratio': '\"Profit\" / \"Sales\"'})\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    expressions: Some(Expressions([\n        (\"Profit Ratio\", \"\\\"Profit\\\" / \\\"Sales\\\"\".into())\n    ].into_iter().collect())),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"expressions » Expressions » Type Conversion and Coercion","path":"expressions.md","text":"Perspective expressions are strongly typed — each column and literal has a fixed\ntype, and most operators require matching types on both sides. To work across\ntypes, use the conversion functions:\n\n| Function        | Description                                                  |\n| --------------- | ------------------------------------------------------------ |\n| `to_string(x)`  | Convert any type to string                                   |\n| `to_integer(x)` | Convert to integer (null if not parsable)                    |\n| `to_float(x)`   | Convert to float (null if not parsable)                      |\n| `to_boolean(x)` | Convert to boolean (truthy/falsy)                            |\n| `integer(x)`    | Alias for `to_integer(x)`                                    |\n| `float(x)`      | Alias for `to_float(x)`                                      |\n| `datetime(x)`   | Construct a datetime from a POSIX timestamp (ms since epoch) |\n| `date(y, m, d)` | Construct a date from year, month, day                       |"},{"title":"expressions » Expressions » Type Conversion and Coercion » How coercion works","path":"expressions.md","text":"Perspective does not implicitly coerce types. For example, you cannot directly\nadd an `integer` to a `float` — you must cast one side explicitly. Similarly,\n`datetime` and `date` values are not numeric: to perform arithmetic on them, you\nmust first convert to a numeric representation, do the math, then convert back.\n\nInternally, `datetime` values are stored as milliseconds since the Unix epoch\n(1970-01-01T00:00:00Z). Converting a `datetime` to a `float` yields this\nmillisecond timestamp, and `datetime()` accepts a millisecond timestamp to\nproduce a `datetime`."},{"title":"expressions » Expressions » Type Conversion and Coercion » Example: offsetting a datetime by 7 days","path":"expressions.md","text":"This expression takes a `\"Shipped Date\"` column, converts it to its\nmillisecond-epoch representation, adds 7 days worth of milliseconds (7 &times;\n24 &times; 60 &times; 60 &times; 1000 = 604800000), and converts the result back\nto a `datetime`:\n\n```\n// Due Date\ndatetime(float(\"Shipped Date\") + 604800000)\n```\n\nexpressions » Expressions » Operators\n\nStandard arithmetic and comparison operators are supported:\n\n| Operator                         | Description |\n| -------------------------------- | ----------- |\n| `+`, `-`, `*`, `/`               | Arithmetic  |\n| `%`                              | Modulo      |\n| `==`, `!=`, `<`, `>`, `<=`, `>=` | Comparison  |\n| `and`, `or`, `not`               | Logical     |\n| `if ... else ...`                | Conditional |"},{"title":"expressions » Expressions » Numeric Functions","path":"expressions.md","text":"ExprTK provides a rich set of built-in numeric functions including `abs`,\n`ceil`, `floor`, `round`, `exp`, `log`, `log10`, `sqrt`, `min`, `max`, `pow`,\n`clamp`, `iclamp`, `inrange`, and trigonometric functions (`sin`, `cos`, `tan`,\n`asin`, `acos`, `atan`).\n\nexpressions » Expressions » String Functions\n\n| Function                        | Description                                             |\n| ------------------------------- | ------------------------------------------------------- |\n| `concat(a, b, ...)`             | Concatenate strings                                     |\n| `upper(s)`                      | Convert to uppercase                                    |\n| `lower(s)`                      | Convert to lowercase                                    |\n| `length(s)`                     | String length                                           |\n| `contains(s, substr)`           | Whether `s` contains `substr`                           |\n| `order(col, 'B', 'C', 'A')`     | Custom sort order for a string column                   |\n| `match(s, pattern)`             | Regex partial match (returns boolean)                   |\n| `match_all(s, pattern)`         | Regex full match (returns boolean)                      |\n| `search(s, pattern)`            | First capturing group match                             |\n| `indexof(s, pattern)`           | Start index of first regex match                        |\n| `substring(s, start, end)`      | Substring from `start` (inclusive) to `end` (exclusive) |\n| `replace(s, repl, pattern)`     | Replace first regex match                               |\n| `replace_all(s, repl, pattern)` | Replace all regex matches                               |"},{"title":"expressions » Expressions » Date/Datetime Functions","path":"expressions.md","text":"| Function                 | Description                                                              |\n| ------------------------ | ------------------------------------------------------------------------ |\n| `today()`                | Current date                                                             |\n| `now()`                  | Current datetime                                                         |\n| `date(year, month, day)` | Construct a date                                                         |\n| `datetime(timestamp_ms)` | Construct a datetime from a POSIX timestamp (ms since epoch)             |\n| `hour_of_day(dt)`        | Hour component (0-23)                                                    |\n| `day_of_week(dt)`        | Day of the week as a string                                              |\n| `month_of_year(dt)`      | Month of the year as a string                                            |\n| `bucket(dt, unit)`       | Bucket datetime by unit: `'s'`, `'m'`, `'h'`, `'D'`, `'W'`, `'M'`, `'Y'` |\n\n`bucket` also works on numeric columns: `bucket(\"Price\", 10)` rounds values down\nto the nearest multiple of 10."},{"title":"expressions » Expressions » Other Functions","path":"expressions.md","text":"| Function                  | Description                                           |\n| ------------------------- | ----------------------------------------------------- |\n| `is_null(x)`              | Whether the value is null                             |\n| `is_not_null(x)`          | Whether the value is not null                         |\n| `percent_of(a, b)`        | `a` as a percentage of `b`                            |\n| `inrange(low, val, high)` | Whether `val` is between `low` and `high` (inclusive) |\n| `min(a, b, ...)`          | Minimum of inputs                                     |\n| `max(a, b, ...)`          | Maximum of inputs                                     |\n| `random()`                | Random float between 0.0 and 1.0                      |\n| `col(name)`               | Look up a column by string name at runtime            |\n| `vlookup(col, key)`       | Look up a value in another column by row key          |"},{"title":"expressions » Expressions » See also","path":"expressions.md","text":"Expressions are row-local — each output cell is computed from that row's\nvalues alone. For calculations which span rows, such as moving averages,\ncumulative sums or period-over-period differences, see\n[Window Columns](./windows.md)."},{"title":"grouping_and_pivots » Grouping and Pivots » Group By","path":"grouping_and_pivots.md","text":"A group by _groups_ the dataset by the unique values of each column used as a\ngroup by - a close analogue in SQL to the `GROUP BY` statement. The underlying\ndataset is aggregated to show the values belonging to each group, and a total\nrow is calculated for each group, showing the currently selected aggregated\nvalue (e.g. `sum`) of the column. Group by are useful for hierarchies,\ncategorizing data and attributing values, i.e. showing the number of units sold\nbased on State and City. In Perspective, group by are represented as an array of\nstring column names to pivot, are applied in the order provided; For example, a\ngroup by of `[\"State\", \"City\", \"Postal Code\"]` shows the values for each Postal\nCode, which are grouped by City, which are in turn grouped by State.\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({ group_by: [\"a\", \"c\"] });\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(group_by=[\"a\", \"c\"])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    group_by: Some(vec![\"a\".into(), \"c\".into()]),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Group By » group_rollup_mode","path":"grouping_and_pivots.md","text":"The `group_rollup_mode` option controls how the grouped rows themselves render:\n\n-   `\"rollup\"` (the default) - the full hierarchy, with a subtotal row for\n    every group at every level and a grand total row, each addressable by its\n    `__ROW_PATH__`.\n-   `\"flat\"` - leaf rows only, one row per deepest-level group, with no\n    subtotal or grand total rows. Useful for chart plugins and exports where\n    subtotal rows would double-count.\n-   `\"total\"` - the grand total row _only_. `\"total\"` is mutually exclusive\n    with `group_by` (which is cleared when it is set) - it is the one shape\n    an empty `group_by` cannot express, since no `group_by` at all yields the\n    unaggregated dataset.\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    group_by: [\"a\"],\n    group_rollup_mode: \"flat\",\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(group_by=[\"a\"], group_rollup_mode=\"flat\")\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    group_by: Some(vec![\"a\".into()]),\n    group_rollup_mode: Some(GroupRollupMode::Flat),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Split By","path":"grouping_and_pivots.md","text":"A split by _splits_ the dataset by the unique values of each column used as a\nsplit by. The underlying dataset is not aggregated, and a new column is created\nfor each unique value of the split by. Each newly created column contains the\nparts of the dataset that correspond to the column header, i.e. a `View` that\nhas `[\"State\"]` as its split by will have a new column for each state. In\nPerspective, Split By are represented as an array of string column names to\npivot:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({ split_by: [\"a\", \"c\"] });\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(split_by=[\"a\", \"c\"])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    split_by: Some(vec![\"a\".into(), \"c\".into()]),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Split By » split_rollup_mode","path":"grouping_and_pivots.md","text":"The `split_rollup_mode` option is the `split_by` counterpart to\n[`group_rollup_mode`](#group_rollup_mode), controlling whether subtotal\n_column groups_ are emitted:\n\n-   `\"flat\"` (the default) - only full-depth split combinations appear as\n    columns, e.g. `\"CA|Sales\"`. This is Perspective's historical behavior.\n-   `\"rollup\"` - additionally emits a grand-total column per aggregate (named\n    by the bare column name, e.g. `\"Sales\"`, aggregating across every split\n    group) and, when more than one `split_by` column is applied, a subtotal\n    column per intermediate split group (e.g. `\"CA|Sales\"` alongside\n    `\"CA|First Class|Sales\"`). Total and subtotal columns precede their\n    groups, in pre-order.\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    group_by: [\"State\"],\n    split_by: [\"Ship Mode\"],\n    split_rollup_mode: \"rollup\",\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(\n    group_by=[\"State\"],\n    split_by=[\"Ship Mode\"],\n    split_rollup_mode=\"rollup\",\n)\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    group_by: Some(vec![\"State\".into()]),\n    split_by: Some(vec![\"Ship Mode\".into()]),\n    split_rollup_mode: Some(SplitRollupMode::Rollup),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates","path":"grouping_and_pivots.md","text":"Aggregates perform a calculation over an entire column, and are displayed when\none or more [Group By](#group-by) are applied to the `View`. Aggregates can be\nspecified by the user, or Perspective will use the following sensible default\naggregates based on column type:\n\n-   \"sum\" for `integer` and `float` columns\n-   \"count\" for all other columns\n\nPerspective provides a selection of aggregate functions that can be applied to\ncolumns in the `View` constructor using a dictionary of column name to aggregate\nfunction name.\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    aggregates: {\n        a: \"avg\",\n        b: \"distinct count\",\n    },\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(\n  aggregates={\n    \"a\": \"avg\",\n    \"b\": \"distinct count\"\n  }\n)\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nuse std::collections::HashMap;\nlet view = table.view(Some(ViewConfigUpdate {\n    aggregates: Some(HashMap::from([\n        (\"a\".into(), \"avg\".into()),\n        (\"b\".into(), \"distinct count\".into()),\n    ])),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>\n\nEvery aggregate is described below, grouped by what it computes. Which of them\na given column accepts depends on its type — see\n[Availability by column type](#availability-by-column-type)."},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Sums and products","path":"grouping_and_pivots.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `sum` | Total of the group's values | `integer` or `float` |\n| `sum not null` | As `sum`, but non-finite (`NaN`) values are skipped rather than poisoning the total | `integer` or `float` |\n| `sum abs` | Sum of the absolute values — `Σ abs(v)` | `integer` or `float` |\n| `abs sum` | Absolute value of the sum — `abs(Σ v)` | `integer` or `float` |\n| `mul` | Product of the group's values | `integer` or `float` |\n| `gmv` | Gross market value — leaf rows are a plain `sum`, parent rows sum the _absolute_ subtotal of each immediate child group | `integer` or `float` |\n| `pct sum parent` | The group's `sum` as a percentage of its parent row's, `0`–`100`; `100` at the root, and `null` when the parent's sum is `0` | `float` |\n| `pct sum total` | The group's `sum` as a percentage of the grand total, `0`–`100` | `float` |\n\nA numeric aggregate widens to the input's numeric class — `integer` columns\naccumulate as `integer`, `float` columns as `float`."},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Averages and dispersion","path":"grouping_and_pivots.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `avg` | Arithmetic mean of the non-null values | `float` |\n| `weighted mean` | `Σ(value × weight) / Σ(weight)`, over rows where both the value and the weight are non-null and finite; `null` when the weights sum to `0`. Takes a **weight column** as an argument | `float` |\n| `stddev` | Population standard deviation | `float` |\n| `var` | Population variance — divides by `N`, not `N - 1` | `float` |\n\n`stddev` and `var` are `null` for a group of fewer than two non-null values."},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Extrema and order statistics","path":"grouping_and_pivots.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `min`, `max` | Smallest and largest of the group's current values | input type |\n| `min by`, `max by` | The value from the row at which a **second column**, supplied as an argument, is smallest or largest | input type |\n| `high`, `low` | High and low _water mark_ — the largest and smallest value this `View` has ever observed for the group, which never moves back when rows are updated or removed | input type |\n| `high minus low` | `max - min` of the group's current values, i.e. its range. Despite the name this uses `min`/`max`, not the water marks | input type |\n| `median`, `q1`, `q3` | The value at the 50%, 25% and 75% position of the group's values; on `float` columns an exact split averages the two adjacent values | input type |"},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Positional","path":"grouping_and_pivots.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `first` | Value from the group's earliest row | input type |\n| `last by index` | Value from the group's latest row | input type |\n| `last minus first` | `last by index` minus `first` | input type |\n| `last` | Value from the group's most recently _updated_ row | input type |\n\n\"Earliest\" and \"latest\" are by the `Table`'s `index` column, or by row order\nwhen the `Table` is unindexed. This is not the same as `last`, which tracks\nupdate recency rather than position."},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Cardinality and identity","path":"grouping_and_pivots.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `count` | Number of rows in the group | `integer` |\n| `distinct count` | Number of distinct values in the group | `integer` |\n| `unique` | The group's value when every row shares one, otherwise `null` | input type |\n| `distinct leaf` | As `unique`, but only on leaf rows — parent rows are blank | input type |\n| `dominant` | The most frequent non-null value, i.e. the mode; a tie resolves to whichever value reached the winning count first | input type |\n| `any` | The group's first _truthy_ value — any non-null value for `string` columns, the first non-zero for numbers and dates, the first `true` for `boolean` — or `null` if it has none | input type |\n| `or` | Identical to `any` | input type |\n| `and` | `true` when every value in the group is truthy, else `false` | `boolean` |\n| `join` | The group's distinct values, sorted and rendered as a `\", \"`-delimited string, truncated at 280 characters. Nulls render as `null` | `string` |"},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Nulls","path":"grouping_and_pivots.md","text":"Null handling is not uniform, and is usually what makes two similar-looking\naggregates differ:\n\n- `count` counts **rows**, not values — a group of 3 rows whose value is\n  `null` counts `3`. This is not the same as the `count` [window\n  aggregate](./windows.md#aggregates), which counts non-null values.\n- `distinct count` counts `null` as **one distinct value**, so a group of\n  `[1, null, null]` counts `2`.\n- `sum`, `avg`, `stddev`, `var`, `dominant` and `weighted mean` skip nulls\n  entirely. `avg` divides by the count of non-null values, so a group of all\n  nulls is `null` rather than `0`.\n- `any` and `or` return the first _truthy_ value, not the first non-null one —\n  a numeric group of all `0`, or a `boolean` group of all `false`, aggregates\n  to `null`.\n- `join` renders nulls into its output as the literal text `null`."},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Availability by column type","path":"grouping_and_pivots.md","text":"The aggregates a column accepts depend on its type:\n\n**Numeric columns** (`integer`, `float`): `sum`, `abs sum`, `sum abs`,\n`sum not null`, `mul`, `gmv`, `any`, `avg`, `mean`, `count`, `distinct count`,\n`distinct leaf`, `dominant`, `first`, `last`, `last by index`, `high`, `low`,\n`max`, `min`, `min by`, `max by`, `high minus low`, `last minus first`,\n`median`, `q1`, `q3`, `pct sum parent`, `pct sum total`, `stddev`, `var`,\n`unique`, `weighted mean`.\n\n**String columns**: `count`, `any`, `distinct count`, `distinct leaf`,\n`dominant`, `first`, `last`, `last by index`, `join`, `median`, `q1`, `q3`,\n`unique`, `min by`, `max by`.\n\n**Date/Datetime columns**: `count`, `any`, `avg`, `distinct count`,\n`distinct leaf`, `dominant`, `first`, `last`, `last by index`, `high`, `low`,\n`max`, `min`, `median`, `q1`, `q3`, `unique`.\n\n**Boolean columns**: `count`, `any`, `and`, `or`, `distinct count`,\n`distinct leaf`, `dominant`, `first`, `last`, `last by index`, `unique`.\n\n<div class=\"warning\"><code>avg</code> on a <code>date</code> or\n<code>datetime</code> column returns a <code>float</code> — the mean of the\ncolumn's underlying numeric representation — not a date.</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Argument-taking aggregates","path":"grouping_and_pivots.md","text":"`weighted mean`, `min by` and `max by` each read a second column, and are\nwritten as a `[name, [argument]]` pair rather than a bare string:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    aggregates: { a: [\"weighted mean\", [\"b\"]] },\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(aggregates={\"a\": (\"weighted mean\", [\"b\"])})\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    aggregates: Some(HashMap::from([(\n        \"a\".into(),\n        Aggregate::MultiAggregate(\"weighted mean\".into(), vec![\"b\".into()]),\n    )])),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>\n\n<div class=\"warning\">In Rust, <code>Aggregate::from(&amp;str)</code> splits on\n<code>\" by \"</code> to build a <code>MultiAggregate</code>. Single aggregates\nwhose names contain that substring — <code>\"last by index\"</code> — must\ntherefore be constructed as\n<code>Aggregate::SingleAggregate(\"last by index\".into())</code> rather than\n<code>\"last by index\".into()</code>, which silently resolves to\n<code>last</code>.</div>"},{"title":"grouping_and_pivots » Grouping and Pivots » Aggregates » Aliases","path":"grouping_and_pivots.md","text":"Several aggregates answer to more than one name. Every name below is accepted\nanywhere an aggregate is, and each group refers to one function:\n\n| Canonical | Also accepted |\n| --- | --- |\n| `avg` | `mean` |\n| `distinct count` | `distinct`, `distinctcount`, `distinct_count` |\n| `first` | `first by index` |\n| `last` | `last_value` |\n| `high` | `high_water_mark` |\n| `low` | `low_water_mark` |\n| `pct sum total` | `pct sum grand total`, `pct_sum_grand_total` |\n| `var` | `variance` |\n| `stddev` | `standard deviation` |\n\nMost multi-word aggregates also answer to a snake_case spelling —\n`sum_not_null`, `sum_abs`, `abs_sum`, `weighted_mean`, `distinct_leaf`,\n`pct_sum_parent`, `pct_sum_total`, `min_by`, `max_by`. Three do not, and are\nonly accepted spelled with spaces: `high minus low`, `last minus first` and\n`last by index`.\n\nA few names the engine parses are _not implemented_ — `identity`,\n`mean by count`, and `div`/`add`, which have no way to receive their operands\nfrom a `ViewConfig`. Naming one is rejected exactly as a misspelled aggregate\nis: the `View` fails to construct with an error naming the aggregate and the\ncolumn it was given for, and the `Table` is left untouched."},{"title":"selection_and_ordering » Selection and Ordering » Columns","path":"selection_and_ordering.md","text":"The `columns` property specifies which columns should be included in the\n`View`'s output. This allows users to show or hide a specific subset of columns,\nas well as control the order in which columns appear to the user. This is\nrepresented in Perspective as an array of string column names:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    columns: [\"a\"],\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(columns=[\"a\"])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    columns: Some(vec![Some(\"a\".into())]),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>"},{"title":"selection_and_ordering » Selection and Ordering » Sort","path":"selection_and_ordering.md","text":"The `sort` property specifies columns on which the query should be sorted,\nanalogous to `ORDER BY` in SQL. A column can be sorted regardless of its data\ntype, and sorts can be applied in ascending or descending order. Perspective\nrepresents `sort` as an array of arrays, with the values of each inner array\nbeing a string column name and a string sort direction. When `split_by` are\napplied, the additional sort directions `\"col asc\"` and `\"col desc\"` will\ndetermine the order of pivot column groups.\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    sort: [[\"a\", \"asc\"]],\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(sort=[[\"a\", \"asc\"]])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    sort: Some(vec![Sort(\"a\".into(), SortDir::Asc)]),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>\n\nThe available sort directions are:\n\n| Direction | Description |\n|---|---|\n| `\"asc\"` | Ascending order |\n| `\"desc\"` | Descending order |\n| `\"asc abs\"` | Ascending by absolute value |\n| `\"desc abs\"` | Descending by absolute value |\n| `\"col asc\"` | Ascending order for pivot column groups (requires `split_by`) |\n| `\"col desc\"` | Descending order for pivot column groups (requires `split_by`) |\n| `\"col asc abs\"` | Ascending by absolute value for pivot column groups |\n| `\"col desc abs\"` | Descending by absolute value for pivot column groups |"},{"title":"selection_and_ordering » Selection and Ordering » Filter","path":"selection_and_ordering.md","text":"The `filter` property specifies columns on which the query can be filtered,\nreturning rows that pass the specified filter condition. This is analogous to\nthe `WHERE` clause in SQL. There is no limit on the number of columns where\n`filter` is applied, but the resulting dataset is one that passes all the filter\nconditions, i.e. the filters are joined with an `AND` condition. The join\ncondition can be changed to `OR` via the `filter_op` property.\n\nPerspective represents `filter` as an array of arrays, with the values of each\ninner array being a string column name, a string filter operator, and a filter\noperand in the type of the column:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    filter: [[\"a\", \"<\", 100]],\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(filter=[[\"a\", \"<\", 100]])\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet view = table.view(Some(ViewConfigUpdate {\n    filter: Some(vec![Filter::new(\"a\", \"<\", FilterTerm::Scalar(Scalar::Float(100.0)))]),\n    ..ViewConfigUpdate::default()\n})).await?;\n```\n\n</div>\n\nThe available filter operators depend on the column type:\n\n**String columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `begins with`,\n`not begins with`, `contains`, `not contains`, `ends with`, `not ends with`,\n`matches`, `not matches`, `in`, `not in`, `is not null`, `is null`.\n\nThe string matching operators (`begins with`, `contains`, `ends with` and\ntheir negations) are case-insensitive, and `matches` / `not matches` are\ncase-sensitive partial-match [RE2](https://github.com/google/re2) regular\nexpressions. Null cells match none of these operators, including the negated\nforms - filter on `is null` to select them.\n\n**Numeric columns** (`integer`, `float`): `==`, `!=`, `>`, `>=`, `<`, `<=`,\n`is not null`, `is null`.\n\n**Boolean columns**: `==`, `is not null`, `is null`.\n\n**Date/Datetime columns**: `==`, `!=`, `>`, `>=`, `<`, `<=`, `is not null`,\n`is null`."},{"title":"windows » Window Columns","path":"windows.md","text":"The `windows` property declares _ordered, partitioned rolling computations_\nover the rows of a `Table` — moving averages, cumulative sums,\nperiod-over-period differences — analogous to SQL window functions.\n\nWindow Columns are declared per-`View`, keyed by output alias, exactly as\n[`expressions`](./expressions.md) are:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    columns: [\"10-tick avg Sales\"],\n    windows: {\n        \"10-tick avg Sales\": {\n            column: \"Sales\",\n            aggregate: \"avg\",\n            rows: 10,\n        },\n    },\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(\n    columns=[\"10-tick avg Sales\"],\n    windows={\n        \"10-tick avg Sales\": {\n            \"column\": \"Sales\",\n            \"aggregate\": \"avg\",\n            \"rows\": 10,\n        }\n    },\n)\n```\n\n</div>\n\nEach window produces a new column which may be used anywhere a `Table` column\ncan — in `columns`, `filter`, `sort`, `group_by`, and so on. An alias must not\ncollide with a `Table` column, an expression alias, or another window's key.\n\nWindow Columns update incrementally as the `Table` updates, including rows\n_outside_ an update batch whose window frames were affected by it."},{"title":"windows » Window Columns » Spec fields","path":"windows.md","text":"| Field | Type | Description |\n| --- | --- | --- |\n| `column` | `string` | The input column — a `Table` column or an expression alias from the same config |\n| `aggregate` | `string` | The window function to apply (see below) |\n| `partition_by` | `string[]` | Columns whose distinct value tuples partition the rows; omitted partitions the whole `Table` as one group |\n| `order_by` | `[string, \"asc\" \\| \"desc\"]` | The column which orders each partition, and its direction |\n| `rows` | `integer` | Frame of the N rows preceding each row, plus the row itself |\n| `range` | `number` | Frame of rows whose `order_by` value lies within `range` of each row's |\n| `cumulative` | `true` | Frame of all rows from the partition start through each row |\n| `offset` | `integer` | Row offset for `lag`/`lead` (default `1`) |\n| `alpha` | `number` | Smoothing factor in `(0, 1]` for `ema` |\n\n`rows`, `range` and `cumulative` are **mutually exclusive** — supplying more\nthan one is an error. `range` requires a numeric or temporal `order_by`.\n\n<div class=\"warning\"><code>order_by</code> orders rows <em>within the window\nframe</em> only. It does not reorder the <code>View</code> — that is what the\nview-level <a href=\"./selection_and_ordering.md#sort\"><code>sort</code></a>\nproperty does.</div>"},{"title":"windows » Window Columns » Aggregates","path":"windows.md","text":"| Aggregate | Description | Result type |\n| --- | --- | --- |\n| `sum`, `avg` | Rolling sum and mean over the frame | `float` |\n| `stddev`, `var` | Rolling standard deviation and variance | `float` |\n| `count` | Number of non-null values in the frame | `integer` |\n| `min`, `max` | Smallest and largest value in the frame | input type |\n| `lag`, `lead` | Value `offset` rows behind or ahead | input type |\n| `diff` | This row's value minus the value `offset` rows behind | `float` |\n| `rate` | Rate of change across the frame | `float` |\n| `ema` | Exponential moving average, smoothed by `alpha` | `float` |\n\n`sum`, `avg`, `stddev`, `var`, `diff`, `rate` and `ema` require a numeric\ninput column."},{"title":"windows » Window Columns » Aggregates » Frame compatibility","path":"windows.md","text":"- `sum`, `avg`, `count`, `min`, `max`, `stddev` and `var` accept any frame.\n- `lag`, `lead`, `diff` and `ema` are frame-independent — they are computed\n  from row offsets rather than a frame.\n- **`rate` requires a `range` frame**, and is invalid with `rows` or\n  `cumulative`.\n\n<div class=\"warning\">The <code>first</code> and <code>last</code> window\naggregates are declared in the type definitions but are <em>not yet\nimplemented</em> by the engine; a <code>View</code> which uses them will be\nrejected.</div>"},{"title":"windows » Window Columns » Examples » Moving average over a fixed row count","path":"windows.md","text":"A 10-tick moving average, over the whole table in its natural order:\n\n```json\n{\n    \"columns\": [\"10-tick avg Sales\"],\n    \"windows\": {\n        \"10-tick avg Sales\": {\n            \"column\": \"Sales\",\n            \"aggregate\": \"avg\",\n            \"rows\": 10\n        }\n    }\n}\n```\n\nwindows » Window Columns » Examples » Moving average over a time range\n\nA 5-second moving average, framing rows by their `Order Date` rather than by\ncount:\n\n```json\n{\n    \"columns\": [\"5s avg Sales\"],\n    \"windows\": {\n        \"5s avg Sales\": {\n            \"column\": \"Sales\",\n            \"aggregate\": \"avg\",\n            \"order_by\": [\"Order Date\", \"asc\"],\n            \"range\": 5000\n        }\n    }\n}\n```"},{"title":"windows » Window Columns » Examples » Cumulative sum","path":"windows.md","text":"A running total from the start of each partition:\n\n```json\n{\n    \"columns\": [\"Cumulative Sales\"],\n    \"windows\": {\n        \"Cumulative Sales\": {\n            \"column\": \"Sales\",\n            \"aggregate\": \"sum\",\n            \"order_by\": [\"Order Date\", \"asc\"],\n            \"cumulative\": true\n        }\n    }\n}\n```\n\nwindows » Window Columns » Examples » Period-over-period change, per group\n\n`partition_by` restarts the window at each new `Region`, so each region's\nfirst row has no predecessor to difference against:\n\n```json\n{\n    \"columns\": [\"Region\", \"Sales\", \"Sales Δ\"],\n    \"windows\": {\n        \"Sales Δ\": {\n            \"column\": \"Sales\",\n            \"aggregate\": \"diff\",\n            \"partition_by\": [\"Region\"],\n            \"order_by\": [\"Order Date\", \"asc\"]\n        }\n    }\n}\n```"},{"title":"windows » Window Columns » Support","path":"windows.md","text":"Window Columns are implemented by Perspective's built-in engine, by the\nDuckDB, ClickHouse and Polars\n[Virtual Servers](../../virtual_servers.md), and by the\n`<perspective-viewer>` UI. Virtual Servers advertise support through their\n_features_ declaration, so the UI control is hidden for backends which do not\nimplement it."},{"title":"querying » Querying data","path":"querying.md","text":"To query the table, create a [`Table::view`] on the table instance with an\noptional configuration object. A [`Table`] can have as many [`View`]s associated\nwith it as you need - Perspective conserves memory by relying on a single\n[`Table`] to power multiple [`View`]s concurrently:\n\n<div class=\"javascript\">\n\n```javascript\nconst view = await table.view({\n    columns: [\"Sales\"],\n    aggregates: { Sales: \"sum\" },\n    group_by: [\"Region\", \"Country\"],\n    filter: [[\"Category\", \"in\", [\"Furniture\", \"Technology\"]]],\n});\n```\n\n</div>\n<div class=\"python\">\n\n```python\nview = table.view(\n  columns=[\"Sales\"],\n  aggregates={\"Sales\": \"sum\"},\n  group_by=[\"Region\", \"Country\"],\n  filter=[[\"Category\", \"in\", [\"Furniture\", \"Technology\"]]]\n)\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nuse crate::config::*;\nlet view = table\n    .view(Some(ViewConfigUpdate {\n        columns: Some(vec![Some(\"Sales\".into())]),\n        aggregates: Some(HashMap::from_iter(vec![(\"Sales\".into(), \"sum\".into())])),\n        group_by: Some(vec![\"Region\".into(), \"Country\".into()]),\n        filter: Some(vec![Filter::new(\"Category\", \"in\", &[\n            \"Furniture\",\n            \"Technology\",\n        ])]),\n        ..ViewConfigUpdate::default()\n    }))\n    .await?;\n```\n\n</div>"},{"title":"view » View","path":"view.md","text":"The [`View`] struct is Perspective's query and serialization interface. It\nrepresents a query on the `Table`'s dataset and is always created from an\nexisting `Table` instance via the [`Table::view`] method.\n\n[`View`]s are immutable with respect to the arguments provided to the\n[`Table::view`] method; to change these parameters, you must create a new\n[`View`] on the same [`Table`]. However, each [`View`] is _live_ with respect to\nthe [`Table`]'s data, and will (within a conflation window) update with the\nlatest state as its parent [`Table`] updates, including incrementally\nrecalculating all aggregates, pivots, filters, etc. [`View`] query parameters\nare composable, in that each parameter works independently _and_ in conjunction\nwith each other, and there is no limit to the number of pivots, filters, etc.\nwhich can be applied.\n\n<div class=\"javascript\">\n<div class=\"warning\">\nThe examples in this module are in JavaScript. See <a href=\"https://docs.rs/crate/perspective/latest\"><code>perspective</code></a> docs for the Rust API.\n</div>\n</div>\n<div class=\"python\">\n<div class=\"warning\">\nThe examples in this module are in Python. See <a href=\"https://docs.rs/crate/perspective/latest\"><code>perspective</code></a> docs for the Rust API.\n</div>\n</div>"},{"title":"view » Examples","path":"view.md","text":"<div class=\"javascript\">\n\n```javascript\nconst table = await perspective.table({\n    id: [1, 2, 3, 4],\n    name: [\"a\", \"b\", \"c\", \"d\"],\n});\n\nconst view = await table.view({ columns: [\"name\"] });\nconst json = await view.to_json();\nawait view.delete();\n```\n\n</div>\n<div class=\"python\">\n\n```python\ntable = perspective.Table({\n  \"id\": [1, 2, 3, 4],\n  \"name\": [\"a\", \"b\", \"c\", \"d\"]\n});\n\nview = table.view(columns=[\"name\"])\narrow = view.to_arrow()\nview.delete()\n```\n\n</div>\n<div class=\"rust\">\n\n```rust\nlet opts = TableInitOptions::default();\nlet data = TableData::Update(UpdateData::Csv(\"x,y\\n1,2\\n3,4\".into()));\nlet table = client.table(data, opts).await?;\n\nlet view = table.view(None).await?;\nlet arrow = view.to_arrow().await?;\nview.delete().await?;\n```\n\n</div>"},{"title":"virtual_servers » Virtual Servers","path":"virtual_servers.md","text":"A Virtual Server allows Perspective to query external data sources (such as\nDuckDB or ClickHouse) without loading the entire dataset into Perspective's\nbuilt-in data engine. Instead, Perspective translates its query operations\n(group by, sort, filter, etc.) into queries the external data source can execute\nnatively, and only transfers the data needed for the current view.\n\nThe Virtual Server API works on any platform that has a Perspective Client —\nincluding JavaScript (both Node.js and the browser via WebAssembly), Python, and\nRust. In the browser, this means a virtual server can front a WASM-based engine\nlike `@duckdb/duckdb-wasm`, giving `<perspective-viewer>` the ability to query a\ndatabase running entirely client-side without loading data into Perspective's\nown engine.\n\nThis is useful when:\n\n- The dataset is too large to fit in browser memory or a single process.\n- Data already lives in a database and you want to avoid duplicating it.\n- You want to leverage a database's native query optimizations.\n- A WASM build of the data source is available in the browser (e.g.\n  `@duckdb/duckdb-wasm`) and you want to query it directly."},{"title":"virtual_servers » Virtual Servers » How it works","path":"virtual_servers.md","text":"A virtual server implements a handler interface that Perspective calls to\nsatisfy `Table` and `View` operations. The handler translates Perspective's view\nconfiguration into the external system's query language (typically SQL),\nexecutes the query, and returns the results as columnar data. Because the\nhandler speaks the standard Perspective Client protocol, it can run anywhere a\nClient can — in-process, in a WebWorker, or on a remote server.\n\n```\n┌──────────────────────────────────────────────────┐\n│  <perspective-viewer>                            │\n└──┬───────────────────────────────────────────────┘\n   │   ┌──────────────────────────────────────────────────┐\n   └──►│  Perspective Virtual Server Handler              │\n       └──┬───────────────────────────────────────────────┘\n          │   ┌──────────────────────────────────────────────────┐\n          └──►│  External DB (DuckDB, ClickHouse, …).            │\n              └──────────────────────────────────────────────────┘\n```\n\nThe viewer communicates with the virtual server handler the same way it would\nwith a regular Perspective server. The handler advertises its capabilities\n(which operations it supports) via a _features_ object, and the viewer UI adapts\naccordingly — disabling controls for unsupported operations."},{"title":"virtual_servers » Virtual Servers » Built-in implementations","path":"virtual_servers.md","text":"Perspective ships with virtual server implementations for:\n\n- **DuckDB** — query DuckDB databases in-browser via WASM\n  ([JavaScript](../how_to/javascript/virtual_server/duckdb.md)) or server-side\n  ([Python](../how_to/python/virtual_server/duckdb.md)).\n- **ClickHouse** — query a ClickHouse server from the browser\n  ([JavaScript](../how_to/javascript/virtual_server/clickhouse.md)) or from\n  Python ([Python](../how_to/python/virtual_server/clickhouse.md)).\n\nvirtual_servers » Virtual Servers » Custom implementations\n\nYou can implement your own virtual server to connect Perspective to any data\nsource. See the language-specific guides:\n\n- [JavaScript: Implementing a custom Virtual Server](../how_to/javascript/virtual_server/custom.md)\n- [Python: Implementing a custom Virtual Server](../how_to/python/virtual_server/custom.md)"},{"title":"virtual_servers » Virtual Servers » Features declaration","path":"virtual_servers.md","text":"The `get_features()` / `getFeatures()` method returns an object that tells\nPerspective which query operations the virtual server supports. The viewer will\nonly show controls for supported operations:\n\n| Field         | Type   | Description                                                 |\n| ------------- | ------ | ----------------------------------------------------------- |\n| `group_by`    | `bool` | Whether group-by aggregation is supported                   |\n| `split_by`    | `bool` | Whether split-by (pivot) is supported                       |\n| `sort`        | `bool` | Whether sorting is supported                                |\n| `expressions` | `bool` | Whether computed expressions are supported                  |\n| `filter_ops`  | `dict` | Map of column type to list of supported filter operators    |\n| `aggregates`  | `dict` | Map of column type to list of supported aggregate functions |\n| `on_update`   | `bool` | Whether update callbacks are supported                      |"},{"title":"getting_started » Getting Started","path":"getting_started.md","text":"Guides for installing and using Perspective in JavaScript (Browser & Node.js),\nPython and Rust. Each section includes installation steps, basic usage examples,\nand language-specific integration details."},{"title":"agent » Configuring the LLM agent","path":"agent.md","text":"`<perspective-viewer>` ships with an embedded LLM agent which drives the viewer\nthrough its public API — reading the schema, writing the `ViewerConfig`,\nchoosing a plugin, authoring ExprTK expressions and managing panels. It is\n**opt-in**: the **Chat** tab in the settings sidebar stays hidden, and no\nnetwork request is ever made, until you call\n`HTMLPerspectiveViewerElement::agentConfig`.\n\n```javascript\nimport { providers } from \"@perspective-dev/viewer\";\n\nconst viewer = document.querySelector(\"perspective-viewer\");\nviewer.agentConfig({\n    ...providers.anthropic,\n    apiKey: \"sk-ant-...\",\n});\n```"},{"title":"agent » Configuring the LLM agent » Connecting to a model","path":"agent.md","text":"The agent core connects via OpenAI chat-completions conventional API, over\nprimitive connection fields. Exactly one of `url` or `engine` is required:\n\n- `url` — a full chat-completions endpoint. Any OpenAI-compatible service works:\n  the Anthropic and Gemini compatibility endpoints, OpenRouter, LM Studio,\n  Ollama, llama.cpp, vLLM, or your own proxy.\n- `engine` — an in-page engine object exposing\n  `chat.completions.create(request)`, e.g.\n  [WebLLM](https://github.com/mlc-ai/web-llm)'s `MLCEngine`. Mutually exclusive\n  with `url`.\n\nThe remaining connection fields are `headers`, `apiKey` (sugar for an\n`Authorization: Bearer` header), `model`, and `name`. The `providers` export\nsupplies presets for the common ones — `anthropic`, `gemini`, `openai`,\n`openrouter`, `lmstudio` and `ollama` — and spread order is override order:\n\n```javascript\nviewer.agentConfig({\n    ...providers.anthropic,\n    apiKey: \"sk-ant-...\",\n    model: \"claude-haiku-4-5\", // overrides the preset's default\n});\n```\n\nLocal servers usually need their CORS opt-in enabled first:\n[LM Studio](https://lmstudio.ai/) has a setting in its developer server panel,\nand Ollama reads `OLLAMA_ORIGINS`.\n\n> **A key in `agentConfig` is a key in the browser tab.** It is sent directly to\n> the provider from the page, which is fine for local development and internal\n> tools, but for anything shared you should point `url` at a proxy you control\n> and keep the credential on the server.\n\nTool-calling quality varies more than general chat quality does. Frontier models\nhandle the viewer's tool surface reliably; among local models, recent Qwen and\nLlama instruct builds are the ones to try first."},{"title":"agent » Configuring the LLM agent » Connecting to a model » In-page engines","path":"agent.md","text":"An `engine` runs the model in the tab, so no prompt and no data leave the\nmachine and no key is involved. [WebLLM ](https://github.com/mlc-ai/web-llm) for\nexample:\n\n```javascript\nimport * as webllm from \"@mlc-ai/web-llm\";\n\nconst engine = await webllm.CreateMLCEngine(\n    \"Hermes-3-Llama-3.1-8B-q4f16_1-MLC\",\n    { initProgressCallback: (x) => console.log(x.text) },\n    { context_window_size: 16384 },\n);\n\nviewer.agentConfig({\n    name: \"webllm\",\n    engine,\n    systemRole: \"user\",\n});\n```\n\nagent » Configuring the LLM agent » The documentation bundle\n\n`<perspective-viewer>` publishes a metadata bundle at\n`dist/docs/perspective-docs.json` containing a searchable corpus of the\nPerspective documentation plus generated JSON schemas for the viewer's config\ntypes. Passing it as `docs` is optional, but without it the agent will not be\nvery capable — it is what lets the agent look things up rather than guess:\n\n```javascript\nimport docs from \"@perspective-dev/viewer/dist/docs/perspective-docs.json\" with { type: \"json\" };\n\nviewer.agentConfig({ ...providers.anthropic, apiKey: \"sk-ant-...\", docs });\n\n// ... or ...\n\nviewer.agentConfig({\n    ...providers.anthropic,\n    apiKey: \"sk-ant-...\",\n    docs: fetch(\n        \"node_modules/@perspective-dev/viewer/dist/docs/perspective-docs.json\",\n    ),\n});\n```\n\nWithout it the agent still works: `search_docs` searches an empty corpus and the\ntool parameter schemas degrade to permissive objects. The practical difference\nis how often a weaker model invents a field name that doesn't exist, or writes\nan ExprTK expression against syntax Perspective doesn't have."},{"title":"agent » Configuring the LLM agent » The documentation bundle » Telling the agent about your data","path":"agent.md","text":"The agent learns column names and types from `get_schema`, but not what they\n_mean_ — that `Discount` is a ratio rather than a percent, or that a negative\n`Profit` is a return rather than an error. Add those notes as extra corpus\nentries:\n\n```javascript\nimport bundle from \"@perspective-dev/viewer/dist/docs/perspective-docs.json\" with { type: \"json\" };\n\nconst DATASET_DOCS = [\n    {\n        title: \"Superstore columns\",\n        text: \"`Discount` is a ratio in [0, 1], not a percent. `Profit` is net of `Discount` and is negative for returns.\",\n    },\n];\n\nviewer.agentConfig({\n    ...providers.anthropic,\n    apiKey: \"sk-ant-...\",\n    docs: { ...bundle, chunks: [...bundle.chunks, ...DATASET_DOCS] },\n});\n```\n\nAn inline `[{title?, text}]` array may also be passed as `docs` on its own, when\nyou have host notes but no packaged bundle."},{"title":"custom_worker » Customizing perspective.worker()","path":"custom_worker.md","text":"`perspective.worker()` creates a `Client` that connects to a Perspective data\nengine. By default it spins up a dedicated `Worker` running the built-in\nWebAssembly engine, but you can pass an argument to change this behavior:\n\n-   A **`Worker`**, **`SharedWorker`**, or **`ServiceWorker`** — runs the\n    built-in engine in a different worker context.\n-   A **`MessagePort`** from `createMessageHandler()` — connects to a\n    [Virtual Server](virtual_server/custom.md) instead of the built-in engine.\n\ncustom_worker » Customizing perspective.worker() » Built-in engine with a custom Worker\n\nPass a `Worker`, `SharedWorker`, or `ServiceWorker` that loads the worker script\ndistributed at\n`\"@perspective-dev/client/dist/cdn/perspective-server.worker.js\"`.\n\n<span class=\"warning\">`SharedWorker` and `ServiceWorker` have more complicated\nbehavior compared to a dedicated `Worker`, and will need special consideration\nto integrate (or debug).</span>"},{"title":"custom_worker » Customizing perspective.worker() » Built-in engine with a custom Worker » Dedicated Worker","path":"custom_worker.md","text":"```javascript\nconst worker = await perspective.worker(new Worker(url));\n```\n\ncustom_worker » Customizing perspective.worker() » Built-in engine with a custom Worker » SharedWorker\n\n```javascript\nconst worker = await perspective.worker(new SharedWorker(url));\n```\n\ncustom_worker » Customizing perspective.worker() » Built-in engine with a custom Worker » ServiceWorker\n\n```javascript\nconst registration = await navigator.serviceWorker.register(url, {\n    scope: \"\", // Your scope here\n});\n\nconst worker = await perspective.worker(registration.active);\n```"},{"title":"custom_worker » Customizing perspective.worker() » Virtual Server","path":"custom_worker.md","text":"Instead of the built-in WebAssembly engine, `perspective.worker()` can connect\nto a Virtual Server — an adapter that translates Perspective queries into\noperations on an external data source such as\n[DuckDB](virtual_server/duckdb.md) or\n[ClickHouse](virtual_server/clickhouse.md).\n\nUse `perspective.createMessageHandler()` with a `VirtualServerHandler` to create\na `MessagePort`, then pass it to `worker()`:\n\n```javascript\nimport perspective from \"@perspective-dev/client\";\n\nconst handler = {\n    /* VirtualServerHandler implementation */\n};\n\nconst server = perspective.createMessageHandler(handler);\nconst client = await perspective.worker(server);\nconst table = await client.open_table(\"my_table\");\n```\n\nThe returned `Client` works identically to one backed by the built-in engine —\nyou can pass it to `<perspective-viewer>.load()`, call `open_table()`, etc. The\ndifference is that queries are fulfilled by your handler rather than the WASM\nengine.\n\nFor the full `VirtualServerHandler` interface and a worked example, see\n[Implementing a custom Virtual Server](virtual_server/custom.md)."},{"title":"deleting » Deleting a table() or view()","path":"deleting.md","text":"Unlike standard JavaScript objects, Perspective objects such as `table()` and\n`view()` store their associated data in the WebAssembly heap. Because of this,\nas well as the current lack of a hook into the JavaScript runtime's garbage\ncollector from WebAssembly, the memory allocated to these Perspective objects\ndoes not automatically get cleaned up when the object falls out of scope.\n\nIn order to prevent memory leaks and reclaim the memory associated with a\nPerspective `table()` or `view()`, you must call the `delete()` method:\n\n```javascript\nawait view.delete();\n\n// This method will throw an exception if there are still `view()`s depending\n// on this `table()`!\nawait table.delete();\n```\n\nSimilarly, `<perspective-viewer>` Custom Elements do not delete the memory\nallocated for the UI when they are removed from the DOM.\n\n```javascript\nawait viewer.delete();\n```"},{"title":"events » Listening for events","path":"events.md","text":"The `<perspective-viewer>` Custom Element fires all the same HTML `Event`s that\nstandard DOM `HTMLElement` objects fire, in addition to a few custom\n`CustomEvent`s which relate to UI updates including those initiaed through user\ninteraction.\n\nevents » Listening for events » Update events\n\nWhenever a `<perspective-viewer>`s underlying `table()` is changed via the\n`load()` or `update()` methods, a `perspective-view-update` DOM event is fired.\nSimilarly, `view()` updates instigated either through the Attribute API or\nthrough user interaction will fire a `perspective-config-update` event:\n\n```javascript\nelem.addEventListener(\"perspective-config-update\", function (event) {\n    var config = elem.save();\n    console.log(\"The view() config has changed to \" + JSON.stringify(config));\n});\n```"},{"title":"events » Listening for events » Click events","path":"events.md","text":"Whenever a `<perspective-viewer>`'s grid or chart is clicked, a\n`perspective-click` DOM event is fired containing a detail object with\n`config`, `column_names`, `row` and `panel`.\n\nThe `config` object contains an array of `filters` that can be applied to a\n`<perspective-viewer>` through the use of `restore()` updating it to show the\nfiltered subset of data.\n\nThe `column_names` property contains an array of matching columns, the `row`\nproperty returns the associated row data, and `panel` identifies the panel\nwhich fired the event in a multi-panel viewer.\n\n```javascript\nelem.addEventListener(\"perspective-click\", function (event) {\n    const { config, panel } = event.detail;\n    elem.restore(config, { panel });\n});\n```"},{"title":"events » Listening for events » Selection events","path":"events.md","text":"`perspective-select` fires when a plugin's selection changes. Its detail is a\n`PerspectiveSelectDetail`, exported from `@perspective-dev/viewer`:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `selected` | `boolean` | Whether anything is currently selected |\n| `row` | `object` | The associated row data |\n| `column_names` | `string[]` | Matching column names |\n| `removeConfigs` | `ViewConfigUpdate[]` | Configs whose filters should be _removed_ |\n| `insertConfigs` | `ViewConfigUpdate[]` | Configs whose filters should be _applied_ |\n| `panel` | `string?` | The originating panel, in a multi-panel viewer |\n\n`removeConfigs` is applied first, then `insertConfigs`. The\n`removeFilters` and `insertFilters` getters flatten each to a plain `Filter[]`.\n\n```javascript\nimport { PerspectiveSelectDetail } from \"@perspective-dev/viewer\";\n\nelem.addEventListener(\"perspective-select\", function (event) {\n    const { insertFilters, removeFilters } = event.detail;\n    console.log(\"apply\", insertFilters, \"clear\", removeFilters);\n});\n```\n\n<div class=\"warning\">The <code>detail.config</code> field on\n<code>perspective-select</code> was replaced by <code>insertConfigs</code> and\n<code>removeConfigs</code>. Without an explicit <code>removeConfigs</code>,\na filter on a column outside the source's <code>group_by</code>,\n<code>split_by</code> or <code>filter</code> cannot be cleared.</div>"},{"title":"events » Listening for events » Global filter events","path":"events.md","text":"In a multi-panel viewer, panels toggled to _Master_ contribute filter clauses\nto an element-level global filter set, which is applied as a transient overlay\nto every _detail_ panel (and never written into their saved configs).\n\n- `perspective-global-filter` fires on a master panel's selection.\n- `perspective-global-filter-update` fires whenever the global filter set\n  changes, with a `Filter[]` detail.\n\n```javascript\nelem.addEventListener(\"perspective-global-filter-update\", function (event) {\n    console.log(\"Global filters are now\", event.detail);\n});\n```"},{"title":"events » Listening for events » Layout events","path":"events.md","text":"A multi-panel `<perspective-viewer>` reports changes to its panel _collection_\non two separate channels. They are distinct facts — which panels exist, and\nwhich one is selected — so neither event implies the other.\n\n- `perspective-layout-update` fires when a panel is added to or removed from\n  the layout. Its `detail.panels` is the placed panel ids in insertion order,\n  identical to what [`getPanelNames()`](#) returns.\n- `perspective-active-panel-update` fires when the active panel changes, with\n  a `detail.panel` of the new panel's id — or `null` at zero panels.\n\n```javascript\nelem.addEventListener(\"perspective-layout-update\", function (event) {\n    console.log(\"Panels are now\", event.detail.panels);\n});\n```\n\nGeometry changes — dragging a split divider, reordering tabs — do **not** fire\nthese events, because they change the layout tree without changing the panel\nset. Use `saveWorkspace()` to read the current geometry.\n\n<div class=\"warning\">The <code>workspace-layout-update</code> and\n<code>workspace-new-view</code> events from the removed\n<code>@perspective-dev/workspace</code> package no longer exist.\n<code>perspective-layout-update</code> is the closest replacement for the\nformer; for per-panel config changes use\n<code>perspective-config-update</code>.</div>"},{"title":"importing » JavaScript - Importing with or without a bundler","path":"importing.md","text":"Perspective requires the browser to have access to Perspective's `.wasm`\nbinaries _in addition_ to the bundled `.js` files, and as a result the build\nprocess requires a few extra steps. Perspective's NPM releases come with\nmultiple prebuilt configurations.\n\nimporting » JavaScript - Importing with or without a bundler » ESM builds with a bundler\n\nThe recommended builds for production use are packaged as ES Modules and require\na _bootstrapping_ step in order to acquire the `.wasm` binaries and initialize\nPerspective's JavaScript with them. Because they have no hard-coded dependencies\non the `.wasm` paths, they are ideal for use with JavaScript bundlers such as\nESBuild, Rollup, Vite or Webpack.\n\nESM builds must be _bootstrapped_ with their `.wasm` binaries to initialize. The\n`wasm` binaries can be found in their respective `dist/wasm` directories.\n\n```javascript\nimport perspective_viewer from \"@perspective-dev/viewer\";\nimport perspective from \"@perspective-dev/client\";\n\n// TODO These paths must be provided by the bundler!\nconst SERVER_WASM = ... // \"@perspective-dev/server/dist/wasm/perspective-server.wasm\"\nconst CLIENT_WASM = ... // \"@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm\"\n\nawait Promise.all([\n    perspective.init_server(SERVER_WASM),\n    perspective_viewer.init_client(CLIENT_WASM),\n]);\n\n// Now Perspective API will work!\nconst worker = await perspective.worker();\nconst viewer = document.createElement(\"perspective-viewer\");\n```\n\nThe exact syntax will vary slightly depending on the bundler."},{"title":"importing » JavaScript - Importing with or without a bundler » ESM builds with a bundler » Memory64 (wasm64)","path":"importing.md","text":"`@perspective-dev/server` also ships a WebAssembly Memory64 build of the\nengine, `dist/wasm/perspective-server.memory64.wasm`, which raises the\nengine's heap ceiling from 4GB to 16GB (at some engine performance cost).\n`init_server` accepts both binaries at once — register each as a _thunk_ and\nonly the selected binary is ever downloaded. The wasm64 binary is used\nwhenever the browser supports Memory64; registering only the wasm32 binary\n(as above) opts out.\n\n```javascript\nperspective.init_server({\n    wasm32: () => fetch(SERVER_WASM),\n    wasm64: () => fetch(SERVER_WASM64),\n});\n```"},{"title":"importing » JavaScript - Importing with or without a bundler » ESM builds with a bundler » Vite","path":"importing.md","text":"```javascript\nimport SERVER_WASM from \"@perspective-dev/server/dist/wasm/perspective-server.wasm?url\";\nimport CLIENT_WASM from \"@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm?url\";\n\nawait Promise.all([\n    perspective.init_server(fetch(SERVER_WASM)),\n    perspective_viewer.init_client(fetch(CLIENT_WASM)),\n]);\n```\n\nYou'll also need to target `esnext` in your `vite.config.js` in order to run the\n`build` step:\n\n```javascript\nimport { defineConfig } from \"vite\";\nexport default defineConfig({\n    build: {\n        target: \"esnext\",\n    },\n});\n```"},{"title":"importing » JavaScript - Importing with or without a bundler » ESM builds with a bundler » ESBuild","path":"importing.md","text":"```javascript\nimport SERVER_WASM from \"@perspective-dev/server/dist/wasm/perspective-server.wasm\";\nimport CLIENT_WASM from \"@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm\";\n\nawait Promise.all([\n    perspective.init_server(fetch(SERVER_WASM)),\n    perspective_viewer.init_client(fetch(CLIENT_WASM)),\n]);\n```\n\nESBuild config JSON to encode this asset as a `file`:\n\n```javascript\n{\n    // ...\n    \"loader\": {\n        // ...\n        \".wasm\": \"file\"\n    }\n}\n```\n\nimporting » JavaScript - Importing with or without a bundler » ESM builds with a bundler » Webpack\n\n```javascript\nimport SERVER_WASM from \"@perspective-dev/server/dist/wasm/perspective-server.wasm\";\nimport CLIENT_WASM from \"@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm\";\n\nawait Promise.all([\n    perspective.init_server(SERVER_WASM),\n    perspective_viewer.init_client(CLIENT_WASM),\n]);\n```\n\nWebpack config:\n\n```javascript\n{\n    // ...\n    module: {\n        // ...\n        rules: [\n            // ...\n            {\n                test: /\\.wasm$/,\n                type: \"asset/resource\"\n            },\n        ]\n    },\n    experiments: {\n        // ...\n        asyncWebAssembly: false,\n        syncWebAssembly: false,\n    },\n}\n```"},{"title":"importing » JavaScript - Importing with or without a bundler » Inline builds with a bundler","path":"importing.md","text":"<span class=\"warning\">Inline builds are deprecated and will be removed in a\nfuture release.</span>\n\nPerspective's _Inline_ Builds work by _inlining_ WebAssembly binary content as\na base64-encoded string. While inline builds work with most bundlers and _do\nnot_ require bootstrapping, there is an inherent file-size and boot-performance\npenalty. Prefer your bundler's inlining features and Perspective ESM builds\nwhere possible.\n\n```javascript\nimport \"@perspective-dev/viewer/dist/esm/perspective-viewer.inline.js\";\nimport psp from \"@perspective-dev/client/dist/esm/perspective.inline.js\";\n```"},{"title":"importing » JavaScript - Importing with or without a bundler » CDN builds","path":"importing.md","text":"Perspective's CDN builds are good for non-bundled scenarios, such as importing\ndirectly from a `<script>` tag. CDN builds _do not_ require _bootstrapping_ the\nWebAssembly binaries, but they also generally _do not_ work with bundlers.\n\nCDN builds are in ES Module format, thus to include them via a CDN they must be\nimported from a `<script type=\"module\">`:\n\n```html\n<script type=\"module\">\n    import \"https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js\";\n    import \"https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js\";\n    import \"https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js\";\n    import perspective from \"https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js\";\n\n    // .. Do stuff here ..\n</script>\n```"},{"title":"importing » JavaScript - Importing with or without a bundler » Node.js builds","path":"importing.md","text":"The Node.js runtime for the `@perspective-dev/client` module runs in-process by\ndefault and does not implement a `child_process` interface. Hence, there is no\n`worker()` method, and the module object itself directly exports the full\n`perspective` API.\n\n```javascript\nconst perspective = require(\"@perspective-dev/client\");\n```\n\nIn Node.js, perspective does not run in a WebWorker (as this API does not exist\nin Node.js), so no need to call the `.worker()` factory function - the\n`perspective` library exports the functions directly and run synchronously in\nthe main process."},{"title":"installation » JavaScript Installation and Module Structure","path":"installation.md","text":"Perspective is designed for flexibility, allowing developers to pick and choose\nwhich modules they need. The main modules are:\n\n- `@perspective-dev/client`\n  The data engine library, as both a browser ES6 and Node.js module. Provides a\n  WebAssembly, WebWorker (browser) and Process (node.js) runtime.\n\n- `@perspective-dev/viewer`\n  A user-configurable visualization widget, bundled as a\n  [Web Component](https://www.webcomponents.org/introduction). This module\n  includes the core data engine module as a dependency.\n\n`<perspective-viewer>` by itself only implements a trivial debug renderer, which\nprints the currently configured `view()` as a CSV. Plugin modules are packaged\nseparately and must be imported individually.\n\n- `@perspective-dev/viewer-datagrid`\n  A custom high-performance data-grid component based on HTML `<table>`.\n\n- `@perspective-dev/viewer-charts`\n  A set of charting components base on WebGL.\n\nWhen imported after `@perspective-dev/viewer`, the plugin modules will register\nthemselves automatically, and the renderers they export will be available in the\n`plugin` dropdown in the `<perspective-viewer>` UI."},{"title":"installation » JavaScript Installation and Module Structure » Browser","path":"installation.md","text":"Perspective's WebAssembly data engine is available via NPM in the same package\nas its Node.js counterpart, `@perspective-dev/client`. The Perspective Viewer UI\n(which has no Node.js component) must be installed separately:\n\n```bash\n$ npm add @perspective-dev/client @perspective-dev/viewer\n```\n\nBy itself, `@perspective-dev/viewer` does not provide any visualizations, only\nthe UI framework. Perspective _Plugins_ provide visualizations and must be\ninstalled separately. All Plugins are optional - but a `<perspective-viewer>`\nwithout Plugins would be rather boring!\n\n```bash\n$ npm add @perspective-dev/viewer-charts @perspective-dev/viewer-datagrid\n```"},{"title":"installation » JavaScript Installation and Module Structure » Node.js","path":"installation.md","text":"To use Perspective from a Node.js server, simply install via NPM.\n\n```bash\n$ npm add @perspective-dev/client\n```"},{"title":"join » Joining Tables","path":"join.md","text":"`perspective.join()` creates a read-only `Table` by joining two source tables on\na shared key column. The result is reactive — it updates automatically when\neither source table changes. See [`Join`](../../explanation/join.md) for\nconceptual details.\n\njoin » Joining Tables » Basic Inner Join\n\n```javascript\nconst orders = await perspective.table([\n    { id: 1, product_id: 101, qty: 5 },\n    { id: 2, product_id: 102, qty: 3 },\n    { id: 3, product_id: 101, qty: 7 },\n]);\n\nconst products = await perspective.table([\n    { product_id: 101, name: \"Widget\" },\n    { product_id: 102, name: \"Gadget\" },\n]);\n\nconst joined = await perspective.join(orders, products, \"product_id\");\nconst view = await joined.view();\nconst json = await view.to_json();\n// [\n//   { product_id: 101, id: 1, qty: 5, name: \"Widget\" },\n//   { product_id: 101, id: 3, qty: 7, name: \"Widget\" },\n//   { product_id: 102, id: 2, qty: 3, name: \"Gadget\" },\n// ]\n```"},{"title":"join » Joining Tables » Join Types","path":"join.md","text":"Pass `join_type` in the options to select inner, left, or outer join behavior:\n\n```javascript\n// Left join: all left rows, nulls for unmatched right columns\nconst left_joined = await perspective.join(left, right, \"id\", {\n    join_type: \"left\",\n});\n\n// Outer join: all rows from both tables\nconst outer_joined = await perspective.join(left, right, \"id\", {\n    join_type: \"outer\",\n});\n```\n\njoin » Joining Tables » Reactive Updates\n\nThe joined table recomputes automatically when either source table is updated:\n\n```javascript\nconst left = await perspective.table([{ id: 1, x: 10 }]);\nconst right = await perspective.table([{ id: 2, y: \"b\" }]);\n\nconst joined = await perspective.join(left, right, \"id\");\nconst view = await joined.view();\n\nlet json = await view.to_json();\n// [] — no matching keys yet\n\nawait right.update([{ id: 1, y: \"a\" }]);\njson = await view.to_json();\n// [{ id: 1, x: 10, y: \"a\" }] — new match detected\n```"},{"title":"loading_data » Loading data from a Table","path":"loading_data.md","text":"Data can be loaded into `<perspective-viewer>` in the form of a `Table()` or a\n`Promise<Table>` via the `load()` method.\n\n```javascript\n// Create a new worker, then a new table promise on that worker.\nconst worker = await perspective.worker();\nconst table = await worker.table(data);\n\n// Bind a viewer element to this table.\nawait viewer.load(table);\n```\n\nloading_data » Loading data from a Table » Sharing a Table between multiple <perspective-viewer>s\n\nMultiple `<perspective-viewer>`s can share a `table()` by passing the `table()`\ninto the `load()` method of each viewer. Each `perspective-viewer` will update\nwhen the underlying `table()` is updated, but `table.delete()` will fail until\nall `perspective-viewer` instances referencing it are also deleted:\n\n```javascript\nconst viewer1 = document.getElementById(\"viewer1\");\nconst viewer2 = document.getElementById(\"viewer2\");\n\n// Create a new WebWorker\nconst worker = await perspective.worker();\n\n// Create a table in this worker\nconst table = await worker.table(data);\n\n// Load the same table in 2 different <perspective-viewer> elements\nawait viewer1.load(table);\nawait viewer2.load(table);\n\n// Both `viewer1` and `viewer2` will reflect this update\nawait table.update([{ x: 5, y: \"e\", z: true }]);\n```"},{"title":"loading_data » Loading data from a Table » Loading from a virtual Table","path":"loading_data.md","text":"Loading a virtual (server-only) `Table` works just like loading a local/Web\nWorker `Table` — just pass the virtual `Table` to `viewer.load()`. In the\nbrowser:\n\n```javascript\nconst elem = document.getElementsByTagName(\"perspective-viewer\")[0];\n\n// Bind to the server's worker instead of instantiating a Web Worker.\nconst websocket = await perspective.websocket(\n    window.location.origin.replace(\"http\", \"ws\")\n);\n\n// Bind the viewer to the preloaded data source.  `table` and `view` objects\n// live on the server.\nconst server_table = await websocket.open_table(\"table_one\");\nawait elem.load(server_table);\n```\n\nAlternatively, data can be _cloned_ from a server-side virtual `Table` into a\nclient-side WebAssembly `Table`. The browser clone will be synced via delta\nupdates transferred via Apache Arrow IPC format, but local `View`s created will\nbe calculated locally on the client browser.\n\n```javascript\nconst worker = await perspective.worker();\nconst server_view = await server_table.view();\nconst client_table = worker.table(server_view);\nawait elem.load(client_table);\n```\n\n`<perspective-viewer>` instances bound in this way are otherwise no different\nthan `<perspective-viewer>`s which rely on a Web Worker, and can even share a\nhost application with Web Worker-bound `table()`s. The same `promise`-based API\nis used to communicate with the server-instantiated `view()`, only in this case\nit is over a websocket."},{"title":"nodejs_server » Server-only via WebSocketServer() and Node.js","path":"nodejs_server.md","text":"For exceptionally large datasets, a `Client` can be bound to a\n`perspective.table()` instance running in Node.js/Python/Rust remotely, rather\nthan creating one in a Web Worker and downloading the entire data set. This\ntrades off network bandwidth and server resource requirements for a smaller\nbrowser memory and CPU footprint.\n\nAn example in Node.js:\n\n```javascript\nconst { WebSocketServer, table } = require(\"@perspective-dev/client\");\nconst fs = require(\"fs\");\n\n// Start a WS/HTTP host on port 8080.  The `assets` property allows\n// the `WebSocketServer()` to also serves the file structure rooted in this\n// module's directory.\nconst host = new WebSocketServer({ assets: [__dirname], port: 8080 });\n\n// Read an arrow file from the file system and host it as a named table.\nconst arr = fs.readFileSync(__dirname + \"/superstore.lz4.arrow\");\nawait table(arr, { name: \"table_one\" });\n```\n\n... and the [`Client`] implementation in the browser:\n\n```javascript\nconst elem = document.getElementsByTagName(\"perspective-viewer\")[0];\n\n// Bind to the server's worker instead of instantiating a Web Worker.\nconst websocket = await perspective.websocket(\n    window.location.origin.replace(\"http\", \"ws\"),\n);\n\n// Create a virtual `Table` to the preloaded data source.  `table` and `view`\n// objects live on the server.\nconst server_table = await websocket.open_table(\"table_one\");\n```"},{"title":"plugin_settings » Plugin render limits","path":"plugin_settings.md","text":"`<perspective-viewer>` plugins (especially charts) may in some cases generate\nextremely large output which may lock up the browser. In order to prevent\naccidents (which generally require a browser refresh to fix), each plugin has a\n`max_cells` and `max_columns` heuristic which requires the user to opt-in to\nfully rendering `View`s which exceed these limits. To override this behavior,\nset these values for each plugin type individually, _before_ the plugin itself\nis rendered (e.g. calling `HTMLPerspectiveViewerElement::restore` with the\nrespective `plugin` name).\n\nIf you have a `<perspective-viewer>` instance, you can configure plugins via\n`HTMLPerspectiveViewerElement::getPlugin` and\n`HTMLPerspectiveViewerElement::getAllPlugins`:\n\n```javascript\nconst viewer = document.querySelector(\"perspective-viewer\");\nconst plugin = viewer.getPlugin(\"Treemap\");\nplugin.max_cells = 1_000_000;\nplugin.max_columns = 1000;\n```\n\n... Or alternatively, you can look up the Custom Element classes and set the\nstatic variants if you know the element name (you can e.g. look this up in your\nbrowser's DOM inspector):\n\n```javascript\nconst plugin = customElements.get(\"perspective-viewer-charts-treemap\");\nplugin.max_cells = 1_000_000;\nplugin.max_columns = 1000;\n```"},{"title":"react » React Component","path":"react.md","text":"We provide a React wrapper to prevent common issues and mistakes associated with\nusing the perspective-viewer web component in the context of React.\n\nBefore trying this example, please take a look at\n[how to bootstrap perspective](./importing.md).\n\nreact » React Component » PerspectiveViewer\n\nA simple example using the `PerspectiveViewer` component:\n\n```typescript\nimport React, { useCallback, useEffect, useRef } from \"react\";\nimport {\n    PerspectiveViewer,\n} from \"@perspective-dev/react\";\nimport perspective from \"@perspective-dev/client\";\n\nfunction App() {\n    const worker = useRef(null);\n\n    useEffect(() => {\n        (async () => {\n            worker.current = await perspective.worker();\n            const resp = await fetch(\"data.arrow\");\n            const arrow = await resp.arrayBuffer();\n            await worker.current.table(arrow, { name: \"my_table\" });\n        })();\n    }, []);\n\n    return (\n            <PerspectiveViewer\n                client={worker.current}\n                config={{group_by: [\"State\"], columns: [\"Sales\"]}}\n            />\n    );\n}\n```"},{"title":"save_restore » Saving and restoring UI state.","path":"save_restore.md","text":"`<perspective-viewer>` is _persistent_, in that its entire state (sans the data\nitself) can be serialized or deserialized. This include all column, filter,\npivot, expressions, etc. properties, as well as datagrid style settings, config\npanel visibility, and more. This overloaded feature covers a range of use cases:\n\n- Setting a `<perspective-viewer>`'s initial state after a `load()` call.\n- Updating a single or subset of properties, without modifying others.\n- Resetting some or all properties to their data-relative default.\n- Persisting a user's configuration to `localStorage` or a server."},{"title":"save_restore » Saving and restoring UI state. » Serializing and deserializing the viewer state","path":"save_restore.md","text":"To retrieve the entire state as a JSON-ready JavaScript object, use the `save()`\nmethod. `save()` also supports a few other formats such as `\"arraybuffer\"` and\n`\"string\"` (base64, not JSON), which you may choose for size at the expense of\neasy migration/manual-editing.\n\n```javascript\nconst json_token = await elem.save();\nconst string_token = await elem.save(\"string\");\n```\n\nFor any format, the serialized token can be restored to any\n`<perspective-viewer>` with a `Table` of identical schema, via the `restore()`\nmethod. Note that while the data for a token returned from `save()` may differ,\ngenerally its schema may not, as many other settings depend on column names and\ntypes.\n\n```javascript\nawait elem.restore(json_token);\nawait elem.restore(string_token);\n```\n\nAs `restore()` dispatches on the token's type, it is important to make sure that\nthese types match! A common source of error occurs when passing a\nJSON-stringified token to `restore()`, which will assume base64-encoded msgpack\nwhen a string token is used.\n\n```javascript\n// This will error!\nawait elem.restore(JSON.stringify(json_token));\n```"},{"title":"save_restore » Saving and restoring UI state. » Serializing and deserializing the viewer state » Updating individual properties","path":"save_restore.md","text":"Using the JSON format, every facet of a `<perspective-viewer>`'s configuration\ncan be manipulated from JavaScript using the `restore()` method. The valid\nstructure of properties is described via the\n[`ViewerConfigUpdate`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/ts/ts-rs/ViewerConfigUpdate.ts)\nand embedded\n[`ViewConfigUpdate`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-js/src/ts/ts-rs/ViewConfigUpdate.ts)\ntype declarations (both generated from the Rust definitions), and the\n[`View`](../../explanation/view.md) chapter of the documentation which has\nseveral examples for each `ViewConfig` property.\n\n```javascript\n// Set the plugin (will also update `columns` to plugin-defaults)\nawait elem.restore({ plugin: \"X Bar\" });\n\n// Update plugin and columns (only draws once)\nawait elem.restore({ plugin: \"X Bar\", columns: [\"Sales\"] });\n\n// Open the config panel\nawait elem.restore({ settings: true });\n\n// Create an expression\nawait elem.restore({\n    columns: ['\"Sales\" + 100'],\n    expressions: { \"New Column\": '\"Sales\" + 100' },\n});\n\n// ERROR if the column does not exist in the schema or expressions\n// await elem.restore({columns: [\"\\\"Sales\\\" + 100\"], expressions: {}});\n\n// Add a filter\nawait elem.restore({ filter: [[\"Sales\", \"<\", 100]] });\n\n// Add a sort, don't remove filter\nawait elem.restore({ sort: [[\"Prodit\", \"desc\"]] });\n\n// Reset just filter, preserve sort\nawait elem.restore({ filter: undefined });\n\n// Reset all properties to default e.g. after `load()`\nawait elem.reset();\n```\n\nAnother effective way to quickly create a token for a desired configuration is\nto simply copy the token returned from `save()` after settings the view manually\nin the browser. The JSON format is human-readable and should be quite easy to\ntweak once generated, as `save()` will return even the default settings for all\nproperties. You can call `save()` in your application code, or e.g. through the\nChrome developer console:\n\n```javascript\n// Copy to clipboard\ncopy(await document.querySelector(\"perspective-viewer\").save());\n```"},{"title":"save_restore » Saving and restoring UI state. » Multi-panel viewers","path":"save_restore.md","text":"`save()` and `restore()` operate on a _single_ panel — the _active_ one by\ndefault, or a specific panel via their optional `{ panel }` selector (e.g.\n`await elem.save({ panel: \"my-panel\" })`). If `restore()`'s `panel` names no\nexisting panel, a new panel is created with that id.\n\nA `<perspective-viewer>` may host multiple panels. To serialize or restore the\n_whole element_ — every panel plus the layout and cross-filter state — use\n`saveWorkspace()` and `restoreWorkspace()` instead:\n\n```javascript\nconst workspace_token = await elem.saveWorkspace();\nawait elem.restoreWorkspace(workspace_token);\n```\n\nA `saveWorkspace()` token is a `WorkspaceConfig`\n(`{ version, layout, panels, ... }`), not a `ViewerConfig` — passing it to the\nsingle-panel `restore()` will _not_ restore the layout (its `panels`/`layout`\nkeys are ignored)."},{"title":"save_restore » Saving and restoring UI state. » Colors, palettes and gradients (1)","path":"save_restore.md","text":"Per-column color styling lives in a panel's `columns_config`, keyed by column\nname, and every color-scale value is a string usable verbatim in CSS:\n\n| Kind     | Value                                                                               |\n| -------- | ----------------------------------------------------------------------------------- |\n| color    | `\"#rrggbb\"` (`#rgb`, `rgb()` and `rgba()` are accepted on input)                    |\n| palette  | `\"linear-gradient(to right, #rrggbb, #rrggbb, …)\"` — N colors, **no** positions     |\n| gradient | `\"linear-gradient(to right, #rrggbb 0%, #rrggbb 37.5%, …)\"` — every stop positioned |\n\nWhich reader applies is decided by the style control's kind (the datagrid's\n`fg_colors`/`bg_colors` and the charts' `gradient` are gradients; `palette` is a\npalette), never by inspecting the string — a position anywhere in a palette is\nrejected, while a gradient may omit positions on input (the CSS\nimplicit-position rules fill them) and may carry any direction token, which is\nnormalized to `to right`. Values equal to the plugin's default are not\nserialized.\n\n```javascript\nawait viewer.restore({\n    plugin: \"Datagrid\",\n    columns_config: {\n        Profit: {\n            number_bg_mode: \"gradient\",\n            bg_colors: \"linear-gradient(to right, #ff0000, #ffffff, #0000ff)\",\n        },\n    },\n});\n```\n\nAny of these may instead be a reference to a CSS custom property of the same\nkind — `\"var(--psp-user--color-<name>)\"`, `\"var(--psp-user--palette-<name>)\"` or\n`\"var(--psp-user--gradient-<name>)\"`. References are resolved when the config is\nwritten, against the element's computed style: the `palette` of the last\n`restoreWorkspace()` (below) takes precedence, then any `--psp-user--*` property\na theme or the page defines on the element. An unresolvable reference is dropped\n(the plugin's default renders). Panels hold literals from then on — `save()`\nalways emits literals, and the column style tab always edits a literal."},{"title":"save_restore » Saving and restoring UI state. » Colors, palettes and gradients (2)","path":"save_restore.md","text":"`saveWorkspace()` emits a **palette**: every color value in use across the\npanels is written in `panels` as a `var()` reference, and the top-level\n`palette` map (custom property name → value) carries each referenced definition\nonce. Names are stable — a value keeps the name the last `restoreWorkspace()`\ngave it when the values match, reuses a theme entry's name when it matches one\n(`--psp-user--<kind>-1`, `-2`, … are discovered by contiguous numbering), and\notherwise takes a fresh `--psp-user--<kind>-N`. `restoreWorkspace()` applies\n`palette` to the element as inline custom properties (replacing any previously\nrestored palette) before the panels' references resolve — which also makes it\nthe way to inject a brand or theme variation for a workspace to draw on.\n\nBy default only the values the panels reference are serialized; a restored\npalette's unused entries, and values pinned during a session, are in-session\nstate. Pass `{ full_palette: true }` to emit the element's whole set — in-use\nvalues unioned with the last restored palette and anything pinned since — for a\nsymmetric round trip:\n\n```javascript\nconst used_only = await elem.saveWorkspace();\nconst everything = await elem.saveWorkspace({ full_palette: true });\n```\n\nIn the column style tab, each color field's **Load** control lists the element's\nset (plus theme entries) for every panel and applies a chosen entry's value to\nthe field; **Pin** — offered while the field holds a value the restored set\nlacks — adds that value to the set for the rest of the session.\n\n```javascript\nawait elem.restoreWorkspace({\n    palette: {\n        \"--psp-user--gradient-heat\":\n            \"linear-gradient(to right, #0366d6, #ff7f0e)\",\n        \"--psp-user--palette-brand\":\n            \"linear-gradient(to right, #2771a8, #8b86ff, #ff471e)\",\n    },\n    panels: {\n        sales: {\n            table: \"superstore\",\n            plugin: \"Heatmap\",\n            columns: [\"Sales\"],\n            columns_config: {\n                Sales: { gradient: \"var(--psp-user--gradient-heat)\" },\n            },\n        },\n    },\n});\n```\n\nA malformed `palette` entry (a key outside\n`--psp-user--{gradient,palette,color}-`, or a value its kind rejects) fails the\nwhole `restoreWorkspace()` before any panel changes."},{"title":"serializing » Serializing data","path":"serializing.md","text":"The `view()` allows for serialization of data to JavaScript through the\n`to_json()`, `to_ndjson()`, `to_columns()`, `to_csv()`, and `to_arrow()` methods\n(the same data formats supported by the `Client::table` factory function). These\nmethods return a `promise` for the calculated data:\n\n```javascript\nconst view = await table.view({ group_by: [\"State\"], columns: [\"Sales\"] });\n\n// JavaScript Objects\nconsole.log(await view.to_json());\nconsole.log(await view.to_columns());\n\n// String\nconsole.log(await view.to_csv());\nconsole.log(await view.to_ndjson());\n\n// ArrayBuffer\nconsole.log(await view.to_arrow());\n```"},{"title":"theming » Theming","path":"theming.md","text":"Theming is supported in `perspective-viewer` and its accompanying plugins. A\nnumber of themes come bundled with `perspective-viewer`; you can import any of\nthese themes directly into your app, and the `perspective-viewer`s will be\nthemed accordingly:\n\n```javascript\n// Themes based on Thought Merchants's Prospective design\nimport \"@perspective-dev/viewer/dist/css/pro.css\";\nimport \"@perspective-dev/viewer/dist/css/pro-dark.css\";\n\n// Other themes\nimport \"@perspective-dev/viewer/dist/css/solarized.css\";\nimport \"@perspective-dev/viewer/dist/css/monokai.css\";\nimport \"@perspective-dev/viewer/dist/css/vaporwave.css\";\n\n// ...\n\n```\n\nAlternatively, you may use `themes.css`, which bundles all default themes\n\n```javascript\nimport \"@perspective-dev/viewer/dist/css/themes.css\";\n```\n\nIf you choose not to bundle the themes yourself, they are available through\n[CDN](https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/). These can\nbe directly linked in your HTML file:\n\n```html\n<link\n    rel=\"stylesheet\"\n    crossorigin=\"anonymous\"\n    href=\"https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/pro.css\"\n/>\n```\n\nNote the `crossorigin=\"anonymous\"` attribute. When including a theme from a\ncross-origin context, this attribute may be required to allow\n`<perspective-viewer>` to detect the theme. If this fails, additional themes are\nadded to the `document` after `<perspective-viewer>` init, or for any other\nreason theme auto-detection fails, you may manually inform\n`<perspective-viewer>` of the available theme names with the `.resetThemes()`\nmethod.\n\n```javascript\n// re-auto-detect themes\nviewer.resetThemes();\n\n// Set available themes explicitly (they still must be imported as CSS!)\nviewer.resetThemes([\"Pro Light\", \"Pro Dark\"]);\n```\n\n`<perspective-viewer>` will default to the first loaded theme when initialized.\nYou may override this via `.restore()`, or provide an initial theme by setting\nthe `theme` attribute:\n\n```html\n<perspective-viewer theme=\"Pro Light\"></perspective-viewer>\n```\n\nor\n\n```javascript\nconst viewer = document.querySelector(\"perspective-viewer\");\nawait viewer.restore({ theme: \"Pro Dark\" });\n```"},{"title":"theming » Theming » Custom Themes","path":"theming.md","text":"The best way to write a new theme is to\n[fork and modify an existing theme](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes),\nwhich are _just_ collections of regular CSS variables — Perspective's own\nthemes are plain `.css` files, with no preprocessor involved.\n`<perspective-viewer>` is\nnot \"themed\" by default and will lack icons and label text in addition to colors\nand fonts, so starting from an empty theme forces you to define _every_\ntheme-able variable to get a functional UI."},{"title":"theming » Theming » Custom Themes » Icons and Translation","path":"theming.md","text":"UI icons are defined by CSS variables provided by\n[`@perspective-dev/viewer/dist/css/icons.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/icons.css).\nThese variables must be defined for the UI icons to work - there are no default\nicons without a theme.\n\nUI text is also defined in CSS variables provided by\n[`@perspective-dev/viewer/dist/css/intl.css`](https://github.com/perspective-dev/perspective/blob/master/rust/perspective-viewer/src/themes/intl.css),\nand has identical import requirements. Some _example definitions_\n(automatically-translated sans-editing) can be found\n[`@perspective-dev/viewer/dist/css/intl/` folder](https://github.com/perspective-dev/perspective/tree/master/rust/perspective-viewer/src/themes/intl).\n\nImporting the pre-built `themes.css` stylesheet as well as a custom theme will\ndefine Icons and Translation globally as a side-effect. You can still customize\nicons in this mode with rules (of the appropriate specificity), _but_ if you do\nnot still remember to define these variables yourself, your theme will not work\nwithout the base `themes.css` package available."},{"title":"viewer » <perspective-viewer> Custom Element library","path":"viewer.md","text":"`<perspective-viewer>` provides a complete graphical UI for configuring the\n`perspective` library and formatting its output to the provided visualization\nplugins.\n\nOnce imported and initialized in JavaScript, the `<perspective-viewer>` Web\nComponent will be available in any standard HTML on your site. A simple example:\n\n```html\n<perspective-viewer id=\"view1\"></perspective-viewer>\n<script type=\"module\">\n    import perspective from \"@perspective-dev/client\";\n    import \"@perspective-dev/viewer\";\n\n    const viewer = document.getElementById(\"view1\");\n    const worker = await perspective.worker();\n    await worker.table(data, { name: \"my_table\" });\n\n    await viewer.load(worker);\n    await viewer.restore({ table: \"my_table\" });\n</script>\n```\n\n`load()` binds the viewer to a `Client`, and `restore()` selects which of that\nclient's `Table`s to show via the `table` field. Because `load()` alone\nselects no table, it does not render — the pair guarantees exactly one atomic\nrender.\n\nPassing a `Table` directly is still supported as a legacy shorthand,\n\n```javascript\nawait viewer.load(table);\n```\n\n... which is internally equivalent to:\n\n```javascript\nawait viewer.load(await table.get_client());\nawait viewer.restore({ table: await table.get_name() });\n```\n\n<div class=\"warning\">Always give your <code>Table</code> a <code>name</code>.\nWhen <code>name</code> is omitted a random one is assigned, so the\n<code>table</code> field in a token from <code>save()</code> will not match\nthe table after a page reload, and <code>restore()</code> will fail.</div>"},{"title":"viewer » <perspective-viewer> Custom Element library » Attributes","path":"viewer.md","text":"`<perspective-viewer>` can be configured via HTML attributes or JavaScript\nproperties. When set as attributes, the viewer will apply the configuration on\ninitialization:\n\n```html\n<perspective-viewer\n    columns='[\"Sales\", \"Profit\"]'\n    group-by='[\"Region\"]'\n    sort='[[\"Sales\", \"desc\"]]'>\n</perspective-viewer>\n```\n\nviewer » <perspective-viewer> Custom Element library » UI Features\n\nThe viewer provides an interactive side panel with:\n\n- **Column list** - drag and drop columns to configure `group_by`, `split_by`,\n  `sort`, and `filter` fields.\n- **New Column** button - opens an expression editor for creating computed\n  columns via the [expression language](../../explanation/view/config/expressions.md).\n- **Plugin selector** - switch between the visualization plugins registered on\n  the page. `@perspective-dev/viewer-datagrid` provides `Datagrid`;\n  `@perspective-dev/viewer-charts` provides `X Bar`, `Y Bar`, `Y Line`,\n  `Y Scatter`, `Y Area`, `X/Y Scatter`, `X/Y Line`, `Density`, `Treemap`,\n  `Sunburst`, `Heatmap`, `Candlestick`, `OHLC`, `Map Scatter`, `Map Line` and\n  `Map Density`.\n- **Theme** selector - toggle between available themes.\n- **Export** - download the current view as CSV or Arrow.\n- **Copy** - copy the current view to the clipboard.\n- **Reset** - restore the viewer to its default configuration."},{"title":"viewer » <perspective-viewer> Custom Element library » Methods","path":"viewer.md","text":"A `<perspective-viewer>` hosts one or more _panels_. Methods which address a\nsingle panel take an options-dict with an optional `panel` id, defaulting to\nthe _active_ panel — e.g. `await viewer.save({ panel: \"PANEL_ID_0\" })`.\n\nviewer » <perspective-viewer> Custom Element library » Methods » Binding\n\n| Method | Description |\n|---|---|\n| `load(client)` | Bind a `Client` (or, legacy, a `Table`) to the viewer |\n| `eject(options?)` | Remove a `Client` and dispose every panel bound to it |\n| `delete()` | Release the element's resources |\n| `getClient(options?)` | Get a bound `Client` |\n| `getTable(options?)` | Get a panel's `Table` |\n| `getView(options?)` | Get a panel's `View` |\n| `getViewConfig(options?)` | Get a panel's `ViewConfig` |"},{"title":"viewer » <perspective-viewer> Custom Element library » Methods » Configuration","path":"viewer.md","text":"| Method | Description |\n|---|---|\n| `save(options?)` | Serialize one panel's configuration |\n| `restore(config, options?)` | Apply a configuration to one panel |\n| `saveWorkspace()` | Serialize the whole element — every panel, plus layout and global filters |\n| `restoreWorkspace(config)` | Restore a whole-element configuration |\n| `reset(all?, options?)` | Reset configuration (pass `true` to also reset expressions) |\n| `resetError()` | Clear the error overlay |\n\nviewer » <perspective-viewer> Custom Element library » Methods » Panels\n\n| Method | Description |\n|---|---|\n| `addPanel(config)` | Add a panel, returning its generated id |\n| `removePanel(id)` | Remove a panel |\n| `getPanelNames()` | List panel ids |\n| `getActivePanel()` / `setActivePanel(id)` | Get or set the active panel |"},{"title":"viewer » <perspective-viewer> Custom Element library » Methods » Output","path":"viewer.md","text":"| Method | Description |\n|---|---|\n| `export(options?)` | Export a panel — see the export methods below |\n| `download(options?)` | Export and download as a file |\n| `copy(options?)` | Copy a panel to the clipboard |\n\n`export()`, `download()` and `copy()` all take a `method`, one of `\"csv\"`,\n`\"json\"`, `\"ndjson\"` or `\"arrow\"` — each with `-all` and `-selected` variants\n(e.g. `\"csv-selected\"`) — plus `\"html\"`, `\"json-config\"`, and `\"plugin\"`.\nThe `\"plugin\"` method asks the plugin to render itself, which produces a PNG\nfor charts and text for the datagrid.\n| `getSelection(options?)` / `setSelection(...)` | Get or set the selected region |\n| `getEditPort(options?)` | Get a panel's edit port |\n| `getRenderStats(options?)` | Get render timing statistics |"},{"title":"viewer » <perspective-viewer> Custom Element library » Methods » Rendering and chrome","path":"viewer.md","text":"| Method | Description |\n|---|---|\n| `flush()` | Wait for any pending UI updates to complete |\n| `resize(options?)` | Redraw, optionally at a `{dimensions: {width, height}}` size hint |\n| `setAutoSize(bool)` / `setAutoPause(bool)` / `setThrottle(ms)` | Render policy |\n| `toggleConfig(force?)` | Toggle the settings sidebar |\n| `toggleColumnSettings(...)` | Toggle the column settings sidebar |\n| `resetThemes(themes?)` | Re-detect or explicitly set available themes |\n| `restyleElement()` | Re-read CSS and repaint |\n| `getPlugin(name?)` / `getAllPlugins()` | Look up registered plugins |\n\nSee [Saving and restoring UI state](./save_restore.md) for the `save`/`restore`\nformats and the panel selector, and\n[Plugin render limits](./plugin_settings.md) for `getPlugin`."},{"title":"clickhouse » ClickHouse Virtual Server","path":"clickhouse.md","text":"Perspective provides a built-in virtual server for\n[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` to query\nClickHouse tables directly from the browser.\n\nFor server-side Python usage, see the\n[Python ClickHouse guide](../../python/virtual_server/clickhouse.md).\n\nclickhouse » ClickHouse Virtual Server » Installation\n\n```bash\nnpm install @perspective-dev/client @perspective-dev/viewer @clickhouse/client-web\n```\n\nclickhouse » ClickHouse Virtual Server » Usage\n\nConnect to a ClickHouse instance and bind it to a Perspective viewer:\n\n```javascript\nimport perspective from \"@perspective-dev/client\";\nimport \"@perspective-dev/viewer\";\nimport { createClient } from \"@clickhouse/client-web\";\n\n// Connect to ClickHouse\nconst clickhouseClient = createClient({\n    url: \"http://localhost:8123\",\n    database: \"default\",\n});\n\n// Create a Perspective virtual server backed by ClickHouse\nconst handler = perspective.ClickhouseHandler(clickhouseClient);\nconst messageHandler = perspective.createMessageHandler(handler);\n\n// Connect a viewer\nconst client = await perspective.worker(messageHandler);\nconst table = await client.open_table(\"my_table\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"clickhouse » ClickHouse Virtual Server » Examples","path":"clickhouse.md","text":"- [Browser ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-clickhouse-virtual)"},{"title":"custom » Implementing a custom Virtual Server","path":"custom.md","text":"You can connect Perspective to any data source by implementing the\n`VirtualServerHandler` interface and passing it to `createMessageHandler()`.\n\nFor background on virtual servers, see the\n[Virtual Servers overview](../../../explanation/virtual_servers.md).\n\ncustom » Implementing a custom Virtual Server » Example\n\n```typescript\nimport perspective from \"@perspective-dev/client\";\nimport type {\n    VirtualServerHandler,\n    ColumnType,\n    ViewConfig,\n    ViewWindow,\n    VirtualDataSlice,\n} from \"@perspective-dev/client\";\n\nconst handler = {\n    async getHostedTables(): Promise<string[]> {\n        return [\"my_table\"];\n    },\n\n    async tableSchema(tableId: string): Promise<Record<string, ColumnType>> {\n        return { name: \"string\", price: \"float\", date: \"date\" };\n    },\n\n    async tableSize(tableId: string): Promise<number> {\n        return 1000;\n    },\n\n    async tableMakeView(\n        tableId: string,\n        viewId: string,\n        config: ViewConfig,\n    ): Promise<void> {\n        // Translate `config` (group_by, sort, filter, etc.) into a query\n        // against your data source. Store the query keyed by `viewId`\n        // for later data retrieval.\n    },\n\n    async viewDelete(viewId: string): Promise<void> {\n        // Clean up resources for this view\n    },\n\n    async viewGetData(\n        viewId: string,\n        config: ViewConfig,\n        schema: Record<string, ColumnType>,\n        viewport: ViewWindow,\n        dataSlice: VirtualDataSlice,\n    ): Promise<void> {\n        // Query your data source using `config` and `viewport` for the\n        // row/column window. Push columnar results via `dataSlice.setCol()`.\n    },\n\n    getFeatures() {\n        return {\n            group_by: true,\n            sort: true,\n            filter_ops: {\n                string: [\"==\", \"!=\", \"contains\", \"is null\", \"is not null\"],\n                float: [\"==\", \"!=\", \">\", \"<\", \">=\", \"<=\"],\n            },\n            aggregates: {\n                float: [\"sum\", \"avg\", \"count\", \"min\", \"max\"],\n                string: [\"count\", \"any\"],\n            },\n        };\n    },\n} satisfies VirtualServerHandler;\n\n// Create a message handler and use it like a worker\nconst messageHandler = perspective.createMessageHandler(handler);\nconst client = await perspective.worker(messageHandler);\nconst table = await client.open_table(\"my_table\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"duckdb » DuckDB Virtual Server (1)","path":"duckdb.md","text":"Perspective provides a built-in virtual server for\n[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` to query\nDuckDB-WASM databases directly in the browser.\n\nFor server-side Python usage, see the\n[Python DuckDB guide](../../python/virtual_server/duckdb.md).\n\nduckdb » DuckDB Virtual Server » Installation\n\n```bash\nnpm install @perspective-dev/client @perspective-dev/viewer @duckdb/duckdb-wasm\n```\n\nduckdb » DuckDB Virtual Server » Usage\n\nInitialize DuckDB-WASM, load data, and connect it to a Perspective viewer:\n\n`DuckDBHandler` is an optional submodule and is _not_ exported from the package\nroot, so it must be imported by path. It takes an `AsyncDuckDBConnection` — the\nresult of `db.connect()` — not the `AsyncDuckDB` itself.\n\n```javascript\nimport perspective, { createMessageHandler } from \"@perspective-dev/client\";\nimport \"@perspective-dev/viewer\";\nimport * as duckdb from \"@duckdb/duckdb-wasm\";\nimport { DuckDBHandler } from \"@perspective-dev/client/dist/esm/virtual_servers/duckdb.js\";\n\n// Initialize DuckDB-WASM\nconst DUCKDB_BUNDLES = duckdb.getJsDelivrBundles();\nconst bundle = await duckdb.selectBundle(DUCKDB_BUNDLES);\nconst worker_url = URL.createObjectURL(\n    new Blob([`importScripts(\"${bundle.mainWorker}\");`], {\n        type: \"text/javascript\",\n    }),\n);\n\nconst worker = new Worker(worker_url);\nconst logger = new duckdb.ConsoleLogger();\nconst db = new duckdb.AsyncDuckDB(logger, worker);\nawait db.instantiate(bundle.mainModule, bundle.pthreadWorker);\nURL.revokeObjectURL(worker_url);\n\n// Load data into DuckDB. This pragma is required to match Perspective's\n// sort-null semantics.\nconst conn = await db.connect();\nawait conn.query(`SET default_null_order=NULLS_FIRST_ON_ASC_LAST_ON_DESC;`);\nawait conn.query(`CREATE TABLE my_table AS SELECT * FROM 'data.parquet'`);\n\n// Create a Perspective virtual server backed by DuckDB\nconst messageHandler = await createMessageHandler(new DuckDBHandler(conn));\n\n// Connect a viewer. Table ids are database-qualified, so a table created as\n// `my_table` is hosted as `memory.my_table`.\nconst client = await perspective.worker(messageHandler);\nconst viewer = document.getElementById(\"viewer\");\nviewer.load(client);\nviewer.restore({ table: \"memory.my_table\" });\n```"},{"title":"duckdb » DuckDB Virtual Server (2)","path":"duckdb.md","text":"<div class=\"warning\">In the browser, <code>DuckDBHandler</code> resolves\nPerspective's WASM module from the registered\n<code>&lt;perspective-viewer&gt;</code> custom element, so it cannot be\nconstructed until that element has been defined. Off-browser, pass the module\nexplicitly as the second constructor argument.</div>\n\nPerspective never intercepts your SQL — it only discovers what `SHOW ALL\nTABLES` reports — so DuckDB's own remote-data features are available directly:\n\n```javascript\nawait conn.query(`CREATE SECRET (TYPE s3, KEY_ID '...', SECRET '...', REGION 'us-east-1')`);\nawait conn.query(`CREATE TABLE trades AS SELECT * FROM read_parquet('s3://bucket/trades/*.parquet')`);\n```"},{"title":"duckdb » DuckDB Virtual Server » Examples","path":"duckdb.md","text":"- [Browser DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/esbuild-duckdb-virtual)"},{"title":"virtual_server » Virtual Servers","path":"virtual_server.md","text":"Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to\nexternal data sources without loading data into Perspective's built-in engine.\nInstead, queries are translated and executed natively by the external database.\n\nFor a detailed explanation of how virtual servers work, see the\n[Virtual Servers](../../explanation/virtual_servers.md) concepts page.\n\nPerspective ships with built-in virtual server implementations for:\n\n- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases in-browser\n  via `@duckdb/duckdb-wasm`, or on the server via Node.js.\n- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server\n  directly from the browser or from Node.js.\n\nYou can also [**implement your own**](./virtual_server/custom.md) virtual server\nto connect Perspective to any data source by implementing the\n`VirtualServerHandler` interface."},{"title":"worker » Accessing the Perspective engine via a Client instance","path":"worker.md","text":"An instance of a `Client` is needed to talk to a Perspective `Server`, of which\nthere are a few varieties available in JavaScript.\n\nworker » Accessing the Perspective engine via a Client instance » Web Worker (Browser)\n\nPerspective's Web Worker client is actually a `Client` and `Server` rolled into\none. Instantiating this `Client` will also create a _dedicated_ Perspective\n`Server` in a Web Worker process.\n\nTo use it, you'll need to instantiate a Web Worker `perspective` engine via the\n`worker()` method. This will create a new Web Worker (browser) and load the\nWebAssembly binary. All calculation and data accumulation will occur in this\nseparate process.\n\n```javascript\nconst client = await perspective.worker();\n```\n\nThe `worker` symbol will expose the full `perspective` API for one managed Web\nWorker process. You are free to create as many as your browser supports, but be\nsure to keep track of the `worker` instances themselves, as you'll need them to\ninteract with your data in each instance."},{"title":"worker » Accessing the Perspective engine via a Client instance » Websocket (Browser)","path":"worker.md","text":"Alternatively, with a Perspective server running in Node.js, Python or Rust, you\ncan create a _virtual_ `Client` via the `websocket()` method.\n\n```javascript\nconst client = perspective.websocket(\"http://localhost:8080/\");\n```\n\nworker » Accessing the Perspective engine via a Client instance » Node.js\n\nThe Node.js runtime for the `@perspective-dev/client` module runs in-process by\ndefault and does not implement a `child_process` interface, so no need to call\nthe `.worker()` factory function. Instead, the `perspective` library exports the\nfunctions directly and run synchronously in the main process.\n\n```javascript\nconst client = require(\"@perspective-dev/client\");\n```"},{"title":"callbacks » Callbacks and Events","path":"callbacks.md","text":"`perspective.Table` allows for `on_update` and `on_delete` callbacks to be\nset—simply call `on_update` or `on_delete` with a reference to a function or a\nlambda without any parameters:\n\n```python\ndef update_callback():\n    print(\"Updated!\")\n\n# set the update callback\non_update_id = view.on_update(update_callback)\n\n\ndef delete_callback():\n    print(\"Deleted!\")\n\n# set the delete callback\non_delete_id = view.on_delete(delete_callback)\n\n# set a lambda as a callback\nview.on_delete(lambda: print(\"Deleted x2!\"))\n```\n\nIf the callback is a named reference to a function, it can be removed with\n`remove_update` or `remove_delete`:\n\n```python\nview.remove_update(on_update_id)\nview.remove_delete(on_delete_id)\n```\n\nCallbacks defined with a lambda function cannot be removed, as lambda functions\nhave no identifier."},{"title":"installation » Installation","path":"installation.md","text":"`perspective-python` contains full bindings to the Perspective API, a JupyterLab\nwidget, and WebSocket handlers for several webserver libraries that allow you to\nhost Perspective using server-side Python.\n\ninstallation » Installation » PyPI\n\n`perspective-python` can be installed from [PyPI](https://pypi.org) via `pip`:\n\n```bash\npip install perspective-python\n```\n\nThat's it! If JupyterLab is installed in this Python environment, you'll also\nget the `perspective.widget.PerspectiveWidget` class when you import\n`perspective` in a Jupyter Lab kernel.\n\n<!--"},{"title":"installation » Installation » PyPI » Anaconda","path":"installation.md","text":"`perspective-python` can also be installed for [Anaconda](https://anaconda.org/)\nvia [Conda Forge](https://conda-forge.org)\n\n```bash\nconda install -c conda-forge perspective\n``` -->"},{"title":"join » Joining Tables","path":"join.md","text":"`perspective.join()` creates a read-only `Table` by joining two source tables on\na shared key column. The result is reactive — it updates automatically when\neither source table changes. See [`Join`](../../explanation/join.md) for\nconceptual details.\n\njoin » Joining Tables » Basic Inner Join\n\n```python\norders = perspective.table([\n    {\"id\": 1, \"product_id\": 101, \"qty\": 5},\n    {\"id\": 2, \"product_id\": 102, \"qty\": 3},\n    {\"id\": 3, \"product_id\": 101, \"qty\": 7},\n])\n\nproducts = perspective.table([\n    {\"product_id\": 101, \"name\": \"Widget\"},\n    {\"product_id\": 102, \"name\": \"Gadget\"},\n])\n\njoined = perspective.join(orders, products, \"product_id\")\nview = joined.view()\njson = view.to_json()\n```"},{"title":"join » Joining Tables » Join Types","path":"join.md","text":"Pass `join_type` to select inner, left, or outer join behavior:\n\n```python\n# Left join: all left rows, nulls for unmatched right columns\nleft_joined = perspective.join(left, right, \"id\", join_type=\"left\")\n\n# Outer join: all rows from both tables\nouter_joined = perspective.join(left, right, \"id\", join_type=\"outer\")\n```\n\njoin » Joining Tables » Reactive Updates\n\nThe joined table recomputes automatically when either source table is updated:\n\n```python\nleft = perspective.table([{\"id\": 1, \"x\": 10}])\nright = perspective.table([{\"id\": 2, \"y\": \"b\"}])\n\njoined = perspective.join(left, right, \"id\")\nview = joined.view()\n\njson = view.to_json()\n# [] — no matching keys yet\n\nright.update([{\"id\": 1, \"y\": \"a\"}])\njson = view.to_json()\n# [{\"id\": 1, \"x\": 10, \"y\": \"a\"}] — new match detected\n```"},{"title":"join » Joining Tables » Async Client","path":"join.md","text":"The async client has the same API:\n\n```python\njoined = await client.join(orders, products, \"product_id\", join_type=\"left\")\n```"},{"title":"jupyterlab » PerspectiveWidget for notebooks","path":"jupyterlab.md","text":"Building on top of the API provided by `perspective.Table`, the\n`PerspectiveWidget` offers the entire functionality of Perspective within a\nnotebook environment. It supports the same API semantics of\n`<perspective-viewer>`, along with the additional data types supported by\n`perspective.Table`.\n\njupyterlab » PerspectiveWidget for notebooks » Installation\n\n`PerspectiveWidget` is an [AnyWidget](https://anywidget.dev), shipped as a\nprebuilt bundle inside the `perspective-python` wheel. There is no separate\nlabextension to install or version-match — install the `jupyter` extra, which\nadds the `anywidget` dependency:\n\n```bash\npip install \"perspective-python[jupyter]\"\n```\n\nThe same wheel works in JupyterLab, classic Jupyter Notebook, VSCode\nnotebooks, Google Colab and Marimo.\n\n<div class=\"warning\">The <code>@perspective-dev/jupyterlab</code> package is\nnow <em>optional</em> and no longer ships the widget. It provides only the\n\"Open With &rarr; Perspective\" file renderers for <code>csv</code>,\n<code>json</code> and <code>arrow</code> files in JupyterLab.</div>"},{"title":"jupyterlab » PerspectiveWidget for notebooks » Usage","path":"jupyterlab.md","text":"`PerspectiveWidget` takes keyword arguments for the managed `View`:\n\n```python\nfrom perspective.widget import PerspectiveWidget\nw = perspective.PerspectiveWidget(\n    data,\n    plugin=\"X Bar\",\n    aggregates={\"datetime\": \"any\"},\n    sort=[[\"date\", \"desc\"]]\n)\n```\n\njupyterlab » PerspectiveWidget for notebooks » Creating a widget\n\nA widget is created through the `PerspectiveWidget` constructor, which takes as\nits first, required parameter a `perspective.Table`, a dataset, a schema, or\n`None`, which serves as a special value that tells the Widget to defer loading\nany data until later. In maintaining consistency with the Javascript API,\nWidgets cannot be created with empty dictionaries or lists — `None` should be\nused if the intention is to await data for loading later on. A widget can be\nconstructed from a dataset:\n\n```python\nfrom perspective.widget import PerspectiveWidget\nPerspectiveWidget(data, group_by=[\"date\"])\n```\n\n.. or a schema:\n\n```python\nPerspectiveWidget({\"a\": int, \"b\": str})\n```\n\n.. or an instance of a `perspective.Table`:\n\n```python\ntable = perspective.table(data)\nPerspectiveWidget(table)\n```"},{"title":"jupyterlab » PerspectiveWidget for notebooks » Updating a widget","path":"jupyterlab.md","text":"`PerspectiveWidget` shares a similar API to the `<perspective-viewer>` Custom\nElement, and has similar `save()` and `restore()` methods that\nserialize/deserialize UI state for the widget.\n\njupyterlab » PerspectiveWidget for notebooks » PerspectiveRenderer\n\nThe optional `@perspective-dev/jupyterlab` package exposes a JS-only\n`mimerender-extension`. This lets you view `csv`, `json`, and `arrow` files\ndirectly from the JupyterLab file browser — right-click one of these files and\nchoose `Open With → Perspective`.\n\n```bash\njupyter labextension install @perspective-dev/jupyterlab\n```\n\nThis package is independent of `PerspectiveWidget`; install it only if you\nwant the file renderers."},{"title":"multithreading » Multi-threading","path":"multithreading.md","text":"Perspective's API is thread-safe, so methods may be called from different\nthreads without additional consideration for safety/exclusivity/correctness. All\n`perspective.Client` and `perspective.Server` API methods release the GIL, which\ncan be exploited for parallelism.\n\nInterally, `perspective.Server` also dispatches to a thread pool for some\noperations, enabling better parallelism and overall better query performance.\nThis independent threadpool size can be controlled via\n`perspective.set_num_cpus()`, or the `OMP_NUM_THREADS` environment variable.\n\n```python\nimport perspective\n\nperspective.set_num_cpus(2)\n```"},{"title":"multithreading » Multi-threading » Server handlers","path":"multithreading.md","text":"Perspective's server handler implementations each take an optional `executor`\nconstructor argument, which (when provided) will configure the handler to\nprocess WebSocket `Client` requests on a thread pool.\n\n```python\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tornado.web import Application\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\nfrom perspective import Server\n\nargs = {\"perspective_server\": Server(), \"executor\": ThreadPoolExecutor()}\n\napp = Application(\n    [\n        (r\"/websocket\", PerspectiveTornadoHandler, args),\n\n        # ...\n\n    ]\n)\n```"},{"title":"multithreading » Multi-threading » on_poll_request","path":"multithreading.md","text":"`on_poll_request` is an optional keyword argument for `Server()`, which which\ncan be applied in cases where overlapping `Table.update` calls can be safely\ndeferred.\n\nWhen providing a callback function to `on_poll_request`, the `Server` will\ninvoke your callback when there are updates that need to be flushed, after which\nyou must _eventually_ call `Server.poll` (or else no updates will be processed).\n\nThe exact implementation of `on_poll_request` will depend on the context. A\nsimple example which batches calls via `threading.Lock`:\n\n```python\nlock = threading.Lock()\n\ndef on_poll_request(perspective_server):\n    if lock.acquire(blocking=False):\n        try:\n            perspective_server.poll()\n        finally:\n            lock.release()\n\nserver = Server(on_poll_request=on_poll_request)\n```"},{"title":"table » Loading data into a Table","path":"table.md","text":"A `Table` can be created from a dataset or a schema, the specifics of which are\n[discussed](#loading-data-with-table) in the JavaScript section of the user's\nguide. In Python, however, Perspective supports additional data types that are\ncommonly used when processing data:\n\n- `pandas.DataFrame`\n- `polars.DataFrame`\n- `bytes` (encoding an Apache Arrow)\n- `objects` (either extracting a repr or via reference)\n- `str` (encoding as a CSV)\n\nA `Table` is created in a similar fashion to its JavaScript equivalent:\n\n```python\nfrom datetime import date, datetime\nimport numpy as np\nimport pandas as pd\nimport perspective\n\ndata = pd.DataFrame({\n    \"int\": np.arange(100),\n    \"float\": [i * 1.5 for i in range(100)],\n    \"bool\": [True for i in range(100)],\n    \"date\": [date.today() for i in range(100)],\n    \"datetime\": [datetime.now() for i in range(100)],\n    \"string\": [str(i) for i in range(100)]\n})\n\ntable = perspective.table(data, index=\"float\")\n```\n\nLikewise, a `View` can be created via the `view()` method:\n\n```python\nview = table.view(group_by=[\"float\"], filter=[[\"bool\", \"==\", True]])\ncolumn_data = view.to_columns()\nrow_data = view.to_json()\n```"},{"title":"table » Loading data into a Table » Polars Support","path":"table.md","text":"Polars `DataFrame` types work similarly to Apache Arrow input, which Perspective\nuses to interface with Polars.\n\n```python\ndf = polars.DataFrame({\"a\": [1,2,3,4,5]})\ntable = perspective.table(df)\n```\n\ntable » Loading data into a Table » Pandas Support\n\nPerspective's `Table` can be constructed from `pandas.DataFrame` objects.\nInternally, this just uses\n[`pyarrow::from_pandas`](https://arrow.apache.org/docs/python/pandas.html),\nwhich dictates behavior of this feature including type support.\n\nIf the dataframe does not have an index set, an integer-typed column named\n`\"index\"` is created. If you want to preserve the indexing behavior of the\ndataframe passed into Perspective, simply create the `Table` with\n`index=\"index\"` as a keyword argument. This tells Perspective to once again\ntreat the index as a primary key:\n\n```python\ndata.set_index(\"datetime\")\ntable = perspective.table(data, index=\"index\")\n```"},{"title":"table » Loading data into a Table » Time Zone Handling","path":"table.md","text":"When parsing `\"datetime\"` strings, times without an explicit timezone offset are\ninterpreted as _UTC_. Strings with a timezone offset (e.g., `+05:00`) are\nconverted to UTC. All `\"datetime\"` values are stored internally as milliseconds\nsince the Unix epoch, and are _output_ as integer timestamps (milliseconds since\nepoch) from methods like `to_columns()` and `to_json()`.\n\nPython `datetime` objects are serialized to strings before parsing. Naive\n`datetime` objects (without `tzinfo`) produce strings without timezone\ninformation and are therefore treated as UTC. Timezone-aware `datetime` objects\ninclude their offset in the serialized string, which is used to convert to UTC.\n\n`\"date\"` values are timezone-agnostic calendar days with no time component.\nThey are _output_ as integer timestamps at _UTC midnight_ of the calendar day\n(equivalent to Arrow `date32` day arithmetic), and integer timestamp _input_ to\na `\"date\"` column is likewise interpreted as UTC. The host process timezone\nnever affects `\"date\"` values — a `Viewer` renders them in UTC, recovering the\nstored calendar day exactly. Datetime expression functions such as\n`bucket(\"x\", 'D')`, `day_of_week(\"x\")` and `hour_of_day(\"x\")` also compute in\nUTC."},{"title":"table_data » DataFrame and Arrow Compatibility","path":"table_data.md","text":"`perspective-python` accepts a `Table` constructor argument from any of the\ncommon Python columnar data libraries. In all three cases, `perspective.table`\n(and `Table.update()`) consume the input directly — there is no need to\nserialize to Apache Arrow IPC bytes yourself. However, note is\nstill the most efficient way to bulk load data into `Table`.\n\ntable_data » DataFrame and Arrow Compatibility » PyArrow\n\n```python\nimport pyarrow as pa\nimport perspective\n\narrow_table = pa.table({\n    \"int\": pa.array([1, 2, 3], type=pa.int64()),\n    \"float\": pa.array([1.5, 2.5, 3.5], type=pa.float64()),\n    \"string\": pa.array([\"a\", \"b\", \"c\"], type=pa.string()),\n})\n\ntable = perspective.table(arrow_table)\n```\n\nThe same applies to `Table.update()`:\n\n```python\ntable.update(arrow_table)\n```\n\nIf you have Arrow data already in IPC format (e.g. read from disk, received\nover the wire, or produced by another tool), pass the raw `bytes` directly —\nboth stream and file formats are auto-detected:\n\n```python\nwith open(\"data.arrow\", \"rb\") as f:\n    table = perspective.table(f.read())\n```"},{"title":"table_data » DataFrame and Arrow Compatibility » PyArrow » Nested columns","path":"table_data.md","text":"Perspective's data model is flat, so Arrow `struct` and `list` columns are\nnormalized on ingest.\n\nA `struct` column is hoisted into one dotted column per leaf, recursively. A\nnull parent nulls every descendant leaf:\n\n```python\narrow_table = pa.table({\n    \"id\": pa.array([1, 2], type=pa.int64()),\n    \"s\": pa.array([{\"a\": 10}, {\"a\": 20}], type=pa.struct([(\"a\", pa.int64())])),\n})\n\n# Schema is `{\"id\": \"integer\", \"s.a\": \"integer\"}`\ntable = perspective.table(arrow_table)\n```\n\nBecause the flattened names are ordinary columns, a `Table` created from an\nexplicit schema accepts nested updates with no further configuration:\n\n```python\ntable = perspective.table({\"id\": \"integer\", \"s.a\": \"integer\"})\ntable.update(arrow_table)\n```\n\nA `list` column is controlled by the `list_flatten` argument:\n\n-   `\"zip\"` (default) expands a row into one row per list element, repeating\n    its non-list siblings. An empty or null list yields a single row with a\n    null in that column, rather than dropping the row. When a row has more than\n    one list column, their non-empty lengths must match.\n-   `\"cartesian\"` expands a row into the product of its list columns' lengths,\n    with an empty or null list counting as a single null element.\n-   `\"stringify\"` encodes each list as a JSON array in a single string column,\n    leaving the row count unchanged.\n\n```python\narrow_table = pa.table({\n    \"x\": pa.array([1, 2], type=pa.int64()),\n    \"y\": pa.array([[10, 20], [30]], type=pa.list_(pa.int64())),\n})\n\n# `{\"x\": [1, 1, 2], \"y\": [10, 20, 30]}`\nperspective.table(arrow_table)\n\n# `{\"x\": [1, 2], \"y\": [\"[10,20]\", \"[30]\"]}`\nperspective.table(arrow_table, list_flatten=\"stringify\")\n```"},{"title":"table_data » DataFrame and Arrow Compatibility » Polars","path":"table_data.md","text":"```python\nimport polars as pl\nimport perspective\n\ndf = pl.DataFrame({\n    \"a\": [1, 2, 3, 4, 5],\n    \"b\": [\"x\", \"y\", \"z\", \"x\", \"y\"],\n})\n\ntable = perspective.table(df)\n```\n\nInternally, the `DataFrame` is converted to a `pyarrow.Table` before\ningestion, so Polars columns inherit the Arrow type mapping above.\n\nSee also Perspective [Virtual Server support for `polars.DataFrame`](./virtual_server/polars.md)\n\ntable_data » DataFrame and Arrow Compatibility » Pandas\n\n`pandas.DataFrame` is supported via `pyarrow.Table.from_pandas`, which\ndictates behavior including type support — see the\n[pyarrow pandas docs](https://arrow.apache.org/docs/python/pandas.html) for\ndetails on which pandas dtypes round-trip cleanly.\n\n```python\nfrom datetime import date, datetime\nimport numpy as np\nimport pandas as pd\nimport perspective\n\ndata = pd.DataFrame({\n    \"int\": np.arange(100),\n    \"float\": [i * 1.5 for i in range(100)],\n    \"bool\": [True for i in range(100)],\n    \"date\": [date.today() for i in range(100)],\n    \"datetime\": [datetime.now() for i in range(100)],\n    \"string\": [str(i) for i in range(100)],\n})\n\ntable = perspective.table(data, index=\"float\")\n```"},{"title":"clickhouse » ClickHouse Virtual Server","path":"clickhouse.md","text":"Perspective provides a built-in virtual server for\n[ClickHouse](https://clickhouse.com/), allowing `<perspective-viewer>` clients\nto query a ClickHouse server over WebSocket.\n\nFor browser-only usage, see the\n[JavaScript ClickHouse guide](../../javascript/virtual_server/clickhouse.md).\n\nclickhouse » ClickHouse Virtual Server » Installation\n\n```bash\npip install perspective-python clickhouse-connect\n```\n\nclickhouse » ClickHouse Virtual Server » Usage\n\nCreate a server that exposes ClickHouse tables to browser clients:\n\n```python\nimport clickhouse_connect\nimport tornado.web\nimport tornado.ioloop\nfrom perspective.virtual_servers.clickhouse import ClickhouseVirtualServer\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\n# Connect to ClickHouse\nclient = clickhouse_connect.get_client(host=\"localhost\")\n\n# Create virtual server backed by ClickHouse\nserver = ClickhouseVirtualServer(client)\n\n# Serve over WebSocket\napp = tornado.web.Application([\n    (r\"/websocket\", PerspectiveTornadoHandler, {\"perspective_server\": server}),\n])\n\napp.listen(8080)\ntornado.ioloop.IOLoop.current().start()\n```\n\nConnect from the browser:\n\n```javascript\nconst websocket = await perspective.websocket(\"ws://localhost:8080/websocket\");\nconst table = await websocket.open_table(\"my_table\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"clickhouse » ClickHouse Virtual Server » Examples","path":"clickhouse.md","text":"- [Python ClickHouse example](https://github.com/perspective-dev/perspective/tree/master/examples/python-clickhouse-virtual)"},{"title":"custom » Implementing a custom Virtual Server","path":"custom.md","text":"You can connect Perspective to any data source by subclassing\n`VirtualServerHandler`, wrapping it in a `VirtualServer`, and exposing that\nvia a small _session factory_ object which the WebSocket handlers use to give\neach connected client its own session.\n\nFor background on virtual servers, see the\n[Virtual Servers overview](../../../explanation/virtual_servers.md).\n\ncustom » Implementing a custom Virtual Server » The handler\n\n`VirtualServerHandler` is imported from `perspective.virtual_servers`. Only\n`get_hosted_tables`, `table_schema`, `table_size`, `table_make_view`,\n`view_delete` and `view_get_data` are required; the rest have defaults.\n\n```python\nfrom perspective.virtual_servers import VirtualServerHandler\n\nclass MyHandler(VirtualServerHandler):\n    def __init__(self, db):\n        self.db = db\n\n    def get_features(self):\n        return {\n            \"group_by\": True,\n            \"split_by\": False,\n            \"sort\": True,\n            \"filter_ops\": {\n                \"string\": [\"==\", \"!=\", \"contains\"],\n                \"float\": [\"==\", \"!=\", \">\", \"<\"],\n            },\n            \"aggregates\": {\n                \"float\": [\"sum\", \"avg\", \"count\"],\n                \"string\": [\"count\"],\n            },\n        }\n\n    def get_hosted_tables(self):\n        return [\"my_table\"]\n\n    def table_schema(self, table_name):\n        return {\"name\": \"string\", \"price\": \"float\"}\n\n    def table_size(self, table_name):\n        return 1000\n\n    def table_make_view(self, table_name, view_name, config):\n        # Translate `config` (group_by, sort, filter, etc.) into a query\n        # against your data source. Store the query keyed by `view_name`\n        # for later data retrieval.\n        pass\n\n    def view_delete(self, view_name):\n        # Clean up resources for this view. The UI does this automatically,\n        # and can recover if a view dies early.\n        pass\n\n    def view_get_data(self, view_name, config, viewport, data):\n        # Serialize the rectangular slice `viewport` of the temporary table\n        # `view_name` into `data`, a push-only `VirtualDataSlice`. Once a\n        # type has been pushed for a column name it must not change.\n        pass\n```"},{"title":"custom » Implementing a custom Virtual Server » The handler » Optional methods","path":"custom.md","text":"| Method | Default | Purpose |\n| --- | --- | --- |\n| `get_features()` | `columns` only | Which UI controls to enable — see [Features declaration](../../../explanation/virtual_servers.md#features-declaration) |\n| `view_schema(view_name, config)` | `table_schema` | Schema of a temporary table, when it differs from its source |\n| `view_size(view_name)` | `table_size` | Row count of a temporary table, when it differs from its source |\n| `table_validate_expression(view_name, expression)` | allow all | Type-check an expression column; enabled by `\"expressions\"` in `get_features` |\n| `view_get_min_max(view_name, column_name, config)` | unsupported | Column bounds as a `(min, max)` tuple — required for gradient and sparkbar column styles |"},{"title":"custom » Implementing a custom Virtual Server » The session factory","path":"custom.md","text":"The WebSocket handlers call `new_session(callback)` once per connection, so\nthe object passed as `perspective_server` must provide it. Wrap your handler\nin a `perspective.VirtualServer` — which owns the protocol — and return one\nsession per client:\n\n```python\nimport perspective\n\nclass MyVirtualSession:\n    def __init__(self, callback, db):\n        self.session = perspective.VirtualServer(MyHandler(db))\n        self.callback = callback\n\n    def handle_request(self, msg):\n        self.callback(self.session.handle_request(msg))\n\n\nclass MyVirtualServer:\n    def __init__(self, db):\n        self.db = db\n\n    def new_session(self, callback):\n        return MyVirtualSession(callback, self.db)\n```"},{"title":"custom » Implementing a custom Virtual Server » Serving it","path":"custom.md","text":"A `MyVirtualServer` instance can then be passed to a Tornado, Starlette or\nAIOHTTP handler just like a regular `Server`:\n\n```python\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\napp = tornado.web.Application([\n    (r\"/websocket\", PerspectiveTornadoHandler, {\n        \"perspective_server\": MyVirtualServer(db),\n    }),\n])\n```\n\nThe built-in [DuckDB](./duckdb.md), [ClickHouse](./clickhouse.md) and\n[Polars](./polars.md) implementations all follow exactly this shape and are\nworth reading as complete references."},{"title":"duckdb » DuckDB Virtual Server","path":"duckdb.md","text":"Perspective provides a built-in virtual server for\n[DuckDB](https://duckdb.org/), allowing `<perspective-viewer>` clients to query\na server-side DuckDB database over WebSocket.\n\nFor browser-only usage via DuckDB-WASM, see the\n[JavaScript DuckDB guide](../../javascript/virtual_server/duckdb.md).\n\nduckdb » DuckDB Virtual Server » Installation\n\n```bash\npip install perspective-python duckdb\n```\n\nduckdb » DuckDB Virtual Server » Usage\n\nCreate a server that exposes a DuckDB database to browser clients:\n\n```python\nimport duckdb\nimport tornado.web\nimport tornado.ioloop\nfrom perspective.virtual_servers.duckdb import DuckDBVirtualServer\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\n# Create DuckDB connection and load data\nconn = duckdb.connect()\nconn.execute(\"CREATE TABLE my_table AS SELECT * FROM 'data.parquet'\")\n\n# Create virtual server backed by DuckDB\nserver = DuckDBVirtualServer(conn)\n\n# Serve over WebSocket\napp = tornado.web.Application([\n    (r\"/websocket\", PerspectiveTornadoHandler, {\"perspective_server\": server}),\n])\n\napp.listen(8080)\ntornado.ioloop.IOLoop.current().start()\n```\n\nConnect from the browser:\n\n```javascript\nconst websocket = await perspective.websocket(\"ws://localhost:8080/websocket\");\nconst table = await websocket.open_table(\"my_table\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"duckdb » DuckDB Virtual Server » Window functions","path":"duckdb.md","text":"Window columns are DuckDB's own functions, under their DuckDB names — the\nadvertised name is emitted into the `OVER` clause verbatim.\n\n| | |\n| --- | --- |\n| Aggregating | `sum` `avg` `count` `min` `max` `product` `median` |\n| Deviation / variance | `stddev_samp` `stddev_pop` `var_samp` `var_pop` |\n| Navigation | `first_value` `last_value` `nth_value` `lag` `lead` |\n| Ranking | `row_number` `rank` `dense_rank` `percent_rank` `cume_dist` `ntile` |\n| Perspective's own | `diff` `rate` |\n\nduckdb » DuckDB Virtual Server » Examples\n\n- [Python DuckDB example](https://github.com/perspective-dev/perspective/tree/master/examples/python-duckdb-virtual)"},{"title":"polars » Polars Virtual Server","path":"polars.md","text":"Perspective provides a built-in virtual server for\n[Polars](https://pola.rs/), allowing `<perspective-viewer>` clients to query\nin-memory Polars DataFrames over WebSocket.\n\npolars » Polars Virtual Server » Installation\n\n```bash\npip install perspective-python polars\n```\n\npolars » Polars Virtual Server » Usage\n\nCreate a server that exposes Polars DataFrames to browser clients:\n\n```python\nimport polars as pl\nimport tornado.web\nimport tornado.ioloop\nfrom perspective.virtual_servers.polars import PolarsVirtualServer\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\n# Load data into Polars DataFrames\ndf = pl.read_parquet(\"data.parquet\")\n\n# Create virtual server backed by Polars (dict of name -> DataFrame)\nserver = PolarsVirtualServer({\"my_table\": df})\n\n# Serve over WebSocket\napp = tornado.web.Application([\n    (r\"/websocket\", PerspectiveTornadoHandler, {\"perspective_server\": server}),\n])\n\napp.listen(8080)\ntornado.ioloop.IOLoop.current().start()\n```\n\nConnect from the browser:\n\n```javascript\nconst websocket = await perspective.websocket(\"ws://localhost:8080/websocket\");\nconst table = await websocket.open_table(\"my_table\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"polars » Polars Virtual Server » Examples","path":"polars.md","text":"- [Python Polars example](https://github.com/perspective-dev/perspective/tree/master/examples/python-polars-virtual)"},{"title":"virtual_server » Virtual Servers","path":"virtual_server.md","text":"Perspective's Virtual Server feature lets you connect `<perspective-viewer>` to\nexternal data sources without loading data into Perspective's built-in engine.\nInstead, queries are translated and executed natively by the external database.\n\nFor a detailed explanation of how virtual servers work, see the\n[Virtual Servers](../../explanation/virtual_servers.md) concepts page.\n\nPerspective ships with built-in virtual server implementations for:\n\n- [**DuckDB**](./virtual_server/duckdb.md) — query DuckDB databases using the\n  `duckdb` Python package.\n- [**ClickHouse**](./virtual_server/clickhouse.md) — query a ClickHouse server\n  using the `clickhouse-connect` Python package.\n- [**Polars**](./virtual_server/polars.md) — query in-memory Polars DataFrames\n  using the `polars` Python package.\n\nYou can also [**implement your own**](./virtual_server/custom.md) virtual server\nto connect Perspective to any data source by subclassing `VirtualServerHandler`."},{"title":"websocket » Hosting a WebSocket server","path":"websocket.md","text":"An in-memory `Server` \"hosts\" all `perspective.Table` and `perspective.View`\ninstances created by its connected `Client`s. Hosted tables/views can have their\nmethods called from other sources than the Python server, i.e. by a\n`perspective-viewer` running in a JavaScript client over the network,\ninterfacing with `perspective-python` through the websocket API.\n\nThe server has full control of all hosted `Table` and `View` instances, and can\ncall any public API method on hosted instances. This makes it extremely easy to\nstream data to a hosted `Table` using `.update()`:\n\n```python\nserver = perspective.Server()\nclient = server.new_local_client()\ntable = client.table(data, name=\"data_source\")\n\nfor i in range(10):\n    # updates continue to propagate automatically\n    table.update(new_data)\n```\n\nThe `name` provided is important, as it enables Perspective in JavaScript to\nlook up a `Table` and get a handle to it over the network. Otherwise, `name`\nwill be assigned randomly and the `Client` must look this up with\n`Client.get_hosted_table_names()`"},{"title":"websocket » Hosting a WebSocket server » Client/Server Replicated Mode (1)","path":"websocket.md","text":"Using Tornado and\n[`PerspectiveTornadoHandler`](../../explanation/python.md#whats-included), as well as\n`Perspective`'s JavaScript library, we can set up \"distributed\" Perspective\ninstances that allows multiple browser `perspective-viewer` clients to read from\na common `perspective-python` server, as in the\n[Tornado Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-tornado).\n\nThis architecture works by maintaining two `Tables`—one on the server, and one\non the client that mirrors the server's `Table` automatically using `on_update`.\nAll updates to the table on the server are automatically applied to each client,\nwhich makes this architecture a natural fit for streaming dashboards and other\ndistributed use-cases. In conjunction with [multithreading](#multi-threading),\ndistributed Perspective offers consistently high performance over large numbers\nof clients and large datasets.\n\n_*server.py*_\n\n```python\nfrom perspective import Server\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\n# Create an instance of Server, and host a Table\nSERVER = Server()\nCLIENT = SERVER.new_local_client()\n\n# The Table is exposed at `localhost:8888/websocket` with the name `data_source`\nclient.table(data, name = \"data_source\")\n\napp = tornado.web.Application([\n    # create a websocket endpoint that the client JavaScript can access\n    (r\"/websocket\", PerspectiveTornadoHandler, {\"perspective_server\": SERVER})\n])\n\n# Start the Tornado server\napp.listen(8888)\nloop = tornado.ioloop.IOLoop.current()\nloop.start()\n```\n\nInstead of calling `load(server_table)`, create a `View` using `server_table`\nand pass that into `viewer.load()`. This will automatically register an\n`on_update` callback that synchronizes state between the server and the client.\n\n_*index.html*_\n\n```html\n<perspective-viewer id=\"viewer\" editable></perspective-viewer>\n\n<script type=\"module\">\n    // Create a client that expects a Perspective server\n    // to accept connections at the specified URL.\n    const websocket = await perspective.websocket(\n        \"ws://localhost:8888/websocket\",\n    );\n\n    // Get a handle to the Table on the server\n    const server_table = await websocket.open_table(\"data_source_one\");\n\n    // Create a new view\n    const server_view = await table.view();\n\n    // Create a Table on the client using `perspective.worker()`\n    const worker = await perspective.worker();\n    const client_table = await worker.table(view);"},{"title":"websocket » Hosting a WebSocket server » Client/Server Replicated Mode (2)","path":"websocket.md","text":"    // Load the client table in the `<perspective-viewer>`.\n    document.getElementById(\"viewer\").load(client_table);\n</script>\n```\n\nFor a more complex example that offers distributed editing of the server\ndataset, see\n[client_server_editing.html](https://github.com/perspective-dev/perspective/blob/master/examples/python-tornado/client_server_editing.html).\n\nWe also provide examples for Starlette/FastAPI and AIOHTTP:\n\n- [Starlette Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-starlette).\n- [AIOHTTP Example Project](https://github.com/perspective-dev/perspective/tree/master/examples/python-aiohttp)."},{"title":"websocket » Hosting a WebSocket server » Server-only Mode","path":"websocket.md","text":"The server setup is identical to\n[Client/Server Replicated Mode](#client-server-replicated-mode) above, but\ninstead of creating a `View`, the client calls `load(server_table)`: In Python,\nuse `Server` and `PerspectiveTornadoHandler` to create a websocket server that\nexposes a `Table`. In this example, `table` is a proxy for the `Table` we\ncreated on the server. All API methods are available on _proxies_, e.g.\ncalling `view()`, `schema()`, `update()` on `table` will pass those operations\nto the Python `Table`, execute the commands, and return the result back to\nJavascript.\n\n```html\n<perspective-viewer id=\"viewer\" editable></perspective-viewer>\n```\n\n```javascript\nconst websocket = perspective.websocket(\"ws://localhost:8888/websocket\");\nconst table = websocket.open_table(\"data_source\");\ndocument.getElementById(\"viewer\").load(table);\n```"},{"title":"rust » Rust","path":"rust.md","text":"Install via `cargo`:\n\n```bash\ncargo add perspective\n```\n\nrust » Example\n\nInitialize a server and client\n\n```rust\nlet server = Server::default();\nlet client = server.new_local_client();\n```\n\nLoad an Arrow\n\n```rust\nlet mut file = File::open(std::path::Path::new(ROOT_PATH).join(ARROW_FILE_PATH))?;\nlet mut feather = Vec::with_capacity(file.metadata()?.len() as usize);\nfile.read_to_end(&mut feather)?;\nlet data = UpdateData::Arrow(feather.into());\nlet mut options = TableInitOptions::default();\noptions.set_name(\"my_data_source\");\nclient.table(data.into(), options).await?;\n```"},{"title":"rust » Joining Tables","path":"rust.md","text":"`Client::join` creates a read-only `Table` by joining two source tables on a\nshared key column. The result is reactive — it updates automatically when\neither source table changes. See [`Join`](../explanation/join.md) for\nconceptual details.\n\n```rust\nlet orders = client.table(\n    TableData::Update(UpdateData::JsonRows(\n        \"[{\\\"id\\\":1,\\\"product_id\\\":101,\\\"qty\\\":5},{\\\"id\\\":2,\\\"product_id\\\":102,\\\"qty\\\":3}]\".into(),\n    )),\n    TableInitOptions::default(),\n).await?;\n\nlet products = client.table(\n    TableData::Update(UpdateData::JsonRows(\n        \"[{\\\"product_id\\\":101,\\\"name\\\":\\\"Widget\\\"},{\\\"product_id\\\":102,\\\"name\\\":\\\"Gadget\\\"}]\".into(),\n    )),\n    TableInitOptions::default(),\n).await?;\n\nlet joined = client.join(\n    (&orders).into(),\n    (&products).into(),\n    \"product_id\",\n    JoinOptions::default(),\n).await?;\n\nlet view = joined.view(None).await?;\nlet json = view.to_json().await?;\n```\n\nUse `JoinOptions` to configure the join type, table name, or `right_on` column:\n\n```rust\nlet options = JoinOptions {\n    join_type: Some(JoinType::Left),\n    name: Some(\"orders_with_products\".into()),\n    right_on: None,\n};\n\nlet joined = client.join(\n    (&orders).into(),\n    (&products).into(),\n    \"product_id\",\n    options,\n).await?;\n```\n\nYou can also join by table name strings instead of `Table` references:\n\n```rust\nlet joined = client.join(\n    \"orders\".into(),\n    \"products\".into(),\n    \"product_id\",\n    JoinOptions::default(),\n).await?;\n```"},{"title":"tornado » Tutorial: A tornado server in Python","path":"tornado.md","text":"Perspective ships with a pre-built Tornado handler that makes integration with\n`tornado.websockets` extremely easy. This allows you to run an instance of\n`Perspective` on a server using Python, open a websocket to a `Table`, and\naccess the `Table` in JavaScript and through `<perspective-viewer>`. All\ninstructions sent to the `Table` are processed in Python, which executes the\ncommands, and returns its output through the websocket back to Javascript.\n\ntornado » Tutorial: A tornado server in Python » Python setup\n\nMake sure Perspective and Tornado are installed!\n\n```bash\npip install perspective-python tornado\n```\n\nTo use the handler, we need to first have a `Server`, a `Client` and an instance\nof a `Table`:\n\n```python\nimport perspective\n\nSERVER = perspective.Server()\nCLIENT = SERVER.new_local_client()\n```\n\nOnce the server has been created, create a `Table` instance with a name. The\nname that you host the table under is important — it acts as a unique accessor\non the JavaScript side, which will look for a Table hosted at the websocket with\nthe name you specify.\n\n```python\nTABLE = client.table(data, name=\"data_source_one\")\n```\n\nAfter the server and table setup is complete, create a websocket endpoint and\nprovide it a reference to `PerspectiveTornadoHandler`. You must provide the\nconfiguration object in the route tuple, and it must contain\n`\"perspective_server\"`, which is a reference to the `Server` you just created.\n\n```python\nfrom perspective.handlers.tornado import PerspectiveTornadoHandler\n\napp = tornado.web.Application([\n\n    # ... other handlers ...\n\n    # Create a websocket endpoint that the client JavaScript can access\n    (r\"/websocket\", PerspectiveTornadoHandler, {\"perspective_server\": SERVER, \"check_origin\": True})\n])\n```\n\nOptionally, the configuration object can also include `check_origin`, a boolean\nthat determines whether the websocket accepts requests from origins other than\nwhere the server is hosted. See\n[Tornado docs](https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler.check_origin)\nfor more details."},{"title":"tornado » Tutorial: A tornado server in Python » JavaScript setup","path":"tornado.md","text":"Once the server is up and running, you can access the Table you just hosted\nusing `perspective.websocket` and `open_table()`. First, create a client that\nexpects a Perspective server to accept connections at the specified URL:\n\n```javascript\nimport \"@perspective-dev/viewer\";\nimport \"@perspective-dev/viewer-datagrid\";\nimport perspective from \"@perspective-dev/client\";\n\nconst websocket = await perspective.websocket(\"ws://localhost:8888/websocket\");\n```\n\nNext open the `Table` we created on the server by name:\n\n```javascript\nconst table = await websocket.open_table(\"data_source_one\");\n```\n\n`table` is a proxy for the `Table` we created on the server. All operations that\nare possible through the JavaScript API are possible on the Python API as well,\nthus calling `view()`, `schema()`, `update()` etc. on `const table` will pass\nthose operations to the Python `Table`, execute the commands, and return the\nresult back to JavaScript. Similarly, providing this `table` to a\n`<perspective-viewer>` instance will allow virtual rendering:\n\n```javascript\nconst viewer = document.createElement(\"perspective-viewer\");\nviewer.style.height = \"500px\";\ndocument.body.appendChild(viewer);\nawait viewer.load(table);\n```\n\n`perspective.websocket` expects a Websocket URL where it will send instructions.\nWhen `open_table` is called, the name to a hosted Table is passed through, and a\nrequest is sent through the socket to fetch the Table. No actual `Table`\ninstance is passed inbetween the runtimes; all instructions are proxied through\nwebsockets.\n\nThis provides for great flexibility — while `Perspective.js` is full of\nfeatures, browser WebAssembly runtimes currently have some performance\nrestrictions on memory and CPU feature utilization, and the architecture in\ngeneral suffers when the dataset itself is too large to download to the client\nin full.\n\nThe Python runtime does not suffer from memory limitations, utilizes Apache\nArrow internal threadpools for threading and parallel processing, and generates\narchitecture optimized code, which currently makes it more suitable as a\nserver-side runtime than `node.js`."},{"title":"expression_gen (1)","path":"expression_gen.md","text":"<br/>\n\nexpression_gen » Perspective ExprTK Extensions"},{"title":"expression_gen (2)","path":"expression_gen.md","text":"- `var ${1:x := 1}` Declare a new local variable\n- `abs(${1:x})` Absolute value of x\n- `avg(${1:x})` Average of all inputs\n- `bucket(${1:x}, ${2:y})` Bucket x by y\n- `ceil(${1:x})` Smallest integer >= x\n- `exp(${1:x})` Natural exponent of x (e ^ x)\n- `floor(${1:x})` Largest integer <= x\n- `frac(${1:x})` Fractional portion (after the decimal) of x\n- `iclamp(${1:x})` Inverse clamp x within a range\n- `inrange(${1:x})` Returns whether x is within a range\n- `log(${1:x})` Natural log of x\n- `log10(${1:x})` Base 10 log of x\n- `log1p(${1:x})` Natural log of 1 + x where x is very small\n- `log2(${1:x})` Base 2 log of x\n- `logn(${1:x}, ${2:N})` Base N log of x where N >= 0\n- `max(${1:x})` Maximum value of all inputs\n- `min(${1:x})` Minimum value of all inputs\n- `mul(${1:x})` Product of all inputs\n- `percent_of(${1:x})` Percent y of x\n- `pow(${1:x}, ${2:y})` x to the power of y\n- `root(${1:x}, ${2:N})` N-th root of x where N >= 0\n- `round(${1:x})` Round x to the nearest integer\n- `sgn(${1:x})` Sign of x: -1, 1, or 0\n- `sqrt(${1:x})` Square root of x\n- `sum(${1:x})` Sum of all inputs\n- `trunc(${1:x})` Integer portion of x\n- `acos(${1:x})` Arc cosine of x in radians\n- `acosh(${1:x})` Inverse hyperbolic cosine of x in radians\n- `asin(${1:x})` Arc sine of x in radians\n- `asinh(${1:x})` Inverse hyperbolic sine of x in radians\n- `atan(${1:x})` Arc tangent of x in radians\n- `atanh(${1:x})` Inverse hyperbolic tangent of x in radians\n- `cos(${1:x})` Cosine of x\n- `cosh(${1:x})` Hyperbolic cosine of x\n- `cot(${1:x})` Cotangent of x\n- `sin(${1:x})` Sine of x\n- `sinc(${1:x})` Sine cardinal of x\n- `sinh(${1:x})` Hyperbolic sine of x\n- `tan(${1:x})` Tangent of x\n- `tanh(${1:x})` Hyperbolic tangent of x\n- `deg2rad(${1:x})` Convert x from degrees to radians\n- `deg2grad(${1:x})` Convert x from degrees to gradians\n- `rad2deg(${1:x})` Convert x from radians to degrees\n- `grad2deg(${1:x})` Convert x from gradians to degrees\n- `concat(${1:x}, ${2:y})` Concatenate string columns and string literals, such as: concat(\"State\" ', ', \"City\")\n- `order(${1:input column}, ${2:value}, ...)` Generates a sort order for a string column based on the input order of the parameters, such as: order(\"State\", 'Texas', 'New York')\n- `upper(${1:x})` Uppercase of x\n- `lower(${1:x})` Lowercase of x\n- `hour_of_day(${1:x})` Return a datetime's hour of the day as a string\n- `month_of_year(${1:x})` Return a datetime's month of the year as a string\n- `day_of_week(${1:x})` Return a datetime's day of week as a string\n- `now()` The current datetime in local time\n- `today()` The current date in local time\n- `is_null(${1:x})` Whether x is a null value\n- `is_not_null(${1:x})` Whether x is not a null value\n- `coalesce(${1:x}, ${2:y})` Returns the first non-null argument.\n- `contains(${1:x}, ${2:'substr'})` Whether the string column or value contains the literal substring.\n- `not(${1:x})` not x\n- `true` Boolean value true\n- `false` Boolean value false\n- `if (${1:condition}) {} else if (${2:condition}) {} else {}` An if/else conditional, which evaluates a condition such as:  if (\"Sales\" > 100) { true } else { false }\n- `for (${1:expression}) {}` A for loop, which repeatedly evaluates an incrementing expression such as: var x := 0; var y := 1; for (x < 10; x += 1) { y := x + y }\n- `string(${1:x})` Converts the given argument to a string\n- `integer(${1:x})` Converts the given argument to a 32-bit integer. If the result over/under-flows, null is returned\n- `float(${1:x})` Converts the argument to a float\n- `date(${1:year}, ${1:month}, ${1:day})` Given a year, month (1-12) and day, create a new date\n- `datetime(${1:timestamp})` Given a POSIX timestamp of milliseconds since epoch, create a new datetime\n- `boolean(${1:x})` Converts the given argument to a boolean\n- `random()` Returns a random float between 0 and 1, inclusive.\n- `match(${1:string}, ${2:pattern})` Returns True if any part of string matches pattern, and False otherwise.\n- `match_all(${1:string}, ${2:pattern})` Returns True if the whole string matches pattern, and False otherwise.\n- `search(${1:string}, ${2:pattern})` Returns the substring that matches the first capturing group in pattern, or null if there are no capturing groups in the pattern or if there are no matches.\n- `indexof(${1:string}, ${2:pattern}, ${3:output_vector})` Writes into index 0 and 1 of output_vector the start and end indices of the substring that matches the first capturing group in pattern.  Returns true if there is a match and output was written, or false if there are no capturing groups in the pattern, if there are no matches, or if the indices are invalid.\n- `substring(${1:string}, ${2:start_idx}, ${3:length})` Returns a substring of string from start_idx with the given length. If length is not passed in, returns substring from start_idx to the end of the string. Returns null if the string or any indices are invalid.\n- `replace(${1:string}, ${2:pattern}, ${3:replacer})` Replaces the first match of pattern in string with replacer, or return the original string if no replaces were made.\n- `replace_all(${1:string}, ${2:pattern}, ${3:replacer})` Replaces all non-overlapping matches of pattern in string with replacer, or return the original string if no replaces were made.\n- `index()` Looks up the index value of the current row\n- `col(${1:string})` Looks up a column value by name\n- `vlookup(${1:string}, ${2:uint64})` Looks up a value in another column by index"},{"title":"viewer","path":"viewer.md","text":"The JavaScript language bindings for`<perspective-viewer>` Custom Element, the\nmain UI for [Perspective](https://perspective-dev.github.io).\n\n<div class=\"warning\">\nThe examples in this module are in JavaScript. See <a href=\"https://docs.rs/crate/perspective/latest\"><code>perspective</code></a> docs for the Rust API.\n</div>\n\nviewer » <perspective-viewer> Custom Element library\n\n`<perspective-viewer>` provides a complete graphical UI for configuring the\n`perspective` library and formatting its output to the provided visualization\nplugins.\n\nIf you are using `esbuild` or another bundler which supports ES6 modules, you\nonly need to import the `perspective-viewer` libraries somewhere in your\napplication - these modules export nothing, but rather register the components\nfor use within your site's regular HTML:\n\n```javascript\nimport \"@perspective-dev/viewer\";\nimport \"@perspective-dev/viewer-datagrid\";\nimport \"@perspective-dev/viewer-charts\";\n```\n\nOnce imported, the `<perspective-viewer>` Web Component will be available in any\nstandard HTML on your site. A simple example:\n\n```html\n<perspective-viewer id=\"view1\"></perspective-viewer>\n```\n\nor\n\n```javascript\nconst viewer = document.createElement(\"perspective-viewer\");\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Theming","path":"viewer.md","text":"Theming is supported in `perspective-viewer` and its accompanying plugins. A\nnumber of themes come bundled with `perspective-viewer`; you can import any of\nthese themes directly into your app, and the `perspective-viewer`s will be\nthemed accordingly:\n\n```javascript\n// Themes based on Thought Merchants's Prospective design\nimport \"@perspective-dev/viewer/dist/css/pro.css\";\nimport \"@perspective-dev/viewer/dist/css/pro-dark.css\";\n\n// Other themes\nimport \"@perspective-dev/viewer/dist/css/solarized.css\";\nimport \"@perspective-dev/viewer/dist/css/solarized-dark.css\";\nimport \"@perspective-dev/viewer/dist/css/monokai.css\";\nimport \"@perspective-dev/viewer/dist/css/vaporwave.css\";\n```\n\nAlternatively, you may use `themes.css`, which bundles all default themes\n\n```javascript\nimport \"@perspective-dev/viewer/dist/css/themes.css\";\n```\n\nIf you choose not to bundle the themes yourself, they are available through\n[CDN](https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/). These can\nbe directly linked in your HTML file:\n\n```html\n<link\n    rel=\"stylesheet\"\n    crossorigin=\"anonymous\"\n    href=\"https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/css/pro.css\"\n/>\n```\n\nNote the `crossorigin=\"anonymous\"` attribute. When including a theme from a\ncross-origin context, this attribute may be required to allow\n`<perspective-viewer>` to detect the theme. If this fails, additional themes are\nadded to the `document` after `<perspective-viewer>` init, or for any other\nreason theme auto-detection fails, you may manually inform\n`<perspective-viewer>` of the available theme names with the `.resetThemes()`\nmethod.\n\n```javascript\n// re-auto-detect themes\nviewer.resetThemes();\n\n// Set available themes explicitly (they still must be imported as CSS!)\nviewer.resetThemes([\"Pro Light\", \"Pro Dark\"]);\n```\n\n`<perspective-viewer>` will default to the first loaded theme when initialized.\nYou may override this via `.restore()`, or provide an initial theme by setting\nthe `theme` attribute:\n\n```html\n<perspective-viewer theme=\"Pro Light\"></perspective-viewer>\n```\n\nor\n\n```javascript\nconst viewer = document.querySelector(\"perspective-viewer\");\nawait viewer.restore({ theme: \"Pro Dark\" });\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Loading data into <perspective-viewer>","path":"viewer.md","text":"Data can be loaded into `<perspective-viewer>` in the form of a `Table()` or a\n`Promise<Table>` via the `load()` method.\n\n```javascript\n// Create a new worker, then a new table promise on that worker.\nconst worker = await perspective.worker();\nconst table = await worker.table(data);\n\n// Bind a viewer element to this table.\nawait viewer.load(table);\n```\n\nviewer » <perspective-viewer> Custom Element library » Sharing a table() between multiple perspective-viewers\n\nMultiple `perspective-viewer`s can share a `table()` by passing the `table()`\ninto the `load()` method of each viewer. Each `perspective-viewer` will update\nwhen the underlying `table()` is updated, but `table.delete()` will fail until\nall `perspective-viewer` instances referencing it are also deleted:\n\n```javascript\nconst viewer1 = document.getElementById(\"viewer1\");\nconst viewer2 = document.getElementById(\"viewer2\");\n\n// Create a new WebWorker\nconst worker = await perspective.worker();\n\n// Create a table in this worker\nconst table = await worker.table(data);\n\n// Load the same table in 2 different <perspective-viewer> elements\nawait viewer1.load(table);\nawait viewer2.load(table);\n\n// Both `viewer1` and `viewer2` will reflect this update\nawait table.update([{ x: 5, y: \"e\", z: true }]);\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Server-only via WebSocketServer() and Node.js","path":"viewer.md","text":"Loading a virtual (server-only) [`Table`] works just like loading a local/Web\nWorker [`Table`] - just pass the virtual [`Table`] to `viewer.load()`:\n\nIn the browser:\n\n```javascript\nconst elem = document.getElementsByTagName(\"perspective-viewer\")[0];\n\n// Bind to the server's worker instead of instantiating a Web Worker.\nconst websocket = await perspective.websocket(\n    window.location.origin.replace(\"http\", \"ws\"),\n);\n\n// Bind the viewer to the preloaded data source.  `table` and `view` objects\n// live on the server.\nconst server_table = await websocket.open_table(\"table_one\");\nawait elem.load(server_table);\n\n// Or load data from a table using a view. The browser now also has a copy of\n// this view in its own `table`, as well as its updates transferred to the\n// browser using Apache Arrow.\nconst worker = await perspective.worker();\nconst server_view = await server_table.view();\nconst client_table = worker.table(server_view);\nawait elem.load(client_table);\n```\n\n`<perspective-viewer>` instances bound in this way are otherwise no different\nthan `<perspective-viewer>`s which rely on a Web Worker, and can even share a\nhost application with Web Worker-bound `table()`s. The same `promise`-based API\nis used to communicate with the server-instantiated `view()`, only in this case\nit is over a websocket."},{"title":"viewer » <perspective-viewer> Custom Element library » Persistent <perspective-viewer> configuration via save()/restore().","path":"viewer.md","text":"`<perspective-viewer>` is _persistent_, in that its entire state (sans the data\nitself) can be serialized or deserialized. This include all column, filter,\npivot, expressions, etc. properties, as well as datagrid style settings, config\npanel visibility, and more. This overloaded feature covers a range of use cases:\n\n- Setting a `<perspective-viewer>`'s initial state after a `load()` call.\n- Updating a single or subset of properties, without modifying others.\n- Resetting some or all properties to their data-relative default.\n- Persisting a user's configuration to `localStorage` or a server."},{"title":"viewer » <perspective-viewer> Custom Element library » Persistent <perspective-viewer> configuration via save()/restore(). » Serializing and deserializing the viewer state","path":"viewer.md","text":"To retrieve the entire state as a JSON-ready JavaScript object, use the `save()`\nmethod. `save()` also supports a few other formats such as `\"arraybuffer\"` and\n`\"string\"` (base64, not JSON), which you may choose for size at the expense of\neasy migration/manual-editing.\n\n```javascript\nconst json_token = await elem.save();\nconst string_token = await elem.save(\"string\");\n```\n\nFor any format, the serialized token can be restored to any\n`<perspective-viewer>` with a `Table` of identical schema, via the `restore()`\nmethod. Note that while the data for a token returned from `save()` may differ,\ngenerally its schema may not, as many other settings depend on column names and\ntypes.\n\n```javascript\nawait elem.restore(json_token);\nawait elem.restore(string_token);\n```\n\nAs `restore()` dispatches on the token's type, it is important to make sure that\nthese types match! A common source of error occurs when passing a\nJSON-stringified token to `restore()`, which will assume base64-encoded msgpack\nwhen a string token is used.\n\n```javascript\n// This will error!\nawait elem.restore(JSON.stringify(json_token));\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Persistent <perspective-viewer> configuration via save()/restore(). » Updating individual properties","path":"viewer.md","text":"Using the JSON format, every facet of a `<perspective-viewer>`'s configuration\ncan be manipulated from JavaScript using the `restore()` method. The valid\nstructure of properties is described via the\n[`ViewerConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa/rust/perspective-viewer/src/ts/viewer.ts#L16)\nand embedded\n[`ViewConfig`](https://github.com/perspective-dev/perspective/blob/ebced4caa19435a2a57d4687be7e428a4efc759b/packages/perspective/index.d.ts#L140)\ntype declarations, and [`View`](view.md) chapter of the documentation which has\nseveral interactive examples for each `ViewConfig` property.\n\n```javascript\n// Set the plugin (will also update `columns` to plugin-defaults)\nawait elem.restore({ plugin: \"X Bar\" });\n\n// Update plugin and columns (only draws once)\nawait elem.restore({ plugin: \"X Bar\", columns: [\"Sales\"] });\n\n// Open the config panel\nawait elem.restore({ settings: true });\n\n// Create an expression\nawait elem.restore({\n    columns: ['\"Sales\" + 100'],\n    expressions: { \"New Column\": '\"Sales\" + 100' },\n});\n\n// ERROR if the column does not exist in the schema or expressions\n// await elem.restore({columns: [\"\\\"Sales\\\" + 100\"], expressions: {}});\n\n// Add a filter\nawait elem.restore({ filter: [[\"Sales\", \"<\", 100]] });\n\n// Add a sort, don't remove filter\nawait elem.restore({ sort: [[\"Prodit\", \"desc\"]] });\n\n// Reset just filter, preserve sort\nawait elem.restore({ filter: undefined });\n\n// Reset all properties to default e.g. after `load()`\nawait elem.reset();\n```\n\nAnother effective way to quickly create a token for a desired configuration is\nto simply copy the token returned from `save()` after settings the view manually\nin the browser. The JSON format is human-readable and should be quite easy to\ntweak once generated, as `save()` will return even the default settings for all\nproperties. You can call `save()` in your application code, or e.g. through the\nChrome developer console:\n\n```javascript\n// Copy to clipboard\ncopy(await document.querySelector(\"perspective-viewer\").save());\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Update events","path":"viewer.md","text":"Whenever a `<perspective-viewer>`s underlying `table()` is changed via the\n`load()` or `update()` methods, a `perspective-view-update` DOM event is fired.\nSimilarly, `view()` updates instigated either through the Attribute API or\nthrough user interaction will fire a `perspective-config-update` event:\n\n```javascript\nelem.addEventListener(\"perspective-config-update\", function (event) {\n    var config = elem.save();\n    console.log(\"The view() config has changed to \" + JSON.stringify(config));\n});\n```"},{"title":"viewer » <perspective-viewer> Custom Element library » Click events","path":"viewer.md","text":"Whenever a `<perspective-viewer>`'s grid or chart is clicked, a\n`perspective-click` DOM event is fired containing a detail object with `config`,\n`column_names`, and `row`.\n\nThe `config` object contains an array of `filters` that can be applied to a\n`<perspective-viewer>` through the use of `restore()` updating it to show the\nfiltered subset of data.\n\nThe `column_names` property contains an array of matching columns, and the `row`\nproperty returns the associated row data.\n\n```javascript\nelem.addEventListener(\"perspective-click\", function (event) {\n    var config = event.detail.config;\n    elem.restore(config);\n});\n```"},{"title":"JS API » VirtualHostedTable","path":"perspective-viewer.d.ts","text":"export interface VirtualHostedTable\n\nA table hosted by a `VirtualServerHandler`, as returned by\n`getHostedTables()`. A plain `string` is shorthand for `{ name }`.\n\nJS API » VirtualServerHandler\n\nexport interface VirtualServerHandler\n\nHandler interface that you implement to provide custom data sources.\n\nAll methods will be called by the `VirtualServer` when handling protocol\nmessages from Perspective clients. Methods can return values directly or\nreturn Promises for asynchronous operations (e.g., database queries).\nOptional methods fall back to defaults documented per-method."},{"title":"JS API » tableColumnsSize","path":"perspective-viewer.d.ts","text":"tableColumnsSize?(tableId: string): number | Promise<number>\n\nDefaults to the length of `tableSchema(tableId)`.\n\nJS API » viewColumnSize\n\nviewColumnSize?(\n\nDefaults to the length of `viewSchema(viewId, config)`.\n\nJS API » tableValidateExpression\n\ntableValidateExpression?(\n\nRequired when `getFeatures()` reports `expressions: true`.\n\nJS API » Client\n\nexport class Client\n\nAn instance of a [`Client`] is a connection to a single\n`perspective_server::Server`, whether locally in-memory or remote over some\ntransport like a WebSocket.\n\nThe browser and node.js libraries both support the `websocket(url)`\nconstructor, which connects to a remote `perspective_server::Server`\ninstance over a WebSocket transport.\n\nIn the browser, the `worker()` constructor creates a new Web Worker\n`perspective_server::Server` and returns a [`Client`] connected to it.\n\nIn node.js, a pre-instantied [`Client`] connected synhronously to a global\nsingleton `perspective_server::Server` is the default module export.\n\n# JavaScript Examples\n\nCreate a Web Worker `perspective_server::Server` in the browser and return a\n[`Client`] instance connected for it:\n\n```javascript\nimport perspective from \"@perspective-dev/client\";\nconst client = await perspective.worker();\n```\n\nCreate a WebSocket connection to a remote `perspective_server::Server`:\n\n```javascript\nimport perspective from \"@perspective-dev/client\";\nconst client = await perspective.websocket(\"ws://locahost:8080/ws\");\n```\n\nAccess the synchronous client in node.js:\n\n```javascript\nimport { default as client } from \"@perspective-dev/client\";\n```"},{"title":"JS API » __unsafe_open_view","path":"perspective-viewer.d.ts","text":"__unsafe_open_view(entity_id: string): View\n\nUnsafely gets a [`View`] by raw ID, useful for JavaScript multi-threaded\n(via Web Worker) context where a standard `View` cannot otherwise be\nshared because its wrapper is not serializable.\n\n# Safety\n\nThis method is unsafe because the lifetime of a [`View`] is bound to\nthe [`Client`] which created it.\n\nThe caller must guarantee that `entity_id` corresponds to a live\n[`crate::View`] on the connected server (obtained from another\n[`Client`]'s [`crate::View::get_name`] and forwarded across the\nserialization boundary).\n\n# JavaScript Examples\n\n```javascript\nconst view = client.__unsafe_open_view(name_from_main_thread);\nconst cols = await view.to_columns();\n```"},{"title":"JS API » get_hosted_table_names","path":"perspective-viewer.d.ts","text":"get_hosted_table_names(): Promise<string[]>\n\nRetrieves the names of all tables that this client has access to.\n\n`name` is a string identifier unique to the [`Table`] (per [`Client`]),\nwhich can be used in conjunction with [`Client::open_table`] to get\na [`Table`] instance without the use of [`Client::table`]\nconstructor directly (e.g., one created by another [`Client`]).\n\n# JavaScript Examples\n\n```javascript\nconst tables = await client.get_hosted_table_names();\n```\n\nJS API » join\n\njoin(left: any, right: any, on: string, options?: JoinOptions | null): Promise<Table>\n\nCreates a new read-only [`Table`] by performing an INNER JOIN on two\nsource tables. The resulting table is reactive: when either source\ntable is updated, the join is automatically recomputed.\n\n# Arguments\n\n- `left` - The left source table (a [`Table`] instance or a table name\n  string).\n- `right` - The right source table (a [`Table`] instance or a table name\n  string).\n- `on` - The column name to join on. Must exist in both tables with the\n  same type.\n- `options` - Optional join configuration: `{ join_type?: \"inner\" |\n  \"left\" | \"outer\", name?: string }`.\n\n# JavaScript Examples\n\n```javascript\nconst joined = await client.join(orders_table, products_table, \"Product ID\", { join_type: \"left\" });\nconst joined = await client.join(\"orders\", \"products\", \"Product ID\", { join_type: \"left\" });\n```"},{"title":"JS API » on_hosted_tables_update","path":"perspective-viewer.d.ts","text":"on_hosted_tables_update(on_update_js: Function): Promise<number>\n\nRegister a callback which is invoked whenever [`Client::table`] (on this\n[`Client`]) or [`Table::delete`] (on a [`Table`] belinging to this\n[`Client`]) are called.\n\nJS API » open_table\n\nopen_table(entity_id: string): Promise<Table>\n\nOpens a [`Table`] that is hosted on the `perspective_server::Server`\nthat is connected to this [`Client`].\n\nThe `name` property of [`TableInitOptions`] is used to identify each\n[`Table`]. [`Table`] `name`s can be looked up for each [`Client`]\nvia [`Client::get_hosted_table_names`].\n\n# JavaScript Examples\n\nGet a virtual [`Table`] named \"table_one\" from this [`Client`]\n\n```javascript\nconst tables = await client.open_table(\"table_one\");\n```"},{"title":"JS API » remove_hosted_tables_update","path":"perspective-viewer.d.ts","text":"remove_hosted_tables_update(update_id: number): Promise<void>\n\nRemove a callback previously registered via\n`Client::on_hosted_tables_update`.\n\nJS API » system_info\n\nsystem_info(): Promise<SystemInfo>\n\nProvides the [`SystemInfo`] struct, implementation-specific metadata\nabout the [`perspective_server::Server`] runtime such as Memory and\nCPU usage.\n\nFor WebAssembly servers, this method includes the WebAssembly heap size.\n\n# JavaScript Examples\n\n```javascript\nconst info = await client.system_info();\n```"},{"title":"JS API » table (1)","path":"perspective-viewer.d.ts","text":"table(value: string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[] | Record<string, ColumnType>, options?: TableInitOptions | null): Promise<Table>\n\nCreates a new [`Table`] from either a _schema_ or _data_.\n\nThe [`Client::table`] factory function can be initialized with either a\n_schema_ (see [`Table::schema`]), or data in one of these formats:\n\n- Apache Arrow\n- CSV\n- JSON row-oriented\n- JSON column-oriented\n- NDJSON\n\nWhen instantiated with _data_, the schema is inferred from this data.\nWhile this is convenient, inferrence is sometimes imperfect e.g.\nwhen the input is empty, null or ambiguous. For these cases,\n[`Client::table`] can first be instantiated with a explicit schema.\n\nWhen instantiated with a _schema_, the resulting [`Table`] is empty but\nwith known column names and column types. When subsqeuently\npopulated with [`Table::update`], these columns will be _coerced_ to\nthe schema's type. This behavior can be useful when\n[`Client::table`]'s column type inferences doesn't work.\n\nThe resulting [`Table`] is _virtual_, and invoking its methods\ndispatches events to the `perspective_server::Server` this\n[`Client`] connects to, where the data is stored and all calculation\noccurs.\n\n# Arguments\n\n- `arg` - Either _schema_ or initialization _data_.\n- `options` - Optional configuration which provides one of:\n    - `limit` - The max number of rows the resulting [`Table`] can\n      store.\n    - `index` - The column name to use as an _index_ column. If this\n      `Table` is being instantiated by _data_, this column name must be\n      present in the data.\n    - `name` - The name of the table. This will be generated if it is\n      not provided.\n    - `format` - The explicit format of the input data, can be one of\n      `\"json\"`, `\"columns\"`, `\"csv\"` or `\"arrow\"`. This overrides\n      language-specific type dispatch behavior, which allows stringified\n      and byte array alternative inputs.\n\n# JavaScript Examples\n\nLoad a CSV from a `string`:\n\n```javascript\nconst table = await client.table(\"x,y\\n1,2\\n3,4\");\n```\n\nLoad an Arrow from an `ArrayBuffer`:\n\n```javascript\nimport * as fs from \"node:fs/promises\";\nconst table2 = await client.table(await fs.readFile(\"superstore.arrow\"));\n```\n\nLoad a CSV from a `UInt8Array` (the default for this type is Arrow)\nusing a format override:\n\n```javascript\nconst enc = new TextEncoder();\nconst table = await client.table(enc.encode(\"x,y\\n1,2\\n3,4\"), {\n    format: \"csv\",\n});\n```\n\nCreate a table with an `index`:"},{"title":"JS API » table (2)","path":"perspective-viewer.d.ts","text":"```javascript\nconst table = await client.table(data, { index: \"Row ID\" });\n```"},{"title":"JS API » terminate","path":"perspective-viewer.d.ts","text":"terminate(): any\n\nTerminates this [`Client`], cleaning up any [`crate::View`] handles the\n[`Client`] has open as well as its callbacks.\n\nJS API » GenericSQLVirtualServerModel\n\nexport class GenericSQLVirtualServerModel\n\nJavaScript-facing DuckDB SQL query builder.\n\nThis struct wraps the Rust `DuckDBSqlBuilder` and exposes it to JavaScript\nvia wasm_bindgen.\n\nJS API » getHostedTables\n\ngetHostedTables(): string\n\nReturns the SQL query to list all hosted tables.\n\nJS API » constructor\n\nconstructor(args?: any | null)\n\nCreates a new `JsDuckDBSqlBuilder` instance."},{"title":"JS API » tableMakeView","path":"perspective-viewer.d.ts","text":"tableMakeView(table_id: string, view_id: string, config: any, schema: any): string\n\nReturns the SQL query to create a view from a table with the given\nconfiguration.\n\nJS API » tableSchema\n\ntableSchema(table_id: string): string\n\nReturns the SQL query to describe a table's schema.\n\nJS API » tableSize\n\ntableSize(table_id: string): string\n\nReturns the SQL query to get the row count of a table.\n\nJS API » tableValidateExpression\n\ntableValidateExpression(table_id: string, expression: string): string\n\nReturns the SQL query to validate an expression against a table."},{"title":"JS API » viewColumnSize","path":"perspective-viewer.d.ts","text":"viewColumnSize(view_id: string): string\n\nReturns the SQL query to get the column count of a view.\n\nJS API » viewGetData\n\nviewGetData(view_id: string, config: any, viewport: any, schema: any): string\n\nReturns the SQL query to fetch data from a view with the given viewport.\n\nJS API » viewGetMinMax\n\nviewGetMinMax(view_id: string, column_name: string, config: any): string\n\nReturns the SQL query to get the min and max values of a column.\n\nJS API » viewSchema\n\nviewSchema(view_id: string): string\n\nReturns the SQL query to describe a view's schema."},{"title":"JS API » viewSize","path":"perspective-viewer.d.ts","text":"viewSize(view_id: string): string\n\nReturns the SQL query to get the row count of a view.\n\nJS API » PerspectiveDebugPluginElement\n\nexport class PerspectiveDebugPluginElement\n\nThe `<perspective-viewer-plugin>` element.\n\nThe default perspective plugin which is registered and activated\nautomcatically when a `<perspective-viewer>` is loaded without plugins.\nWhile you will not typically instantiate this class directly, it is simple\nenough to function as a good \"default\" plugin implementation which can be\nextended to create custom plugins.\n\n# Example\n```javascript\nclass MyPlugin extends customElements.get(\"perspective-viewer-plugin\") {\n   // Custom plugin overrides\n}\n```"},{"title":"JS API » draw","path":"perspective-viewer.d.ts","text":"draw(view: View): Promise<any>\n\n# Notes\n\nWhen you pass a `wasm_bindgen` wrapped type _into_ Rust, it acts like a\nmove. Ergo, if you replace the `&` in the `view` argument, the JS copy\nof the `View` will be invalid\n\nJS API » update\n\nupdate(view: View): Promise<any>\n\nDelegates to `draw()` VIRTUALLY — through the JS element's `draw`\nproperty, never `self.draw(view)` (Rust static dispatch). This\nelement is the documented base class for custom plugins\n(`class MyPlugin extends\ncustomElements.get(\"perspective-viewer-plugin\")`), whose contract is\n\"`update()` defaults to dispatch to `draw()`\" — a\nsubclass overriding only `draw` must receive `update`-path repaints\n(`BindDisposition::Unchanged`/`Adopted` runs,\n`PLUGIN_DRAW_INVARIANT_PLAN.md`); the static call bypassed the\noverride and repainted the Debug CSV instead (the\n`view_lifecycle.spec` regression)."},{"title":"JS API » PerspectiveViewerElement","path":"perspective-viewer.d.ts","text":"export class PerspectiveViewerElement\n\nThe `<perspective-viewer>` custom element.\n\n# JavaScript Examples\n\nCreate a new `<perspective-viewer>`:\n\n```javascript\nconst viewer = document.createElement(\"perspective-viewer\");\nwindow.body.appendChild(viewer);\n```\n\nComplete example including loading and restoring the [`Table`]:\n\n```javascript\nimport perspective from \"@perspective-dev/viewer\";\nimport perspective from \"@perspective-dev/client\";\n\nconst viewer = document.createElement(\"perspective-viewer\");\nconst worker = await perspective.worker();\n\nawait worker.table(\"x\\n1\", {name: \"table_one\"});\nawait viewer.load(worker);\nawait viewer.restore({table: \"table_one\"});\n```"},{"title":"JS API » __get_model","path":"perspective-viewer.d.ts","text":"__get_model(): PerspectiveViewerElement\n\nCreate a new JavaScript Heap reference for this model instance.\n\nJS API » addPanel\n\naddPanel(config: ViewerConfigInitial): Promise<any>\n\nAdd a new, independent panel to this viewer's layout, rendering the\nsupplied [`ViewerConfigInitial`] into it. Unlike [`Self::restore`]'s\nupdate-shaped argument, a new panel has no prior state, so `table`\nis REQUIRED — a table-less call rejects before the layout is\ntouched. The panel uses the default [`perspective_client::Client`]\n(the first passed to [`Self::load`]) to resolve its `table`. Returns\nthe generated panel id.\n\nThe element-level `settings` field does not exist on the argument\ntype (it is shared across the element, not per-panel)."},{"title":"JS API » agentConfig (1)","path":"perspective-viewer.d.ts","text":"agentConfig(config: any): void\n\nConfigure the embedded LLM agent (see `prompt()`), replacing any prior\nconfiguration and conversation.\n\nThe agent core is provider-agnostic: one OpenAI-chat-completions\nprotocol over primitive connection fields. Exactly one of\n`config.url` or `config.engine` is required; the `providers` presets\nexported by this package are plain spreadable collections of these\nfields (`{...providers.anthropic, apiKey}`)."},{"title":"JS API » agentConfig (2)","path":"perspective-viewer.d.ts","text":"- `config.url` - a full chat-completions endpoint URL (any\n  OpenAI-compatible service: Anthropic/Gemini compatibility endpoints,\n  LM Studio, Ollama, OpenRouter, a proxy...).\n- `config.engine` - an in-page engine object with an OpenAI-compatible\n  `chat.completions.create(request)` method (e.g. WebLLM's `MLCEngine`);\n  mutually exclusive with `url`.\n- `config.headers` - extra request headers, sent verbatim.\n- `config.apiKey` - sugar for the `Authorization: Bearer` header.\n- `config.model` - model id sent with each request; local servers and\n  engines generally answer with whatever model is loaded.\n- `config.name` - cosmetic label for the chat badge (presets set this).\n- `config.systemPrompt` - extra system-prompt context appended to the\n  agent's built-in instructions.\n- `config.maxTurns` - max model turns (tool-call rounds + the final\n  answer) per `prompt()` call. Defaults to 16.\n- `config.docs` - the agent metadata bundle, which supplies the\n  `search_docs` corpus and the rich tool parameter schemas: the packaged\n  `dist/docs/perspective-docs.json` asset as a parsed object (`import\n  docs from \"…json\" with { type: \"json\" }`), a `fetch()` `Response`, an\n  `ArrayBuffer`, a JSON string, or a `Promise` of any of those — and/or\n  an inline array of `{title?, text}` entries for host data definitions.\n  Omitted, `search_docs` searches an empty corpus and the parameter\n  schemas degrade to permissive objects.\n- `config.systemRole` - where the preamble (plus `systemPrompt`) is\n  placed: `\"system\"` (default) or `\"user\"`. Some engines refuse a system\n  message alongside `tools` because they substitute their own — WebLLM's\n  Hermes function calling throws `CustomSystemPromptError` on ANY system\n  message — and those need `\"user\"`, which folds the same text into the\n  opening user turn.\n- `config.entitlements` - access grants limiting which tools the agent\n  is offered (and may call): any of `\"read_view\"`, `\"configure_view\"`,\n  `\"manage_layout\"`, `\"read_docs\"`, `\"read_data\"`. Omitted, all but\n  `\"read_data\"` are granted; `[\"read_view\", \"read_docs\"]` yields a\n  read-only agent.\n\n# JavaScript Examples\n\n```javascript\nimport { providers } from \"@perspective-dev/viewer\";\n\nviewer.agentConfig({\n    ...providers.anthropic,\n    apiKey: \"sk-ant-...\",\n    docs: fetch(\n        \"node_modules/@perspective-dev/viewer/dist/docs/perspective-docs.json\",\n    ),\n});\n```"},{"title":"JS API » agentPrompt","path":"perspective-viewer.d.ts","text":"agentPrompt(prompt: string): Promise<any>\n\nRun one conversational turn of the embedded LLM agent (configured via\n`agentConfig()`), resolving with the agent's final text response after\nany tool calls have been applied to this element. Turns share a\nconversation history (and the chat sidebar's transcript) until\n`agentReset()`; a call made while a turn is already running rejects.\nTool activity is emitted as `perspective-agent-tool` CustomEvents on\nthis element.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.agentPrompt(\"Show me sales by region as a bar chart\");\n```"},{"title":"JS API » agentReset","path":"perspective-viewer.d.ts","text":"agentReset(): Promise<any>\n\nClear the agent's conversation (history and chat transcript), keeping\nits configuration. Cancels any in-flight turn.\n\nJS API » copy\n\ncopy(options?: ExportOptions | null): Promise<any>\n\nCopy this viewer's `View` or `Table` data as CSV to the system\nclipboard.\n\n# Arguments\n\n- `method` - The `ExportMethod` (serialized as a `String`) to use to\n  render the data to the Clipboard.\n\n# JavaScript Examples\n\n```javascript\nmyDownloadButton.addEventListener(\"click\", async () => {\n    await viewer.copy();\n})\n```"},{"title":"JS API » delete","path":"perspective-viewer.d.ts","text":"delete(): Promise<any>\n\nDelete all internal [`View`]s and all associated state, rendering this\n`<perspective-viewer>` unusable and freeing all associated resources.\nDoes not delete any supplied [`Table`] (as this is constructed by the\ncallee).\n\nCalling _any_ method on a `<perspective-viewer>` after [`Self::delete`]\nwill throw.\n\n<div class=\"warning\">\n\nAllowing a `<perspective-viewer>` to be garbage-collected\nwithout calling [`PerspectiveViewerElement::delete`] will leak WASM\nmemory!\n\n</div>\n\n# JavaScript Examples\n\n```javascript\nawait viewer.delete();\n```"},{"title":"JS API » download","path":"perspective-viewer.d.ts","text":"download(options?: ExportOptions | null): Promise<any>\n\nDownload this viewer's internal [`View`] data via a browser download\nevent.\n\n# Arguments\n\n- `method` - The `ExportMethod` to use to render the data to download.\n\n# JavaScript Examples\n\n```javascript\nmyDownloadButton.addEventListener(\"click\", async () => {\n    await viewer.download();\n})\n```\n\nJS API » eject\n\neject(options?: ClientOptions | null): Promise<any>\n\nRemove a [`Client`] from this `<perspective-viewer>` and dispose every\npanel bound to it (each panel's `View` is deleted and its `Table`\nreference released).\n\n# Arguments\n\n- `options` - An optional `{client?: string}` dict naming the client to\n  eject; the active panel's client when omitted.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.eject();\nawait viewer.eject({client: \"remote\"});\n```"},{"title":"JS API » export","path":"perspective-viewer.d.ts","text":"export(options?: ExportOptions | null): Promise<any>\n\nExports this viewer's internal [`View`] as a JavaSript data, the\nexact type of which depends on the `method` but defaults to `String`\nin CSV format.\n\nThis method is only really useful for the `\"plugin\"` method, which\nwill use the configured plugin's export (e.g. PNG for\n`@perspective-dev/viewer-charts`). Otherwise, prefer to call the\nequivalent method on the underlying [`View`] directly.\n\n# Arguments\n\n- `method` - The `ExportMethod` to use to render the data to download.\n\n# JavaScript Examples\n\n```javascript\nconst data = await viewer.export(\"plugin\");\n```"},{"title":"JS API » flush","path":"perspective-viewer.d.ts","text":"flush(): Promise<any>\n\nFlush any pending modifications to this `<perspective-viewer>`.  Since\n`<perspective-viewer>`'s API is almost entirely `async`, it may take\nsome milliseconds before any user-initiated changes to the [`View`]\naffects the rendered element.  If you want to make sure all pending\nactions have been rendered, call and await [`Self::flush`].\n\n[`Self::flush`] will resolve immediately if there is no [`Table`] set.\n\n# JavaScript Examples\n\nIn this example, [`Self::restore`] is called without `await`, but the\neventual render which results from this call can still be awaited by\nimmediately awaiting [`Self::flush`] instead.\n\n```javascript\nviewer.restore(config);\nawait viewer.flush();\n```"},{"title":"JS API » getActivePanel","path":"perspective-viewer.d.ts","text":"getActivePanel(): any\n\nThe id of the active panel — the one the settings panel and status-bar\ntoolbar target — or `null` when the element has zero panels.\n\nJS API » getAllPlugins\n\ngetAllPlugins(): Array<any>\n\nGet an `Array` of all of the plugin custom elements registered for this\nelement. This may not include plugins which called\n[`registerPlugin`] after the host has rendered for the first time.\n\nJS API » getClient\n\ngetClient(options?: GetClientOptions | null): Promise<any>\n\nGet the underlying [`Client`] for this viewer (as passed to, or\nassociated with the [`Table`] passed to,\n[`PerspectiveViewerElement::load`]).\n\n# Arguments\n\n- `wait_for_client` - whether to wait for\n  [`PerspectiveViewerElement::load`] to be called, or fail immediately\n  if [`PerspectiveViewerElement::load`] has not yet been called.\n\n# JavaScript Examples\n\n```javascript\nconst client = await viewer.getClient();\n```"},{"title":"JS API » getEditPort","path":"perspective-viewer.d.ts","text":"getEditPort(options?: PanelOptions | null): number\n\nGet this viewer's edit port for the named panel's [`Table`] (see\n[`Table::update`] for details on ports), or the active panel when\n`panel` is omitted.\n\nJS API » getPanelNames\n\ngetPanelNames(): Array<any>\n\nGet the ids of all panels in this viewer's layout, in insertion order.\n\nJS API » getPlugin\n\ngetPlugin(name?: string | null): any\n\nGets a plugin Custom Element with the `name` field, or get the active\nplugin if no `name` is provided.\n\n# Arguments\n\n- `name` - The `name` property of a perspective plugin Custom Element,\n  or `None` for the active plugin's Custom Element."},{"title":"JS API » getRenderStats","path":"perspective-viewer.d.ts","text":"getRenderStats(options?: PanelOptions | null): any\n\nGet render statistics. Some fields of the returned stats object are\nrelative to the last time [`PerspectiveViewerElement::getRenderStats`]\nwas called, ergo calling this method resets these fields.\n\n# JavaScript Examples\n\n```javascript\nconst {virtual_fps, actual_fps} = await viewer.getRenderStats();\n```\n\nJS API » getSelection\n\ngetSelection(options?: PanelOptions | null): ViewWindow | undefined\n\nReturn a [`perspective_js::JsViewWindow`] for the currently selected\nregion of the named panel, or the active panel when `panel` is omitted."},{"title":"JS API » getTable","path":"perspective-viewer.d.ts","text":"getTable(options?: GetTableOptions | null): Promise<any>\n\nGet the underlying [`Table`] for this viewer (as passed to\n[`PerspectiveViewerElement::load`] or as the `table` field to\n[`PerspectiveViewerElement::restore`]).\n\n# Arguments\n\n- `wait_for_table` - whether to wait for\n  [`PerspectiveViewerElement::load`] to be called, or fail immediately\n  if [`PerspectiveViewerElement::load`] has not yet been called.\n\n# JavaScript Examples\n\n```javascript\nconst table = await viewer.getTable();\n```\n\nJS API » getView\n\ngetView(options?: PanelOptions | null): Promise<any>\n\nGet the underlying [`View`] for this viewer.\n\nUse this method to get promgrammatic access to the [`View`] as currently\nconfigured by the user, for e.g. serializing as an\n[Apache Arrow](https://arrow.apache.org/) before passing to another\nlibrary.\n\nThe [`View`] returned by this method is owned by the\n[`PerspectiveViewerElement`] and may be _invalidated_ by\n[`View::delete`] at any time. Plugins which rely on this [`View`] for\ntheir [`HTMLPerspectiveViewerPluginElement::draw`] implementations\nshould treat this condition as a _cancellation_ by silently aborting on\n\"View already deleted\" errors from method calls.\n\n# JavaScript Examples\n\n```javascript\nconst view = await viewer.getView();\n```"},{"title":"JS API » getViewConfig (1)","path":"perspective-viewer.d.ts","text":"getViewConfig(options?: PanelOptions | null): Promise<any>\n\nGet a copy of the [`ViewConfig`] for the current [`View`]. This is\nnon-blocking as it does not need to access the plugin (unlike\n[`PerspectiveViewerElement::save`]), and also makes no API calls to the\nserver (unlike [`PerspectiveViewerElement::getView`] followed by\n[`View::get_config`])\n\nJS API » load\n\nload(client: Client | Table | Promise<Client | Table>): Promise<any>\n\nLoads a [`Client`], or optionally [`Table`], or optionally a Javascript\n`Promise` which returns a [`Client`] or [`Table`], in this viewer.\n\nLoading a [`Client`] does not render, but subsequent calls to\n[`PerspectiveViewerElement::restore`] will use this [`Client`] to look\nup the proviced `table` name field for the provided\n[`ViewerConfigUpdate`].\n\nLoading a [`Table`] is equivalent to subsequently calling\n[`Self::restore`] with the `table` field set to [`Table::get_name`], and\nwill render the UI in its default state when [`Self::load`] resolves.\nIf you plan to call [`Self::restore`] anyway, prefer passing a\n[`Client`] argument to [`Self::load`] as it will conserve one render.\n\nWhen [`PerspectiveViewerElement::load`] resolves, the first frame of the\nUI + visualization is guaranteed to have been drawn. Awaiting the result\nof this method in a `try`/`catch` block will capture any errors\nthrown during the loading process, or from the [`Client`] `Promise`\nitself.\n\n[`PerspectiveViewerElement::load`] may also be called with a [`Table`],\nwhich is equivalent to:\n\n```javascript\nawait viewer.load(await table.get_client());\nawait viewer.restore({name: await table.get_name()})\n```\n\nIf you plan to call [`PerspectiveViewerElement::restore`] immediately\nafter [`PerspectiveViewerElement::load`] yourself, as is commonly\ndone when loading and configuring a new `<perspective-viewer>`, you\nshould use a [`Client`] as an argument and set the `table` field in the\nrestore call as\n\nA [`Table`] can be created using the\n[`@perspective-dev/client`](https://www.npmjs.com/package/@perspective-dev/client)\nlibrary from NPM (see [`perspective_js`] documentation for details).\n\n# JavaScript Examples\n\n```javascript\nimport perspective from \"@perspective-dev/client\";\n\nconst worker = await perspective.worker();\nviewer.load(worker);\n```\n\n... or\n\n```javascript\nconst table = await worker.table(data, {name: \"superstore\"});\nviewer.load(table);\n```\n\nComplete example:"},{"title":"JS API » getViewConfig (2)","path":"perspective-viewer.d.ts","text":"```javascript\nconst viewer = document.createElement(\"perspective-viewer\");\nconst worker = await perspective.worker();\n\nawait worker.table(\"x\\n1\", {name: \"table_one\"});\nawait viewer.load(worker);\nawait viewer.restore({table: \"table_one\", columns: [\"x\"]});\n```\n\n... or, if you don't want to pass your own arguments to `restore`:\n\n```javascript\nconst viewer = document.createElement(\"perspective-viewer\");\nconst worker = await perspective.worker();\n\nconst table = await worker.table(\"x\\n1\", {name: \"table_one\"});\nawait viewer.load(table);\n```"},{"title":"JS API » removePanel","path":"perspective-viewer.d.ts","text":"removePanel(name: string): Promise<any>\n\nRemove the panel with id `name` from the layout, disposing its engines\n(its `View` is deleted and its `Table` reference released). The last\nremaining panel cannot be removed (resolves as a no-op). Resolves\nafter the panel's teardown run completes, carrying any teardown\nerror — previously fire-and-forget and silently dropped (invariant\nI6). See also [`Self::addPanel`].\n\nJS API » reset\n\nreset(reset_all?: boolean | null, options?: PanelOptions | null): Promise<any>\n\nReset a panel's `ViewerConfig` to its data-relative default.\n\nWithout a `panel`, this is ELEMENT-LEVEL: EVERY panel is reset and the\ncross-filter overlay cleared (symmetric with\n[`Self::saveWorkspace`] / [`Self::restoreWorkspace`]). With `{panel}`,\nonly that panel is reset — the other panels and the overlay are left\nuntouched.\n\n# Arguments\n\n- `reset_all` - If set, will clear expressions and column settings as\n  well.\n- `options` - An optional `{panel?: string}`; the panel to reset, or\n  every panel when omitted.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.reset();                     // every panel\nawait viewer.reset(true, {panel: \"p1\"});  // just \"p1\", + expressions\n```"},{"title":"JS API » resetError","path":"perspective-viewer.d.ts","text":"resetError(): Promise<any>\n\nIf this element is in an _errored_ state, this method will clear it and\nre-render. Calling this method is equivalent to clicking the error reset\nbutton in the UI.\n\nJS API » resetThemes\n\nresetThemes(themes?: any[] | null): Promise<any>\n\nSet the available theme names available in the status bar UI.\n\nCalling [`Self::resetThemes`] may cause the current theme to switch,\nif e.g. the new theme set does not contain the current theme.\n\n# JavaScript Examples\n\nRestrict `<perspective-viewer>` theme options to _only_ default light\nand dark themes, regardless of what is auto-detected from the page's\nCSS:\n\n```javascript\nviewer.resetThemes([\"Pro Light\", \"Pro Dark\"])\n```"},{"title":"JS API » resize","path":"perspective-viewer.d.ts","text":"resize(options?: any | null): Promise<any>\n\nRecalculate the viewer's dimensions and redraw.\n\nUse this method to tell `<perspective-viewer>` its dimensions have\nchanged when auto-size mode has been disabled via [`Self::setAutoSize`].\n[`Self::resize`] resolves when the resize-initiated redraw of this\nelement has completed.\n\n# Arguments\n\n- `options` - An optional object with the following fields:\n  - `dimensions` - An optional object `{width, height}` providing\n    explicit size hints (in pixels) for the plugin container. When\n    provided, the plugin element will be temporarily sized to these\n    dimensions during resize, then reset.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.resize()\nawait viewer.resize({dimensions: {width: 800, height: 600}})\n```"},{"title":"JS API » restore","path":"perspective-viewer.d.ts","text":"restore(update: ViewerConfigUpdate, options?: RestoreOptions | null): Promise<void>\n\nRestore a single panel from a full/partial\n[`perspective_js::JsViewConfig`] (its user-configurable state, including\nthe `Table` name) — the active panel, or a specific panel via the\noptional `{panel}` selector.\n\nIf `panel` names no existing panel, a NEW panel is created with that id\nand the config restored into it (an upsert). Creation REQUIRES a\n`table` — the same rule [`Self::addPanel`] enforces in its argument\ntype — and a would-create call without one REJECTS before any state\n(including `settings`) is applied: with no panel to target and no\n`table`, the patch has no data arrival path. In particular, on an\nelement with zero panels every `restore` must carry a `table`.\n\nOn an empty element with a pending [`Self::load`] whose payload is not\nyet classified, the active-target form (no `panel`) instead claims and\nrestores into that load's reserved first panel — see [`Self::load`].\n\nThis restores a SINGLE panel; a workspace config (with a `panels`\nmap) must be applied via [`Self::restoreWorkspace`] — its `panels` /\n`layout` keys are ignored here.\n\nOne of the best ways to use [`Self::restore`] is by first configuring\na `<perspective-viewer>` as you wish, then using either the `Debug`\npanel or \"Copy\" -> \"config.json\" from the toolbar menu to snapshot\nthe [`Self::restore`] argument as JSON.\n\n# Arguments\n\n- `update` - The config to restore to, as returned by [`Self::save`] in\n  either \"json\", \"string\" or \"arraybuffer\" format.\n- `options.panel` - The panel to target, or the active panel when\n  omitted.\n- `options.suppress_errors` - when `true`, a failed restore only rejects\n  the returned `Promise`; the error is NOT committed to the viewer's\n  visible error state and the session remains usable. The view config is\n  rolled back to its pre-call value, so a rejected patch cannot re-merge\n  into a later restore. Element-level state the call already applied\n  (theme, title, a plugin swap) is NOT undone — restore a known-good\n  config to recover those exactly.\n\n# JavaScript Examples\n\nLoads a default plugin for the table named `\"superstore\"`:\n\n```javascript\nawait viewer.restore({table: \"superstore\"});\n```\n\nApply a `group_by` to the same `viewer` element, without\nmodifying/resetting other fields - you can omit the `table` field,\nthis has already been set once and is not modified:\n\n```javascript\nawait viewer.restore({group_by: [\"State\"]});\n```"},{"title":"JS API » restoreWorkspace","path":"perspective-viewer.d.ts","text":"restoreWorkspace(update: WorkspaceConfigUpdate): Promise<void>\n\nRestore the ENTIRE element from a [`WorkspaceConfigUpdate`]\n(`{version, active?, layout, panels, ...}`) —\nthe multi-panel counterpart of [`Self::restore`]. Every existing panel\nis replaced by the `panels` entries, and the layout tree + master/detail\ncross-filter state re-applied. Unlike [`Self::restore`], this never\nfalls back to the single-panel path.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.restoreWorkspace(await otherViewer.saveWorkspace());\n```"},{"title":"JS API » restyleElement","path":"perspective-viewer.d.ts","text":"restyleElement(): Promise<any>\n\nRestyle all plugins from current document.\n\n<div class=\"warning\">\n\n[`Self::restyleElement`] _must_ be called for many runtime changes to\nCSS properties to be reflected in an already-rendered\n`<perspective-viewer>`.\n\n</div>\n\n# JavaScript Examples\n\n```javascript\nviewer.style = \"--psp--color: red\";\nawait viewer.restyleElement();\n```\n\nJS API » save\n\nsave(options?: PanelOptions | null): Promise<ViewerConfig>\n\nSave a single panel's user-configurable state as a [`ViewerConfig`], one\nwhich can be restored via [`Self::restore`] — the active panel, or a\nspecific panel via the optional `{panel}` selector.\n\nThis saves a SINGLE panel; to snapshot the ENTIRE element (every panel +\nlayout + cross-filters) use [`Self::saveWorkspace`].\n\n# Arguments\n\n- `options` - An optional `{panel?: string}`; the panel to save, or the\n  active panel when omitted.\n\n# JavaScript Examples\n\nGet the current `group_by` setting:\n\n```javascript\nconst {group_by} = await viewer.save();\n```\n\nReset workflow attached to an external button `myResetButton`:\n\n```javascript\nconst token = await viewer.save();\nmyResetButton.addEventListener(\"click\", async () => {\n    await viewer.restore(token);\n});\n```"},{"title":"JS API » saveWorkspace","path":"perspective-viewer.d.ts","text":"saveWorkspace(options?: SaveWorkspaceOptions | null): Promise<WorkspaceConfig>\n\nSave the ENTIRE element to a [`WorkspaceConfig`]\n(`{version, active?, layout, panels, palette?, ...}`) — the\nmulti-panel counterpart of [`Self::save`]. Unlike [`Self::save`]\n(which emits a single `ViewerConfig` for one panel), this ALWAYS\nemits the workspace format, restorable via\n[`Self::restoreWorkspace`].\n\n# JavaScript Examples\n\n```javascript\nconst token = await viewer.saveWorkspace();\nawait viewer.restoreWorkspace(token);\n```"},{"title":"JS API » setActivePanel","path":"perspective-viewer.d.ts","text":"setActivePanel(name: string): Promise<any>\n\nMake the panel with id `name` the active panel, re-targeting the\nsettings panel and status-bar toolbar (and the root's\nsession/renderer subscriptions) to its engines. Resolves after the\nactivation-chrome redraws on both sides of the switch have completed\n(invariant I6).\n\nJS API » setAutoPause\n\nsetAutoPause(autopause: boolean): Promise<any>\n\nSets the auto-pause behavior of this component.\n\nWhen `true`, this `<perspective-viewer>` will skip rendering\nwhenever it cannot be seen — tracked via an `IntersectionObserver`\non itself (scrolled out of the viewport, `display: none`) combined\nwith the document's page visibility (backgrounded browser tab,\nminimized window). Auto-pause is enabled by default.\n\n# Arguments\n\n- `autopause` Whether to enable `auto-pause` behavior or not.\n\n# JavaScript Examples\n\nDisable auto-size behavior:\n\n```javascript\nviewer.setAutoPause(false);\n```"},{"title":"JS API » setAutoSize","path":"perspective-viewer.d.ts","text":"setAutoSize(autosize: boolean): void\n\nSets the auto-size behavior of this component.\n\nWhen `true`, this `<perspective-viewer>` will register a\n`ResizeObserver` on itself and call [`Self::resize`] whenever its own\ndimensions change. However, when embedded in a larger application\ncontext, you may want to call [`Self::resize`] manually to avoid\nover-rendering; in this case auto-sizing can be disabled via this\nmethod. Auto-size behavior is enabled by default.\n\n# Arguments\n\n- `autosize` - Whether to enable `auto-size` behavior or not.\n\n# JavaScript Examples\n\nDisable auto-size behavior:\n\n```javascript\nviewer.setAutoSize(false);\n```"},{"title":"JS API » setSelection","path":"perspective-viewer.d.ts","text":"setSelection(window?: ViewWindow | null, options?: PanelOptions | null): void\n\nSet the selection [`perspective_js::JsViewWindow`] for the named panel,\nor the active panel when `panel` is omitted.\n\nJS API » setThrottle\n\nsetThrottle(val?: number | null): void\n\nDetermines the render throttling behavior. Can be an integer, for\nmillisecond window to throttle render event; or, if `None`, adaptive\nthrottling will be calculated from the measured render time of the\nlast 5 frames.\n\n# Arguments\n\n- `throttle` - The throttle rate in milliseconds (f64), or `None` for\n  adaptive throttling.\n\n# JavaScript Examples\n\nOnly draws at most 1 frame/sec:\n\n```rust\nviewer.setThrottle(1000);\n```"},{"title":"JS API » toggleColumnSettings","path":"perspective-viewer.d.ts","text":"toggleColumnSettings(column_name: string, options?: PanelOptions | null): Promise<any>\n\nAsynchronously opens the column settings for a specific column.\nWhen finished, the `<perspective-viewer>` element will emit a\n\"perspective-toggle-column-settings\" CustomEvent.\nThe event's details property has two fields: `{open: bool, column_name?:\nstring}`. The CustomEvent is also fired whenever the user toggles the\nsidebar manually.\n\nJS API » toggleConfig\n\ntoggleConfig(force?: boolean | null): Promise<any>\n\nToggle (or force) the config panel open/closed.\n\n# Arguments\n\n- `force` - Force the state of the panel open or closed, or `None` to\n  toggle.\n\n# JavaScript Examples\n\n```javascript\nawait viewer.toggleConfig();\n```"},{"title":"JS API » clear","path":"perspective-viewer.d.ts","text":"clear(): Promise<void>\n\nRemoves all the rows in the [`Table`], but preserves everything else\nincluding the schema, index, and any callbacks or registered\n[`View`] instances.\n\nCalling [`Table::clear`], like [`Table::update`] and [`Table::remove`],\nwill trigger an update event to any registered listeners via\n[`View::on_update`].\n\nJS API » columns\n\ncolumns(): Promise<any>\n\nReturns the column names of this [`Table`] in \"natural\" order (the\nordering implied by the input format).\n\n # JavaScript Examples\n\n ```javascript\n const columns = await table.columns();\n ```"},{"title":"JS API » delete","path":"perspective-viewer.d.ts","text":"delete(options?: DeleteOptions | null): Promise<void>\n\nDelete this [`Table`] and cleans up associated resources.\n\n[`Table`]s do not stop consuming resources or processing updates when\nthey are garbage collected in their host language - you must call\nthis method to reclaim these.\n\n# Arguments\n\n- `options` An options dictionary.\n    - `lazy` Whether to delete this [`Table`] _lazily_. When false (the\n      default), the delete will occur immediately, assuming it has no\n      [`View`] instances registered to it (which must be deleted first,\n      otherwise this method will throw an error). When true, the\n      [`Table`] will only be marked for deltion once its [`View`]\n      dependency count reaches 0.\n\n# JavaScript Examples\n\n```javascript\nconst table = await client.table(\"x,y\\n1,2\\n3,4\");\n\n// ...\n\nawait table.delete({ lazy: true });\n```"},{"title":"JS API » get_client","path":"perspective-viewer.d.ts","text":"get_client(): Promise<Client>\n\nGet a copy of the [`Client`] this [`Table`] came from.\n\nJS API » get_index\n\nget_index(): Promise<string>\n\nReturns the name of the index column for the table.\n\n# JavaScript Examples\n\n```javascript\nconst table = await client.table(\"x,y\\n1,2\\n3,4\", { index: \"x\" });\nconst index = table.get_index(); // \"x\"\n```\n\nJS API » get_limit\n\nget_limit(): Promise<number | undefined>\n\nReturns the user-specified row limit for this table.\n\nJS API » get_name\n\nget_name(): Promise<string>\n\nReturns the user-specified name for this table, or the auto-generated\nname if a name was not specified when the table was created."},{"title":"JS API » make_port","path":"perspective-viewer.d.ts","text":"make_port(): Promise<number>\n\nCreate a unique channel ID on this [`Table`], which allows\n`View::on_update` callback calls to be associated with the\n`Table::update` which caused them.\n\nJS API » on_delete\n\non_delete(on_delete: Function): Promise<any>\n\nRegister a callback which is called exactly once, when this [`Table`] is\ndeleted with the [`Table::delete`] method.\n\n[`Table::on_delete`] resolves when the subscription message is sent, not\nwhen the _delete_ event occurs.\n\nJS API » remove\n\nremove(value: any, options?: UpdateOptions | null): Promise<void>\n\nRemoves rows from this [`Table`] with the `index` column values\nsupplied.\n\n# Arguments\n\n- `indices` - A list of `index` column values for rows that should be\n  removed.\n\n# JavaScript Examples\n\n```javascript\nawait table.remove([1, 2, 3]);\n```"},{"title":"JS API » remove_delete","path":"perspective-viewer.d.ts","text":"remove_delete(callback_id: number): Promise<any>\n\nRemoves a listener with a given ID, as returned by a previous call to\n[`Table::on_delete`].\n\nJS API » replace\n\nreplace(input: any, options?: UpdateOptions | null): Promise<void>\n\nReplace all rows in this [`Table`] with the input data, coerced to this\n[`Table`]'s existing [`perspective_client::Schema`], notifying any\nderived [`View`] and [`View::on_update`] callbacks.\n\nCalling [`Table::replace`] is an easy way to replace _all_ the data in a\n[`Table`] without losing any derived [`View`] instances or\n[`View::on_update`] callbacks. [`Table::replace`] does _not_ infer\ndata types like [`Client::table`] does, rather it _coerces_ input\ndata to the `Schema` like [`Table::update`]. If you need a [`Table`]\nwith a different `Schema`, you must create a new one.\n\n# JavaScript Examples\n\n```javascript\nawait table.replace(\"x,y\\n1,2\");\n```"},{"title":"JS API » schema","path":"perspective-viewer.d.ts","text":"schema(): Promise<Record<string, ColumnType>>\n\nReturns a table's [`Schema`], a mapping of column names to column types.\n\nThe mapping of a [`Table`]'s column names to data types is referred to\nas a [`Schema`]. Each column has a unique name and a data type, one\nof:\n\n- `\"boolean\"` - A boolean type\n- `\"date\"` - A timesonze-agnostic date type (month/day/year)\n- `\"datetime\"` - A millisecond-precision datetime type in the UTC\n  timezone\n- `\"float\"` - A 64 bit float\n- `\"integer\"` - A signed 32 bit integer (the integer type supported by\n  JavaScript)\n- `\"string\"` - A [`String`] data type (encoded internally as a\n  _dictionary_)\n\nNote that all [`Table`] columns are _nullable_, regardless of the data\ntype."},{"title":"JS API » size","path":"perspective-viewer.d.ts","text":"size(): Promise<number>\n\nReturns the number of rows in a [`Table`].\n\nJS API » update\n\nupdate(input: string | ArrayBuffer | Record<string, unknown[]> | Record<string, unknown>[] | Record<string, ColumnType>, options?: UpdateOptions | null): Promise<any>\n\nUpdates the rows of this table and any derived [`View`] instances.\n\nCalling [`Table::update`] will trigger the [`View::on_update`] callbacks\nregister to derived [`View`], and the call itself will not resolve until\n_all_ derived [`View`]'s are notified.\n\nWhen updating a [`Table`] with an `index`, [`Table::update`] supports\npartial updates, by omitting columns from the update data.\n\n# Arguments\n\n- `input` - The input data for this [`Table`]. The schema of a [`Table`]\n  is immutable after creation, so this method cannot be called with a\n  schema.\n- `options` - Options for this update step - see [`UpdateOptions`].\n\n# JavaScript Examples\n\n```javascript\nawait table.update(\"x,y\\n1,2\");\n```"},{"title":"JS API » view","path":"perspective-viewer.d.ts","text":"view(config?: ViewConfigUpdate | null): Promise<View>\n\nCreate a new [`View`] from this table with a specified\n[`ViewConfigUpdate`].\n\nSee [`View`] struct.\n\n# JavaScript Examples\n\n```javascript\nconst view = await table.view({\n    columns: [\"Sales\"],\n    aggregates: { Sales: \"sum\" },\n    group_by: [\"Region\", \"Country\"],\n    filter: [[\"Category\", \"in\", [\"Furniture\", \"Technology\"]]],\n});\n```\n\nJS API » View\n\nexport class View\n\nThe [`View`] struct is Perspective's query and serialization interface. It\nrepresents a query on the `Table`'s dataset and is always created from an\nexisting `Table` instance via the [`Table::view`] method.\n\n[`View`]s are immutable with respect to the arguments provided to the\n[`Table::view`] method; to change these parameters, you must create a new\n[`View`] on the same [`Table`]. However, each [`View`] is _live_ with\nrespect to the [`Table`]'s data, and will (within a conflation window)\nupdate with the latest state as its parent [`Table`] updates, including\nincrementally recalculating all aggregates, pivots, filters, etc. [`View`]\nquery parameters are composable, in that each parameter works independently\n_and_ in conjunction with each other, and there is no limit to the number of\npivots, filters, etc. which can be applied."},{"title":"JS API » collapse","path":"perspective-viewer.d.ts","text":"collapse(row_index: number): Promise<number>\n\nCollapses the `group_by` row at `row_index`.\n\nJS API » column_paths\n\ncolumn_paths(window?: ColumnWindow | null): Promise<any>\n\nReturns an array of strings containing the column paths of the [`View`]\nwithout any of the source columns.\n\nA column path shows the columns that a given cell belongs to after\npivots are applied.\n\nJS API » delete\n\ndelete(): Promise<void>\n\nDelete this [`View`] and clean up all resources associated with it.\n[`View`] objects do not stop consuming resources or processing\nupdates when they are garbage collected - you must call this method\nto reclaim these."},{"title":"JS API » dimensions","path":"perspective-viewer.d.ts","text":"dimensions(): Promise<any>\n\nReturns this [`View`]'s _dimensions_, row and column count, as well as\nthose of the [`crate::Table`] from which it was derived.\n\n- `num_table_rows` - The number of rows in the underlying\n  [`crate::Table`].\n- `num_table_columns` - The number of columns in the underlying\n  [`crate::Table`] (including the `index` column if this\n  [`crate::Table`] was constructed with one).\n- `num_view_rows` - The number of rows in this [`View`]. If this\n  [`View`] has a `group_by` clause, `num_view_rows` will also include\n  aggregated rows.\n- `num_view_columns` - The number of columns in this [`View`]. If this\n  [`View`] has a `split_by` clause, `num_view_columns` will include all\n  _column paths_, e.g. the number of `columns` clause times the number\n  of `split_by` groups."},{"title":"JS API » expand","path":"perspective-viewer.d.ts","text":"expand(row_index: number): Promise<number>\n\nExpand the `group_by` row at `row_index`.\n\nJS API » expression_schema\n\nexpression_schema(): Promise<any>\n\nThe expression schema of this [`View`], which contains only the\nexpressions created on this [`View`]. See [`View::schema`] for\ndetails.\n\nJS API » get_config\n\nget_config(): Promise<any>\n\nA copy of the config object passed to the [`Table::view`] method which\ncreated this [`View`].\n\nJS API » get_min_max\n\nget_min_max(name: string): Promise<Array<any>>\n\nCalculates the [min, max] of the leaf nodes of a column `column_name`.\n\n# Returns\n\nA tuple of [min, max], whose types are column and aggregate dependent."},{"title":"JS API » num_columns","path":"perspective-viewer.d.ts","text":"num_columns(): Promise<number>\n\nThe number of aggregated columns in this [`View`]. This is affected by\nthe \"split_by\" configuration parameter supplied to this view's\ncontructor.\n\n# Returns\n\nThe number of aggregated columns.\n\nJS API » num_rows\n\nnum_rows(): Promise<number>\n\nThe number of aggregated rows in this [`View`]. This is affected by the\n\"group_by\" configuration parameter supplied to this view's contructor.\n\n# Returns\n\nThe number of aggregated rows.\n\nJS API » on_delete\n\non_delete(on_delete: Function): Promise<any>\n\nRegister a callback with this [`View`]. Whenever the [`View`] is\ndeleted, this callback will be invoked."},{"title":"JS API » on_update","path":"perspective-viewer.d.ts","text":"on_update(on_update_js: Function, options?: OnUpdateOptions | null): Promise<any>\n\nRegister a callback with this [`View`]. Whenever the view's underlying\ntable emits an update, this callback will be invoked with an object\ncontaining `port_id`, indicating which port the update fired on, and\noptionally `delta`, which is the new data that was updated for each\ncell or each row.\n\n# Arguments\n\n- `on_update` - A callback function invoked on update, which receives an\n  object with two keys: `port_id`, indicating which port the update was\n  triggered on, and `delta`, whose value is dependent on the mode\n  parameter.\n- `options` - If this is provided as `OnUpdateOptions { mode:\n  Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated\n  rows. Otherwise `delta` will be [`Option::None`].\n\n# JavaScript Examples\n\n```javascript\n// Attach an `on_update` callback\nview.on_update((updated) => console.log(updated.port_id));\n```\n\n```javascript\n// `on_update` with row deltas\nview.on_update((updated) => console.log(updated.delta), { mode: \"row\" });\n```"},{"title":"JS API » remove_delete","path":"perspective-viewer.d.ts","text":"remove_delete(callback_id: number): Promise<any>\n\nUnregister a previously registered [`View::on_delete`] callback.\n\nJS API » remove_update\n\nremove_update(callback_id: number): Promise<void>\n\nUnregister a previously registered update callback with this [`View`].\n\n# Arguments\n\n- `id` - A callback `id` as returned by a recipricol call to\n  [`View::on_update`].\n\nJS API » schema\n\nschema(): Promise<any>\n\nThe schema of this [`View`].\n\nThe [`View`] schema differs from the `schema` returned by\n[`Table::schema`]; it may have different column names due to\n`expressions` or `columns` configs, or it maye have _different\ncolumn types_ due to the application og `group_by` and `aggregates`\nconfig. You can think of [`Table::schema`] as the _input_ schema and\n[`View::schema`] as the _output_ schema of a Perspective pipeline."},{"title":"JS API » set_depth","path":"perspective-viewer.d.ts","text":"set_depth(depth: number): Promise<void>\n\nSet expansion `depth` of the `group_by` tree.\n\nJS API » to_arrow\n\nto_arrow(window?: ViewWindow | null): Promise<ArrayBuffer>\n\nSerializes a [`View`] to the Apache Arrow data format.\n\nJS API » to_columns\n\nto_columns(window?: ViewWindow | null): Promise<object>\n\nSerializes this [`View`] to JavaScript objects in a column-oriented\nformat.\n\nJS API » to_columns_string\n\nto_columns_string(window?: ViewWindow | null): Promise<string>\n\nSerializes this [`View`] to a string of JSON data. Useful if you want to\nsave additional round trip serialize/deserialize cycles."},{"title":"JS API » to_csv","path":"perspective-viewer.d.ts","text":"to_csv(window?: ViewWindow | null): Promise<string>\n\nSerializes this [`View`] to CSV data in a standard format.\n\nJS API » to_json\n\nto_json(window?: ViewWindow | null): Promise<Array<any>>\n\nSerializes this [`View`] to JavaScript objects in a row-oriented\nformat.\n\nJS API » to_ndjson\n\nto_ndjson(window?: ViewWindow | null): Promise<string>\n\nRenders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)\nformatted [`String`].\n\nJS API » with_typed_arrays\n\nwith_typed_arrays(window: TypedArrayWindow | null | undefined, callback: Function): Promise<void>\n\nFetches columns from the [`View`] in Arrow format, decodes them, and\npasses typed array views to `callback`. All arrays are only valid for\nthe duration of the callback — if `callback` returns a `Promise`, it\nis awaited before the backing Arrow buffer is released, so async\ncallbacks may use the views for the full duration of the awaited\nwork (e.g. across an `await requestAnimationFrame`-backed promise).\n\n# Arguments\n\n- `window` - Optional [`TypedArrayWindow`] controlling row/column\n  windowing and output options (e.g., `float32` mode).\n- `callback` - A JS function called with `(names: string[], values:\n  TypedArray[], validities: (Uint8Array|null)[], dictionaries:\n  (string[]|null)[]) => void | Promise<void>`."},{"title":"JS API » init","path":"perspective-viewer.d.ts","text":"export function init(module: WebAssembly.Module, url: URL): void\n\nRegister this crate's Custom Elements in the browser's current session.\n\nThis must occur before calling any public API methods on these Custom\nElements from JavaScript, as the methods themselves won't be defined yet.\nBy default, this crate does not register `PerspectiveViewerElement` (as to\npreserve backwards-compatible synchronous API).\n\nJS API » initSync\n\nexport function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput\n\nInstantiates the given `module`, which can either be bytes or\na precompiled `WebAssembly.Module`.\n\n@param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.\n\n@returns {InitOutput}"},{"title":"JS API » __wbg_init","path":"perspective-viewer.d.ts","text":"export default function __wbg_init (module_or_path: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>\n\nIf `module_or_path` is {RequestInfo} or {URL}, makes a request and\nfor everything else, calls `WebAssembly.instantiate` directly.\n\n@param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.\n\n@returns {Promise<InitOutput>}"},{"title":"Datagrid Plugin » EditMode","path":"types.ts","text":"export type EditMode =\n\nDatagrid cell interaction mode (`plugin_config.edit_mode`):\n`\"READ_ONLY\"` (default), `\"EDIT\"` (cells editable, writing back to the\n`Table` - requires an editable table), or the `\"SELECT_*\"` modes which\nemit selection events instead of editing.\n\nDatagrid Plugin » ColumnConfig\n\nexport interface ColumnConfig\n\nDatagrid per-column style configuration - one value of the\n`columns_config` map of a `ViewerConfigUpdate` when the Datagrid plugin\nis active. Valid keys depend on the column's type; the authoritative,\nvalue-dependent declaration is `column_config_schema()` (surfaced at\nruntime via the agent's `get_style_schema` tool and the Style tab)."},{"title":"Datagrid Plugin » color","path":"types.ts","text":"color?: string\n\nString / datetime columns: the applied color (CSS color).\n\nDatagrid Plugin » fg_colors\n\nfg_colors?: string\n\nNumeric columns: foreground sign-split colors — a CSS\n`linear-gradient(to right, #rrggbb 0%, #rrggbb 100%)`, t-ordered\n(the first stop is the negative color, the last the positive).\n\nDatagrid Plugin » bg_colors\n\nbg_colors?: string\n\nNumeric columns: background color scale, t-ordered with the\nsign pivot at offset 0.5.\n\nDatagrid Plugin » fg_gradient\n\nfg_gradient?: number\n\nNumeric columns: the absolute value at which bar/gradient\nforeground modes reach full scale."},{"title":"Datagrid Plugin » bg_gradient","path":"types.ts","text":"bg_gradient?: number\n\nNumeric columns: the absolute value at which gradient background\nmode reaches full scale.\n\nDatagrid Plugin » number_fg_mode\n\nnumber_fg_mode?: string\n\nNumeric columns: foreground treatment - `\"color\"` (default,\ncolored text), `\"bar\"` (proportional bar), `\"label-bar\"` (bar with\nlabel) or `\"disabled\"`.\n\nDatagrid Plugin » number_bg_mode\n\nnumber_bg_mode?: string\n\nNumeric columns: background treatment - `\"disabled\"` (default),\n`\"color\"` (solid fill) or `\"gradient\"` (fill intensity scaled to\nthe value)."},{"title":"Datagrid Plugin » string_color_mode","path":"types.ts","text":"string_color_mode?: string\n\nString columns: color mode (`\"foreground\"`, `\"background\"` or\n`\"series\"`). `\"foreground\"` / `\"background\"` pair with `color`;\n`\"series\"` pairs with `palette`.\n\nDatagrid Plugin » palette\n\npalette?: string\n\nString columns, `\"series\"` mode: explicit palette assigned to\ndistinct values in encounter order.\n\nDatagrid Plugin » datetime_color_mode\n\ndatetime_color_mode?: string\n\nDatetime columns: color mode (`\"foreground\"` or `\"background\"`),\npaired with `color`.\n\nDatagrid Plugin » aggregate_depth\n\naggregate_depth?: number\n\nGroup-by rollup depth override for this column when the view is\npivoted in `Rollup` mode."},{"title":"Datagrid Plugin » column_size_override","path":"types.ts","text":"column_size_override?: number\n\nPixel width override, written when a user drag-resizes a column.\n\nDatagrid Plugin » format\n\nformat?: string\n\nString columns: display format, e.g. `\"link\"`, `\"image\"`, `\"bold\"`.\n\nDatagrid Plugin » date_format\n\ndate_format?: DateFormatConfig\n\nDatetime columns: display format preset or custom fields.\n\nDatagrid Plugin » number_format\n\nnumber_format?: NumberFormatConfig\n\nNumeric columns: `Intl.NumberFormat`-style options controlling\ndigits, notation, currency, etc.\n\nDatagrid Plugin » DatagridPluginConfig\n\nexport interface DatagridPluginConfig\n\nDatagrid plugin-level configuration - the `plugin_config` slot of a\n`ViewerConfigUpdate` when the Datagrid plugin is active (the\n`save()`/`restore()` token)."},{"title":"Datagrid Plugin » scroll_lock","path":"types.ts","text":"scroll_lock?: boolean\n\nWhen `true`, the Datagrid keeps its scroll position pinned during\ndata updates instead of following appended rows.\n\nDatagrid Plugin » edit_mode\n\nedit_mode?: EditMode\n\nCell interaction mode - see {@link EditMode}.\n\nDatagrid Plugin » _panel\n\n_panel?: string\n\nThis datagrid's panel id (the plugin element's `slot`, stamped by the\nhost viewer) — `undefined` for a lone, unslotted panel. Passed as the\n`name` argument of the host's `*Panel` API variants so every viewer\ncall targets THIS panel, never the host's active panel."},{"title":"Datagrid Plugin » panel","path":"types.ts","text":"panel?: string\n\nThe id (`slot`) of the panel that fired this, in a multi-panel viewer."},{"title":"Charts Plugin » requestRender","path":"chart.ts","text":"requestRender(glManager: WebGLContextManager): Promise<void>\n\nThe single render entrypoint. Every render-triggering caller —\nupload chunks, zoom / pan, resize, theme invalidation,\nhost-driven redraws — calls this. Routes through the\nmodule-level scheduler ([render/scheduler.ts]) so concurrent\ncalls collapse to one `_fullRender` per RAF and the host\nblitter receives one bitmap per frame per chart.\n\nThe returned promise resolves after this entry's `_fullRender`\n+ `awaitGpuFence` + `endFrame` chain completes — independent\nof other charts in the same RAF, which run their fence waits\nin parallel.\n\nThe synchronous-render bypass for `snapshotPng` (calls\n`_fullRender` directly, skips `endFrame`) is the only\nsanctioned exception and lives inside the worker renderer."},{"title":"Charts Plugin » _fullRender","path":"chart.ts","text":"_fullRender(glManager: WebGLContextManager): void\n\nThe chart-specific GL frame builder. Submits the GL draw commands\nand stashes the frame's 2D-canvas draws (gridlines + chrome) for\nthe scheduler to flush after `awaitGpuFence`. The scheduler wraps\nthis with fence + 2D flush + `endFrame`; callers must not invoke\nit directly — `snapshotPng` uses {@link renderFrameSync} instead.\n\nCharts Plugin » renderFrameSync\n\nrenderFrameSync(glManager: WebGLContextManager): void\n\nSynchronous full frame (GL + 2D) for the `snapshotPng` bypass,\nwhich sits outside the scheduler and needs the gridline/chrome\ncanvases painted before compositing. Runs the GL pass then flushes\nthe deferred 2D draws immediately, skipping the fence split and\n`endFrame` so the GL backbuffer stays intact for `gl.readPixels`."},{"title":"Charts Plugin » setView","path":"chart.ts","text":"setView?(view: View): void\n\nHand the current View to the chart so it can make on-demand\nper-row queries (for lazy tooltip column lookups). Called on\nevery `draw`; the chart disposes any prior fetcher and clears\ndependent UI (pinned tooltip) so stale rows never surface.\n\nTODO: pinned tooltips are dismissed on view update today. A\nfuture enhancement is to keep a pinned tooltip visible (with its\ncaptured data) until the user dismisses it, even after the\nunderlying view no longer contains that row.\n\nCharts Plugin » setGridlineCanvas\n\nsetGridlineCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void\n\nSet the gridline canvas (behind WebGL, for gridlines)."},{"title":"Charts Plugin » setChromeCanvas","path":"chart.ts","text":"setChromeCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void\n\nSet the chrome canvas (above WebGL, for axes/labels/legend/tooltip).\n\nCharts Plugin » setOverlayPresenter\n\nsetOverlayPresenter?(cb: () => void): void\n\nInstall the renderer's overlay-present hook.\n\nCharts Plugin » setTheme\n\nsetTheme?(vars: Record<string, string>): void\n\nHand the chart a pre-computed CSS-variable map produced on the\nmain thread via `snapshotThemeVars(el)`, which it can decode into\na full `Theme` without touching the DOM (charts always run inside\nthe renderer scope, which has no `getComputedStyle`)."},{"title":"Charts Plugin » setZoomController","path":"chart.ts","text":"setZoomController?(zc: ZoomController): void\n\nSet the zoom controller for interactive zoom/pan.\n\nCharts Plugin » attachTooltip\n\nattachTooltip?(host: HostSink): void\n\nWire the chart's `TooltipController` for virtual-dispatch hover /\nclick events forwarded from the host. The renderer drives\n`dispatchHover` / `dispatchLeave` / `dispatchClick` /\n`dispatchDblClick` from `InteractionEvent`s; the supplied\n`HostSink` posts pin / dismiss / setCursor intents back to the\nhost so the resulting DOM mutations happen there (the renderer\nscope has no DOM in worker mode, and uses the same channel\nin-process for symmetry)."},{"title":"Charts Plugin » setColumnSlots","path":"chart.ts","text":"setColumnSlots?(slots: (string | null)[]): void\n\nSet the column slot config (with nulls for empty slots).\n\nCharts Plugin » setViewPivots\n\nsetViewPivots?(groupBy: string[], splitBy: string[]): void\n\nSet group_by and split_by config from the viewer.\n\nCharts Plugin » setColumnTypes\n\nsetColumnTypes?(schema: Record<string, string>): void\n\nSet column type schema from the view (e.g., { \"col\": \"date\" }).\n\nCharts Plugin » setGroupByTypes\n\nsetGroupByTypes?(schema: Record<string, string>): void\n\nSet the source-column types used for `group_by` level lookups —\nsourced from `table.schema()` + `view.expression_schema()`. Used\nby categorical-axis charts to detect numeric / date / boolean\ngroup_by levels (which are not keyed in `view.schema()` because\nthey surface as `__ROW_PATH_N__` columns)."},{"title":"Charts Plugin » setColumnsConfig","path":"chart.ts","text":"setColumnsConfig?(cfg: Record<string, any>): void\n\nSet per-column render config (the second argument to `plugin.restore`).\nKey is the aggregate base name; value is an open object whose\n`chart_type` / `stack` fields are consumed by the Y-bar glyph router.\n\nCharts Plugin » setDefaultChartType\n\nsetDefaultChartType?(chartType: string): void\n\nSet the plugin's default glyph type. Used by the Y-series chart\nfamily (Y Bar / Y Line / Y Scatter / Y Area): each tag is the same\n`BarChart` impl with a different starting `chart_type` applied to\ncolumns that lack an explicit entry in `columns_config`."},{"title":"Charts Plugin » setFacetConfig","path":"chart.ts","text":"setFacetConfig?(cfg: FacetConfig): void\n\nSet the faceting config: one small-multiple sub-plot per\n`split_by` group, optional shared axes, coordinated tooltip, and\nzoom routing mode. Seeded from `DEFAULT_FACET_CONFIG` at init;\n`plugin_config.facet_mode` / `facet_zoom_mode` override the\nmatching fields via `AbstractChart.setPluginConfig`.\n\nCharts Plugin » setPluginConfig\n\nsetPluginConfig?(cfg: PluginConfig): void\n\nSet the plugin-scoped global configuration — the values backing\n`plugin_config_schema` / `plugin_config` in `restore`. Replaces\nthe previous module-level constants (`LINE_WIDTH_PX`,\n`POINT_SIZE_PX`, `BAND_INNER_FRAC`, `BAR_INNER_PAD`,\n`WICK_WIDTH_PX`, `OHLC_LINE_WIDTH_PX`, `AUTO_ALT_Y_AXIS`) plus\nthe faceted/series zoom-mode semantics described in\n{@link PluginConfig}."},{"title":"Charts Plugin » invalidateTheme","path":"chart.ts","text":"invalidateTheme?(): void\n\nDrop any cached theme values so the next render re-reads CSS\nvariables. Driven from `plugin.restyle()`.\n\nCharts Plugin » resetExpandedDomain\n\nresetExpandedDomain?(): void\n\nClear the `domain_mode: \"expand\"` accumulator state so the next\ndata load starts from the current data extent. Driven from\n`resetAllZooms` (the user clicked \"Reset Zoom\"). View-config\nmutations route through `AbstractChart`'s `setColumnSlots` /\n`setViewPivots` / `setColumnTypes` setters, which call the same\nhook internally."},{"title":"Charts Plugin » deselect","path":"chart.ts","text":"deselect?(): void\n\nSilently clear any active selection state (pinned tooltip) WITHOUT\nemitting selection events. Driven from the host's `deselect`\nmessage — its global filter bar removed a clause this chart's\nselection contributed, so an unselect emit would double-mutate the\nhost's filter set.\n\nCharts Plugin » facet_mode\n\nfacet_mode: \"grid\" | \"overlay\"\n\n\"grid\" = small multiples (default); \"overlay\" = legacy single-plot.\n\nCharts Plugin » shared_x_axis\n\nshared_x_axis: boolean\n\nShare one bottom X axis across all columns of facets."},{"title":"Charts Plugin » shared_y_axis","path":"chart.ts","text":"shared_y_axis: boolean\n\nShare one left Y axis across all rows of facets.\n\nCharts Plugin » coordinated_tooltip\n\ncoordinated_tooltip: boolean\n\nPaint a tooltip in every facet (otherwise only the source facet).\n\nCharts Plugin » zoom_mode\n\nzoom_mode: \"shared\" | \"independent\"\n\n\"shared\" = one viewport for all facets; \"independent\" = per-facet.\n\nCharts Plugin » facet_padding\n\nfacet_padding: number\n\nPixel gap between adjacent facet cells in grid mode.\n\nCharts Plugin » PluginConfig\n\nexport interface PluginConfig\n\nPlugin-scoped global configuration — the user-facing settings backing\n`plugin_config_schema()` / the `plugin_config` slot in `restore`.\n\nEach chart type's `plugin_config_schema` returns only the fields that\nare applicable for that chart (see `applicable_plugin_fields` on\n`ChartTypeConfig`); inapplicable fields are hidden in the UI. The\nchart impl receives the full struct on `setPluginConfig` and reads\nonly the fields its render / build pipeline cares about.\n\nSome fields overlap with {@link FacetConfig} (`facet_mode`,\n`facet_zoom_mode`); the base `AbstractChart.setPluginConfig` syncs\nthose onto `_facetConfig` so deep render code keeps reading the\nsingle facet struct it already does. `series_zoom_mode` toggles the\ncategorical-Y chart base's `_autoFitValue` flag."},{"title":"Charts Plugin » auto_alt_y_axis","path":"chart.ts","text":"auto_alt_y_axis: boolean\n\nAuto-detect Y dual-axis splits when aggregate magnitudes differ\nby more than `DUAL_Y_RATIO_THRESHOLD`×. Series charts only.\nReplaces the `AUTO_ALT_Y_AXIS` compile-time toggle.\n\nCharts Plugin » facet_mode\n\nfacet_mode: \"grid\" | \"overlay\"\n\nFaceting strategy when `split_by` is non-empty.\n\n- `\"grid\"` — one small-multiple sub-plot per split group.\n- `\"overlay\"` — a single plot: cartesian charts differentiate\n  split groups by color; the categorical band pipeline (series\n  charts) stacks splits within each aggregate's band slot.\n  Synced into `_facetConfig.facet_mode`.\n\nThe default differs by family via\n`ChartTypeConfig.plugin_field_defaults`: cartesian / density /\nmap chart types default to `\"grid\"`, the series / financial\nband-pipeline types to `\"overlay\"` (their historical split\nrendering). Series charts REBUILD on a mode change — grid mode\nkeys the stack ladder per split (`facetSplits` in\n`buildSeriesPipeline`) so each facet grows from its own\nbaseline."},{"title":"Charts Plugin » facet_zoom_mode","path":"chart.ts","text":"facet_zoom_mode: \"shared\" | \"independent\"\n\nFaceted-cartesian zoom routing. `\"shared\"` — one viewport across\nall facets; `\"independent\"` — wheel/pan routes to the facet under\nthe cursor with its own viewport. Synced into\n`_facetConfig.zoom_mode`.\n\nCharts Plugin » series_zoom_mode\n\nseries_zoom_mode: \"fixed\" | \"dynamic\"\n\nSeries-chart value-axis behavior on zoom.\n\n- `\"dynamic\"` — value axis refits to the visible categorical\n  slice (current default; `CategoricalYChart._autoFitValue` =\n  true).\n- `\"fixed\"` — value axis stays pinned to the full-data extent."},{"title":"Charts Plugin » include_zero","path":"chart.ts","text":"include_zero: boolean\n\nAnchor the value axis to zero — when true, `0` is forced into\nthe rendered domain even if all data sits well above or below\nit. Natural for bar / area glyphs (which grow from the zero\nbaseline) and surprising for line / scatter (where the\ninteresting variation often lives far from zero). Per-chart-type\ndefaults route through `ChartTypeConfig.plugin_field_defaults`:\n`true` for Y Bar / Y Area / X Bar, `false` elsewhere.\n\nCharts Plugin » domain_mode\n\ndomain_mode: \"fit\" | \"expand\"\n\nDomain accumulation policy across successive `View` updates.\n\n- `\"fit\"` — every update recomputes the affected domains from\n  the current data extent. Can grow or shrink frame-to-frame.\n- `\"expand\"` — the affected domains monotonically *grow*: each\n  update unions the new data extent with the previously rendered\n  extent, so once a value is in scope it stays in scope. Reset\n  by the \"Reset Zoom\" button, view-config changes (group_by /\n  split_by / column-slot / column-type), or toggling back to\n  `\"fit\"`.\n\nAXIS SCOPE differs by family: cartesian charts (X/Y Scatter,\nX/Y Line, Density, Maps) apply it to BOTH axes plus the\ncolor/size scales (categorical string axes opt out — slot\nindices are frame-local); the categorical band pipeline (series\n/ financial) applies it to the VALUE axis only — Y for the\nY-family, X for X Bar — while the category axis always fits, so\na streaming numeric/datetime `group_by` axis releases departed\ncategories instead of pinning to its history."},{"title":"Charts Plugin » line_width_px","path":"chart.ts","text":"line_width_px: number\n\nWidth of polyline glyphs in CSS pixels (multiplied by DPR at GL\nupload). Replaces the duplicated `LINE_WIDTH_PX` constants in\nthe cartesian + series line glyphs.\n\nCharts Plugin » point_size_px\n\npoint_size_px: number\n\nDiameter of scatter point glyphs in CSS pixels. Replaces\n`POINT_SIZE_PX`.\n\nCharts Plugin » band_inner_frac\n\nband_inner_frac: number\n\nFraction of each category band occupied by its slot(s). Replaces\n`BAND_INNER_FRAC`. Affects buffer contents — takes effect on\nnext data load."},{"title":"Charts Plugin » bar_inner_pad","path":"chart.ts","text":"bar_inner_pad: number\n\nRelative inner padding between adjacent slots within a band.\nReplaces `BAR_INNER_PAD`. Affects buffer contents — takes effect\non next data load.\n\nCharts Plugin » wick_width_px\n\nwick_width_px: number\n\nCandlestick wick stroke width in CSS pixels. Replaces\n`WICK_WIDTH_PX`.\n\nCharts Plugin » ohlc_line_width_px\n\nohlc_line_width_px: number\n\nOHLC bar stroke width in CSS pixels. Replaces\n`OHLC_LINE_WIDTH_PX`.\n\nCharts Plugin » gradient_radius_px\n\ngradient_radius_px: number\n\ndensity splat radius in CSS pixels. Each data point is\nrasterized as a soft disk of this radius into the accumulation\nFBO before the gradient LUT pass resolves to a heat color."},{"title":"Charts Plugin » gradient_intensity","path":"chart.ts","text":"gradient_intensity: number\n\ndensity per-splat intensity multiplier. Controls how\nfast the density grows when points overlap (low values produce\na smoother, more diffuse field; high values produce sharper\npeaks).\n\nCharts Plugin » gradient_heat_max\n\ngradient_heat_max: number\n\ndensity clamp on the maximum accumulated heat used for\nthe gradient-LUT lookup. Lower values saturate sooner (more of\nthe LUT's hot stops show up); higher values stay cooler. In\nevery mode this controls the alpha (intensity) ramp; in\n`density` mode it also drives the hue, and in `signed` mode it\nscales the signed-sum-to-hue mapping."},{"title":"Charts Plugin » gradient_color_mode","path":"chart.ts","text":"gradient_color_mode: \"mean\" | \"density\" | \"extreme\" | \"signed\"\n\ndensity color-reduction mode. Controls how each pixel's\nstack of overlapping splats is reduced to a single LUT-t / alpha\npair in the resolve pass.\n\n- `mean` (default) — hue is the density-weighted average of\n  per-point color-t. Reads as \"the typical color-column value\n  in this region.\" Uses robust (5th/95th-percentile) bounds so\n  one outlier can't compress the rest of the data toward the\n  gradient midpoint.\n- `density` — ignore the color column even when wired; hue and\n  alpha both follow density. Reads as \"where do points cluster.\"\n  Useful when the color column is attached for tooltip lookup\n  only.\n- `extreme` — keep the per-pixel maximum signed deviation of\n  `t - 0.5` (split into positive and negative channels, MAX-\n  blended). Reads as \"where are the outliers.\" Density still\n  drives alpha so a single-point extreme fades naturally.\n  Requires a second framebuffer; uses MRT on WebGL2 hardware\n  with `OES_draw_buffers_indexed`, two passes otherwise.\n- `signed` — accumulate signed `t - 0.5` and let positive vs\n  negative cancel out. Reads as \"net positive vs net negative\n  in each region.\" Requires a float-capable framebuffer; on\n  `UNSIGNED_BYTE` fallback hardware degrades silently to\n  `mean` with a one-line console warning."},{"title":"Charts Plugin » map_tile_provider","path":"chart.ts","text":"map_tile_provider: \"carto-positron\" | \"carto-dark-matter\" | \"carto-voyager\"\n\nMap basemap tile provider. Applies only to map plugin tags\n(`map-scatter`, `map-line`, `map-density`). Cartesian charts\nignore the field. Surfaced as an enum on the settings panel so\nusers can switch light/dark/voyager without writing custom\ntile-source code.\n\nCharts Plugin » map_tile_alpha\n\nmap_tile_alpha: number\n\nMap basemap alpha (0..1). Pre-multiplied into the tile fragment\nshader's output so the chart's glyph layer composites over a\ndimmer or brighter version of the basemap. `1.0` (default)\nshows the tiles at full opacity."},{"title":"Charts Plugin » PluginConfigField","path":"charts.ts","text":"export type PluginConfigField = keyof PluginConfig\n\nSubset of `PluginConfig` keys that a chart impl actually consumes.\nDrives `plugin_config_schema()` filtering — the host only renders\nthe controls listed here, and `plugin.restore({ plugin_config })`\nstill hands the full struct to the worker (other keys are inert).\n\nCharts Plugin » applicable_plugin_fields\n\napplicable_plugin_fields: readonly PluginConfigField[]\n\nPlugin-config keys this chart type renders controls for. Empty\nfor plugins with no global settings (heatmap / treemap /\nsunburst). See {@link PluginConfig} for field semantics."},{"title":"Charts Plugin » group_by_role","path":"charts.ts","text":"group_by_role?: string\n\nWhat `group_by` DRAWS in this chart, e.g. `\"X Axis\"` for the\nY-series charts. Omitted where the field has no visual role of\nits own and is a plain aggregation key — the X/Y and map charts,\nwhose axes both come from `columns`.\n\nThe `group_by` counterpart of `initial.names`: one declaration\nsays what every view field draws, so the settings UI's labels and\nthe agent's `list_plugins` contract read the same source instead\nof restating the mapping.\n\nCharts Plugin » split_by_role\n\nsplit_by_role?: string\n\nWhat `split_by` DRAWS in this chart. See {@link group_by_role}."},{"title":"Charts Plugin » connects_row_order","path":"charts.ts","text":"connects_row_order?: boolean\n\nSet on the charts that CONNECT their points in row order, so the\nview's row order shows up in the drawing: without a `sort` the\nline follows the table's natural order, which reads as a tangle\nunless the rows already arrive ordered along the X axis. The point\ncharts (scatter, density) are unaffected, and a pre-ordered table\nneeds no `sort` — which is why this is declared per chart rather\nthan inferred from an empty `sort`.\n\nCharts Plugin » plugin_field_defaults\n\nplugin_field_defaults?: Partial<PluginConfig>\n\nPer-chart-type overrides for `DEFAULT_PLUGIN_CONFIG`. Used when a\nfield's sensible default differs by chart family — currently\n`include_zero` (true for Y Bar / Y Area / X Bar, false for line\n/ scatter / cartesian / financial) and `facet_mode` (\"overlay\"\nfor the band-pipeline families — series / financial — whose\nhistorical split_by rendering is the single stacked/colored\nplot; \"grid\" elsewhere). Applied at schema generation and at\n`restore({})` so the effective default matches the surfaced UI\ndefault."},{"title":"Column Format » CustomNumberFormatConfig","path":"CustomNumberFormatConfig.ts","text":"export type CustomNumberFormatConfig = { minimumIntegerDigits?: number | null, minimumFractionDigits?: number | null, maximumFractionDigits?: number | null, minimumSignificantDigits?: number | null, maximumSignificantDigits?: number | null, roundingPriority?: RoundingPriority | null, roundingIncrement?: number | null, roundingMode?: RoundingMode | null, trailingZeroDisplay?: TrailingZeroDisplay | null,\n\nA numeric column's `number_format` (`columns_config` value) —\n`Intl.NumberFormat`-shaped options, written by the Style tab's number\nformat editor and read by `createNumberFormatter`. The `style` and\n`notation` families ([`NumberFormatStyle`] / [`Notation`]) are serde-\nFLATTENED into this object but `#[ts(skip)]`'d (ts-rs cannot flatten\n`Option<enum>`) — the package's `NumberFormatConfig` re-composes the\nfull wire type as `CustomNumberFormatConfig & Partial<NumberFormatStyle>\n& Partial<Notation>` (see `column-format.ts`)."},{"title":"Column Format » useGrouping","path":"CustomNumberFormatConfig.ts","text":"useGrouping?: UseGrouping | null, signDisplay?: SignDisplay | null, }\n\nNOTE (audit 2026-08-05): serialized values are the STRINGS\n`\"always\"`/`\"auto\"`/`\"min2\"` or the untagged BOOLEAN `false` —\nthe former hand-written TS `useGrouping?: boolean` was wrong for\nthe string cases."},{"title":"Column Format » NumberFormatStyle","path":"NumberFormatStyle.ts","text":"export type NumberFormatStyle = { \"style\": \"decimal\" } | { \"style\": \"currency\" } & CurrencyNumberFormatStyle | { \"style\": \"percent\" } | { \"style\": \"unit\" } & UnitNumberFormatStyle\n\nThe `style` family of a numeric column's `number_format` — serialized\nFLATTENED into [`CustomNumberFormatConfig`]'s object, discriminated by\nthe `style` key (`\"decimal\"` default, `\"currency\"` + `currency`/\n`currencyDisplay`/`currencySign`, `\"percent\"`, `\"unit\"` + `unit`/\n`unitDisplay`), mirroring `Intl.NumberFormat` options."},{"title":"Column Format » Notation","path":"Notation.ts","text":"export type Notation = { \"notation\": \"standard\" } | { \"notation\": \"scientific\" } | { \"notation\": \"engineering\" } | { \"notation\": \"compact\" } & CompactDisplay\n\nThe `notation` family of a numeric column's `number_format` —\nserialized FLATTENED into [`CustomNumberFormatConfig`]'s object,\ndiscriminated by the `notation` key (`\"standard\"` default,\n`\"scientific\"`, `\"engineering\"`, `\"compact\"` + `compactDisplay`),\nmirroring `Intl.NumberFormat` options."},{"title":"Column Format » CustomDatetimeStyleConfig","path":"CustomDatetimeStyleConfig.ts","text":"export type CustomDatetimeStyleConfig = { format: FormatUnit,\n\nA datetime column's `date_format` in its `Custom` form: per-part\n`Intl.DateTimeFormatOptions` overrides, discriminated from the `Simple`\npreset form by the required `format: \"custom\"` key. Each part is a\n[`CustomDatetimeFormat`] variant valid for that `Intl` option;\n`\"disabled\"` omits the part from the formatted output entirely.\n\nColumn Format » timeZone\n\ntimeZone?: string | null,\n\nAn IANA time zone name (e.g. `\"America/New_York\"`); defaults to the\nbrowser's local time zone."},{"title":"Column Format » fractionalSecondDigits","path":"CustomDatetimeStyleConfig.ts","text":"fractionalSecondDigits?: number,\n\nSub-second digits to display, 0-3 (0 = none).\n\nColumn Format » weekday\n\nweekday?: CustomDatetimeFormat,\n\nA name form (`\"long\"`/`\"short\"`/`\"narrow\"`); defaults to\n`\"disabled\"` (weekday not shown).\n\nColumn Format » month\n\nmonth?: CustomDatetimeFormat,\n\nNumeric or name form; defaults to `\"numeric\"`."},{"title":"Column Format » SimpleDatetimeStyleConfig","path":"SimpleDatetimeStyleConfig.ts","text":"export type SimpleDatetimeStyleConfig =\n\nA datetime column's `date_format` in its `Simple` preset form:\n`Intl.DateTimeFormatOptions`' `dateStyle`/`timeStyle` presets. This is\nthe default form (no `format` key); setting `format: \"custom\"` selects\nthe per-part `CustomDatetimeStyleConfig` instead.\n\nColumn Format » timeZone\n\ntimeZone?: string | null,\n\nAn IANA time zone name (e.g. `\"America/New_York\"`); defaults to the\nbrowser's local time zone.\n\nColumn Format » dateStyle\n\ndateStyle?: SimpleDatetimeFormat,\n\nDate preset breadth, or `\"disabled\"` to omit the date entirely;\ndefaults to `\"short\"`."},{"title":"Column Format » timeStyle","path":"SimpleDatetimeStyleConfig.ts","text":"timeStyle?: SimpleDatetimeFormat, }\n\nTime preset breadth, or `\"disabled\"` to omit the time entirely;\ndefaults to `\"medium\"`."}]}