Metadata-Version: 2.5
Name: dash-tensor-grid
Version: 0.1.0
Summary: Plug-and-play OLAP / pivot grid for Dash - symmetric row & column roll-up/down, formula-DSL / SQL / Python aggregations, drill-through. Polars-powered backend, modern React frontend.
Project-URL: Homepage, https://github.com/amosneculau/dash-tensor-grid
Project-URL: Repository, https://github.com/amosneculau/dash-tensor-grid
Project-URL: Issues, https://github.com/amosneculau/dash-tensor-grid/issues
Project-URL: Changelog, https://github.com/amosneculau/dash-tensor-grid/blob/main/CHANGELOG.md
Author-email: Amos Neculau <amos.neculau@gmail.com>
License-Expression: SSPL-1.0
License-File: LICENSE
Keywords: aggregation,cube,dash,data-grid,drill-through,olap,pivot,plotly,polars
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Dash
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.12
Requires-Dist: dash>=2.11
Requires-Dist: numpy>=1.26
Requires-Dist: polars>=1.0
Provides-Extra: pandas
Requires-Dist: pandas>=2.2; extra == 'pandas'
Requires-Dist: pyarrow>=15; extra == 'pandas'
Provides-Extra: xlsx
Requires-Dist: xlsxwriter>=3; extra == 'xlsx'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6; extra == 'yaml'
Description-Content-Type: text/markdown

# Dash TensorGrid

Plug-and-play OLAP / pivot grid for [Dash](https://dash.plotly.com/) -- the
enterprise-grade, multi-dimensional data grid that Dash has been missing, free
and open-source.

Symmetric roll-up / roll-down, aggregations from `sum` to formula and arbitrary
Python (including numpy linear algebra), drill-through to source rows, and a
[Polars](https://pola.rs/) backend that does all the maths so the browser does
none of it.

> Status: alpha (0.1.0) — usable today; APIs may still change before 1.0. See
> [CHANGELOG.md](CHANGELOG.md) for progress.

## Pure Python, no Node required

Dash TensorGrid ships as a normal Python package. The compiled JavaScript
component is bundled inside the wheel, so installing and using it needs **only
`pip`** -- no Node.js, no npm, on any machine.

```bash
pip install dash-tensor-grid            # core (Polars backend)
pip install "dash-tensor-grid[pandas]"  # plus the pandas ingestion path
pip install "dash-tensor-grid[yaml]"    # plus YAML data sources + YAML table configs
```

(Node.js is only needed by maintainers who change the React source; the rebuilt
bundle is committed so the wheel can be built and published with Python alone.)

## Why

Client-side grids choke on nested group aggregation, and the existing answers for
server-side row models and pivoting in Dash sit behind paid enterprise licences.
Dash TensorGrid moves all aggregation to a fast Polars backend and ships a
deliberately thin React frontend: the backend owns the data, the frontend owns
the view.

## Feature highlights

- Symmetric roll-up / roll-down of hierarchical dimensions.
- Every aggregation style in one API:
  - built-in names: `sum`, `mean`, `min`, `max`, `product`, `median`, `std`, ...
  - Polars expressions: weighted sums, ratios of sums, dot products (vectorised).
  - arbitrary Python callables over each group: numpy matrix products, custom maths.
  - calculated measures: diff, multiplication, division, exponentiation between
    measures.
- Drill-through from any aggregated cell back to the source rows.
- Searching, sorting, and typed column filters (text, numeric range, select) -
  client-side and instant.
- Light and dark themes, fully customisable via CSS variables (`theme_overrides`);
  reads common host CSS variables so it blends into your app's existing theme.
- Action-button columns and computed synthetic columns.
- Agnostic input: Polars, pandas, `list[dict]`, or a JSON string.
- Correct at every level: parents re-aggregate from source rows, so means, medians
  and ratios never suffer the average-of-averages error.

### Spreadsheet & grid interactions

- **Cell range selection** (marquee): drag or shift-click a rectangular block
  (`enable_range_selection` -> `range_selection`), **copy it as TSV** with Ctrl/Cmd-C
  (`clipboard_copy`), and total it with the engine into a status-bar
  **range aggregation** (`GridDataEngine.aggregate_cells` -> `range_aggregation`).
- **Floating filter row** under the header (`enable_floating_filters`) and
  **quick-filter match highlighting** (`highlight_matches`).
- **Column tool-panel** to show/hide + reorder columns (`enable_column_panel`), plus
  a **density toggle** (`enable_density_toggle`) and **PDF / print** export
  (`enable_print`).
- **Clear-all-filters** button with an active-filter count (`enable_clear_filters`)
  and a **keyboard-shortcut help** dialog (`enable_shortcut_help`).
- **Row drag-reorder** (`enable_row_drag` -> `row_moved`) and **cell overflow
  tooltips** (`overflow_tooltip`).
- **Auto-group column** for a tidy tree/hierarchy presentation
  (`auto_group_column_def`); adjacency-list tree ingestion
  (`GridDataEngine.build_adjacency_tree`).
- **Per-column aggregation footer** computed by the engine
  (`GridDataEngine.column_footer` -> `footer`).
- **Typed cell renderers**: rating, boolean/checkbox, email/url/phone links, tags,
  badges/chips, progress bars, sparklines, data bars and icon sets - all display-only.
- **Scales to large grids**: row *and* column virtualization
  (`enable_virtualization`, `enable_column_virtualization`), pagination, pinned rows
  and columns; touch / pointer support; keyboard navigation; internationalised UI
  strings (`locale_text`). The frontend still does zero maths.

## Quick start

```python
import polars as pl
from dash import Dash, Input, Output, callback
from dash_tensor_grid import TensorGrid, GridDataEngine

df = pl.DataFrame(
    {
        "region": ["EMEA", "EMEA", "AMER", "AMER"],
        "country": ["DE", "FR", "US", "US"],
        "revenue": [100.0, 80.0, 200.0, 40.0],
        "cost": [60.0, 50.0, 120.0, 25.0],
    }
)
engine = GridDataEngine(df)

app = Dash(__name__)
app.layout = TensorGrid(id="grid", data_mode="full")


@callback(Output("grid", "row_data"), Output("grid", "column_defs"), Input("grid", "id"))
def load(_):
    agg = {"revenue": "sum", "cost": "sum"}
    calculated = {"margin": lambda m: (m["revenue"] - m["cost"]) / m["revenue"]}
    rows = engine.get_tree_payload(["region", "country"], agg, calculated=calculated)
    cols = engine.make_column_defs(
        ["region", "country"], agg, calculated=calculated,
        formats={"revenue": "currency:USD", "cost": "currency:USD", "margin": "percentage:1"},
    )
    return rows, cols


if __name__ == "__main__":
    app.run(debug=True)
```

## Examples

Runnable demos over synthetic high-dimensional data (no JS build needed):

- `examples/engine_demo.py` -- roll-up tree, lazy mode, drill-through (engine only).
- `examples/aggregations_showcase.py` -- every aggregation style, incl. a weighted
  vector dot product and a numpy matrix-multiplication aggregation.
- `examples/showcase_app.py` -- feature-dense grid (grouping, selection, export,
  cell-click, column chooser, action buttons with icons), all via callbacks.
- `examples/pivot_app.py` / `examples/pivot_demo.py` -- symmetric column pivoting.
- `examples/client_side_agg.py` -- front-end (in-browser) aggregation mode.
- `examples/formula_app.py` -- complex calculated measures evaluated in JS.
- `examples/config_app.py` + `examples/table_config.yaml` -- the whole table defined
  in a declarative YAML config.
- `examples/lazy_app.py` -- lazy / server-side row model over 200k rows.
- `examples/infinite_app.py` -- infinite (server-side) row model over a huge flat table.
- `examples/actions_app.py` -- action-button columns + a computed synthetic column.
- `examples/editing_app.py` -- inline cell editing (double-click, commit on Enter).
- `examples/master_detail_app.py` -- master/detail: expand a row to a detail panel.
- `examples/source_editing_app.py` -- master/detail SOURCE-row editing: edit a detail
  cell and `apply_edit` recomputes every aggregate server-side (stale edits rejected).
- `examples/pivot_ui_app.py` -- interactive pivot configurator (drag Rows/Columns/Values).
- `examples/showcase_colgroups.py` -- nested column groups without aggregation.
- `examples/showcase_pivot.py` -- column groups with aggregation (a cross-tab pivot).
- `examples/watchlist_app.py` -- in-cell sparkline / bar charts.
- `examples/showcase_locale.py` -- locale / region number & date formatting.
- `examples/adjacency_app.py` -- flat parent/child rows -> nested tree with rollups.
- `examples/range_agg_app.py` -- cell range selection -> engine-computed aggregation.
- `examples/auto_group_app.py` -- the auto-group (hierarchy) column.
- `examples/footer_app.py` -- an engine-computed per-column aggregation footer.
- `examples/showcase_all.py` -- many interaction features enabled on one grid.
- `usage.py` -- a full Dash application.

## The multi-framework monorepo (`@tensorgrid/*`)

The same engine, beyond Dash: `packages/` hosts an npm-workspaces monorepo that
replicates the Python engine in TypeScript and ships thin adapters for multiple
frontends. **The Python Polars engine is the oracle** — the TS port is verified
byte-for-byte against golden fixtures generated by the real engine
(`tools/oracle/gen_fixtures.py`), and CI re-proves parity against a freshly
regenerated oracle on every push (`parity-drift` job). 110 `node --test` checks
(golden-fixture parity suites + view / interaction / filter units).

| Package | What it is | License |
|---|---|---|
| `@tensorgrid/contract` | Wire types + closed-enum guards + `wire-format.md` (the scalar serialization contract, incl. the editing write-back shape) | SSPL-1.0 |
| `@tensorgrid/core` | The TS engine replica: tree roll-up, pivot, reducers, formula DSL, drill-through, selection/export, source-row editing, view/virtualization/format/render helpers | SSPL-1.0 |
| `@tensorgrid/react`, `@tensorgrid/vue`, `@tensorgrid/vanilla` | Thin adapters: grid + pivot + virtual grid components/mounts | SSPL-1.0 |

Feature surface (all adapters, one engine): roll-up grid, symmetric cross-tab,
windowed virtual grid (65,640 nodes, O(window) DOM), click-to-sort, quick +
column filters, ctrl/cmd-click selection with source-row CSV export,
master-detail (leaf expands to its drill-through source rows), and **editing**
in both modes — client-side (`applyEdit`: source-row write-back with a stale
guard, aggregates recomputed by the engine) and server-side (the engine's
`apply_edit` behind a `POST /api/edit` wire route: one round-trip returns the
recomputed tree; stale edits are rejected by the engine). The frontend does
zero math in every mode.

**Framework-neutral filter UI.** Beyond the quick filter, the `@tensorgrid/react` and
`@tensorgrid/vue` adapters render an opt-in per-column filter across **all three grids**
(`enableColumnFilters`): a root-dimension control chosen by `rootFilter`
(`'text' | 'select' | 'set' | 'conditions' | 'date'` — substring box, single-value
dropdown, multi-value checkbox list, an operator builder, or a from/to date range) plus a
min/max **range** control per measure on `<TensorGrid>`. The same control is reused by
`<TensorVirtualGrid>` (a filter **bar** above the windowed scroller — root dimension only)
and `<TensorPivot>` (a filter **row** that prunes the row hierarchy). `<TensorPivot>` adds
`enableColumnGroupFilter`: a corner checkbox picker that hides pivoted **column** groups —
a client-side display prune (the grand-total column is kept; nothing is re-aggregated). All
of it is driven by ONE framework-neutral core — the pure `filterReducer`,
`columnFilterPredicate`, `facetCounts` (value lists), the single-sourced condition evaluator
(`conditions.ts`), and the pivot column helpers (`pivotColumnGroups` /
`filterPivotColumnDefs`) — over the contract's typed discriminated `FilterValue` model. Each
adapter renders only the idiomatic controls (React hooks / Vue reactive) via one shared
root-control builder per framework and drives the shared reducer through the exported
`useColumnFilters` hook / composable — **zero math in the adapter**, same predicates client
and server.

**Conditional formatting.** A `cellStyles` prop / option (per measure column) styles cells by
their raw value: `{ revenue: [{ op: '<', value: 0, style: { color: 'red' }, className: 'neg' }] }`.
Rules use the SAME operator vocabulary as the filter conditions and are evaluated first-match-wins by
the core `evalCellStyle` (a `CellStyleRule` model) — so a `<` format rule colours the same cells a
`<` filter keeps, and the frontend does zero math. Supported on **all three grids** in **every
adapter** and both reference renderers (`renderGridHtml` + `renderPivotHtml`): `<TensorGrid>` and
`<TensorVirtualGrid>` key `cellStyles` by measure accessorKey; `<TensorPivot>` keys by **measure name**
so a rule heatmaps every pivoted column cell of that measure. React/Vue take camelCase CSS in `style`;
the string renderers auto-convert to kebab for the inline attribute.

**Top-N.** A `topN` prop / option (`{ by, n, others?, othersLabel? }`) keeps the top-`n` children per
level by a measure and (with `others`) rolls the rest into an "Others" node that SUMS the measures —
applied over the built tree via the core `topNChildren` (after filtering, before sort; the frontend
does no math). Available on `<TensorGrid>` in all three adapters. Note it ranks every top-level node,
so avoid combining it with `grandTotal` (the grand-total row would occupy a top-N slot).

**Window columns.** A `windowColumns` prop / option (`[{ kind, by, as, header?, format?, descending?,
dense?, percent? }]`) adds derived read-only measure columns via core transforms — `kind` is one of
`runningTotal` (cumulative sum of `by` across siblings), `rank` (1-based sibling rank), `pctOfTotal`
(share of the grand total), `pctOfParent` (share of the parent), `delta` (change from the previous
sibling — period-over-period; absolute, or a fraction when `percent: true`; a grand-total row is
skipped), or `movingAverage` (trailing simple moving average over the last `window` siblings — default
3). Each writes into key `as` and renders as an extra measure column after the base measures — the
frontend does no math. The percent kinds store a fraction in `[0, 1]`, so format them with a `%` pattern
(e.g. `format: '0.0%'`). Available on `<TensorGrid>` in all three adapters.

**Footer (grand-total) row.** A `showFooter` prop / option renders a sticky `<tfoot>` with each base
measure aggregated over the WHOLE frame via core `gridGrandTotal` (the engine's semantics — not a
client-side sum of pre-aggregated leaves, which would give mean-of-means); `footerLabel` sets the label
(default `"Total"`). Available on `<TensorGrid>` in all three adapters + the reference `renderGridHtml`.

**Color scale (heatmap).** A `colorScales` prop / option (`{ revenue: { min, max, mid?, domainMin?,
domainMax? } }`) heatmaps a measure column — each cell's background is interpolated from its raw value's
position in that column's range (over the whole tree, excluding a grand-total row so it doesn't compress
the scale). Presentation only (like formatting) — the grid does no math; an explicit `cellStyles`
background wins. Available on `<TensorGrid>` in all three adapters + the reference `renderGridHtml`.

**Data bars.** A `dataBars` prop / option (`{ revenue: { color, domainMin?, domainMax? } }`) draws an
in-cell bar (a `linear-gradient` background) whose length is the value's fraction of `[0, column max]` —
works on base measures and window columns. Cell-background precedence when several apply: an explicit
`cellStyles` background > `dataBars` > `colorScales`. Available on `<TensorGrid>` in all three adapters +
the reference `renderGridHtml`.

**Column pinning.** A `pinFirstColumn` prop / option freezes the group (label) column via
`position: sticky` so it stays visible on horizontal scroll (handy when many measure / window columns
overflow). The adapter positions the group cells and tags them with a `tg-pinned` class; the HOST wraps
the grid in an `overflow-x: auto` container and gives `.tg-pinned` an opaque background (so scrolled
content does not bleed through and row hover / selection styling still applies). Available on
`<TensorGrid>` / `mountTensorGrid` in all three adapters, and on the reference `renderGridHtml`.

**Range selection.** Beyond ctrl/cmd-click toggle, a **shift-click** extends the selection to the
contiguous span of rows from the anchor to the clicked row (inclusive, either direction, in visible
order). The anchor is seeded by a ctrl/meta toggle or a prior shift-range, and the core
`selectionRange(orderedIds, anchor, target)` owns the range math — the adapter only wires the event and
unions the result into the selection. Available on `<TensorGrid>` / `mountTensorGrid` in all three
adapters: React/Vue fire `onSelectionChange` with the selected ids; the vanilla mount exposes them via
`handle.getSelected()`.

**Copy to clipboard.** With a non-empty selection, Ctrl/Cmd-C copies the selected SOURCE rows as CSV.
Enable it with `enableClipboard` (best-effort write to the system clipboard) and/or `onCopy(csv)` (a
`<TensorGrid>` prop in React/Vue, a `mountTensorGrid` option in vanilla — receive the CSV directly). It
composes the core `toCsv(rowsForSelection(...))` — the same path the vanilla `handle.selectionToCsv()`
uses — so the adapter does no math. Available on `<TensorGrid>` / `mountTensorGrid` in all three adapters.

**Expand controls.** Reveal or collapse the tree to any depth in one click. React/Vue take an opt-in
`showExpandControls` prop that renders an Expand all / Collapse all / Level N toolbar above the grid; the
vanilla mount exposes it imperatively as `handle.expandToLevel(n)` / `handle.expandAll()` /
`handle.collapseAll()`. All of it runs the core `expandToDepth(tree, depth)` — which returns the expanded-state
that reveals every node down to `depth` — so the adapter owns no expansion math (the host styles the
`.tg-expand-controls` chrome).

**Selection status bar.** An opt-in `showStatusBar` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid`
option in vanilla) renders a `.tg-status-bar` footer with `Selected: N` and, per measure, count/sum/avg/min/max
computed by the core `summarizeRows` over the selected SOURCE rows. It only appears while a selection exists,
and the host styles the `.tg-status-bar` / `.tg-status-count` / `.tg-status-measure` chrome — the adapter does
no math.

**Keyboard navigation.** An opt-in `enableKeyboardNav` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid`
option in vanilla) turns on roving-tabindex keyboard operation: Arrow Up/Down move a focused active row
(`.tg-active`), Home/End jump to first/last, Space toggles the active row's selection, Shift+Arrow extends the
selection range (core `selectionRange`), and Enter / ArrowRight / ArrowLeft expand or collapse a group row. The
host styles the `.tg-active` chrome; the adapter wires the events and reuses the core range math.

**Built-in search.** An opt-in `showSearch` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid` option in
vanilla; plus `searchPlaceholder`) renders a built-in `.tg-search` input above the grid that filters rows via
the core `quickFilterPredicate` (case-insensitive substring over the row dimensions). It ANDs with the
`quickFilter` prop / `setFilter` predicate rather than replacing it; the host styles the `.tg-search` /
`.tg-search-input` chrome. The vanilla mount keeps the input persistent across its innerHTML rebuild (a
`.tg-grid-host` split) so its focus and cursor survive typing.

**CSV export.** An opt-in `showExportButton` renders an Export button above the grid; clicking it fires
`onExport(csv)` with the CSV of the selected SOURCE rows (core `rowsForSelection` + `toCsv`) if a selection
exists, else all source rows. React/Vue are callback-only (the host wires the download/upload); the vanilla
mount additionally triggers a best-effort `.csv` download and exposes `handle.exportCsv()` (headless — the same
selection-or-all CSV). The host styles the `.tg-export` / `.tg-export-button` chrome; the adapter does no math.

**Selection toolbar.** An opt-in `showSelectionToolbar` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid`
option in vanilla; plus `selectAllLabel` / `clearSelectionLabel`) renders a `.tg-selection-toolbar` with
"Select all" / "Clear" buttons above the grid — a one-click complement to the export button (select all, then
export the selection). "Select all" selects every currently-visible row **except** synthetic aggregate rows
(the grand-total "Total" row and top-N "Others" roll-ups — core `isSyntheticNodeId`), since those do not map to
selectable source rows; "Clear" empties the selection. React/Vue fire `onSelectionChange`; vanilla is
pull-model (read the result via `handle.getSelected()`). The host styles the `.tg-select-all` /
`.tg-clear-selection` chrome.

**Density toggle.** An opt-in `showDensityToggle` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid` option
in vanilla; plus a `density` initial level and, for React/Vue/vanilla, an `onDensityChange` callback) renders a
`.tg-density-toggle` button that cycles the row density comfortable -> standard -> compact, applying the chosen
level as a `tg-density-<level>` class on the `.tg-grid` table so the host styles the row padding/height. Setting
just `density` (without the toggle) pins the level to that prop — the host controls it, and changing it later
re-applies the class (React re-syncs via effect, Vue via a watcher, vanilla via `handle.update({ density })`).
Not applicable to `mountVirtualGrid`, which sizes rows by a fixed `rowHeight`. The adapter does no layout math —
it only toggles the class; mirrors the Dash component's `enable_density_toggle`.

**No-rows message.** An opt-in `noRowsMessage` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid` option in
vanilla) renders a single full-width `.tg-no-rows` placeholder row when the grid flattens to zero rows (e.g. a
search/filter matched nothing) instead of an empty body. Omitted = empty body (byte-identical back-compat); the
host styles the `.tg-no-rows` / `.tg-no-rows-cell` chrome. Mirrors the Dash component's `no_rows_message`.

**Column visibility.** An opt-in `showColumnToggle` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid`
option in vanilla; plus `hiddenColumns` for the initial hidden set and an `onColumnVisibilityChange` callback)
renders a `.tg-column-toggle` control — one checkbox per **measure** column that shows/hides it entirely
client-side (the group/dimension column is always shown). No backend round-trip: the adapter just renders fewer
columns. The host styles the `.tg-column-toggle` / `.tg-column-toggle-item` chrome.

**Column resize.** An opt-in `resizableColumns` (a `<TensorGrid>` prop in React/Vue, a `mountTensorGrid` option in
vanilla; plus `columnWidths` for initial widths and an `onColumnResize` callback) adds a drag handle to each
header — the measure columns **and** the group/dimension column (keyed `"__group__"` in the widths map, kept
sticky when `pinFirstColumn` is on) — dragging sets that column's width (client view state; core
`renderGridHtml` emits the `.tg-resize-handle`). For strict widths incl. shrinking below content, give the table
`table-layout: fixed`; otherwise a width acts as a minimum. The host styles the `.tg-resize-handle` chrome (e.g.
an absolutely-positioned right-edge grip with `cursor: col-resize`).

```powershell
npm install
npm run build:ts        # tsc -b (strict) across the workspaces
npm run test:ts         # 147 node:test checks incl. Polars-oracle parity

# Demos: serve packages/ statically and open /index.html
# The server-mode demo instead runs the real Python engine (thread-capped):
uv run python scripts/preview_wire_api.py   # then open http://127.0.0.1:8067/server-demo.html
```

## Documentation

The [`examples/dash_gallery`](examples/dash_gallery) app is a runnable gallery of
every feature — aggregation, pivot / cross-tab, the formula DSL, drill-through,
number / date formatting, conditional formatting, live streaming, export, and
more — each with its input data and source shown. Run it with:

```bash
python examples/dash_gallery/app.py    # then open http://127.0.0.1:8050/
```

## Development

Hybrid Python package plus React component. Python **3.12** is the hard floor.

```powershell
uv sync                  # Python deps (runtime + dev)
.\scripts\run_tests.ps1  # tests (core- and thread-capped)

# Only when the React source under src/ changes (needs Node.js):
npm install
npm run build            # rebuilds the bundle + generated Python class (commit them)
uv build                 # build the wheel (Python only)

# The @tensorgrid/* TS monorepo (separate from the Dash bundle build):
npm run build:ts         # strict tsc -b across packages/
npm run test:ts          # node:test parity + unit suites
```

> This package is Polars-based, and Polars uses every CPU core by default. Run
> tests and benchmarks through `scripts/run_tests.ps1`, which caps cores and
> threads to keep resource use bounded.

## Licence

**Server Side Public License, v1 ([SSPL-1.0](LICENSE)).** Every package — the Dash
component, `@tensorgrid/contract`, `@tensorgrid/core`, and the React / Vue / Vanilla
adapters — is licensed under the SSPL.

In plain terms (the MongoDB model):

- **Free to use, self-host, modify, and build products on** — individuals and companies
  alike, including internal/commercial use inside your own applications.
- **Offering TensorGrid *as a service* to third parties** (a hosted / managed
  "TensorGrid-as-a-service", the AWS / Azure / cloud-provider case) triggers the SSPL's
  copyleft: you must release the *entire* service source under the SSPL, **or** obtain a
  commercial license.

**Commercial licensing** (to embed or offer TensorGrid without the SSPL's service-source
obligations) is available from **INTENDEV LTD** — see [COMMERCIAL-LICENSE.md](COMMERCIAL-LICENSE.md).
