Metadata-Version: 2.4
Name: gt-charts
Version: 0.1.0b2
Summary: Render embeddable charts in Databricks and Jupyter notebooks
Keywords: echarts,charts,visualization,databricks,jupyter,quarto,notebook
Author: Tom Winskell
Author-email: Tom Winskell <129330818+tomwinskell@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Framework :: Jupyter
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/Giving-Tuesday/gt-charts
Project-URL: Repository, https://github.com/Giving-Tuesday/gt-charts
Project-URL: Issues, https://github.com/Giving-Tuesday/gt-charts/issues
Description-Content-Type: text/markdown

# gt-charts

Render embeddable charts inside Databricks and local Jupyter notebooks.

`gt_charts` is a thin Python wrapper around [ECharts](https://echarts.apache.org/): you pass
a native ECharts `option` (`xAxis`, `yAxis`, `series`, `tooltip`, …) and it emits a
self-contained HTML `<section>` that our JS embed app turns into a chart. **Nothing is drawn
in Python** — the option you write is the option the chart renders. The same `<section>` is
what a content manager would paste into a CMS page, so what you preview in the notebook is
what ships.

## Requirements

- Python ≥ 3.11
- [uv](https://docs.astral.sh/uv/) for dependency management (development)
- A running chart bundle host (see [Configuration](#configuration))

## Install

To use the published package:

```bash
pip install gt-charts          # then: import gt_charts as gt
```

For development in this repo:

```bash
uv sync
```

This installs the package and dev dependencies (IPython, JupyterLab, pytest, Ruff) into
`.venv`.

## Quickstart

In a notebook cell:

```python
import gt_charts as gt

gt.chart(
    xAxis={"type": "category", "data": ["Jan", "Feb", "Mar"]},
    yAxis={"type": "value"},
    series=[
        {"type": "line", "name": "Donations", "data": [12, 19, 7]},
        {"type": "line", "name": "Grants", "data": [5, 8, 11]},
    ],
    title="Monthly funding",
).show()   # render inside the notebook
```

Every keyword except the reserved chrome names below is collected into the ECharts `option`
verbatim — if you know the [ECharts option reference](https://echarts.apache.org/en/option.html),
you already know the API. `series` is required.

### Reserved chrome keywords

These keywords are owned by the embed wrapper, not the ECharts option — they become `data-*`
attributes on the `<section>` and are each emitted only when you pass them:

| Keyword | Attribute | Meaning |
|---------|-----------|---------|
| `title` | `data-title` | Heading rendered *around* the canvas |
| `subtitle` | `data-sub-title` | Sub-heading around the canvas |
| `description` | `data-description` | Longer descriptive text |
| `theme` | `data-theme` | Embed visual theme (`gt-light` / `fep-light`) |
| `showDownload` | `data-show-download` | Show the download control (bool) |
| `showWatermark` | `data-show-watermark` | Show the watermark (bool) |

ECharts has its own in-canvas `title` component, but the embed wrapper already renders a
title around the canvas — so `title=` is always embed chrome, and folding a title into the
option would render it twice. Everything the embed owns (grid, tooltip, legend styling)
follows the active `theme` in the JS app, so it is not part of the option either.

### Theme

`theme` selects the embed app's visual theme. Valid values are `"gt-light"` and
`"fep-light"`; an unknown or omitted theme drops the `data-theme` attribute and the embed app
falls back to its own default.

### Outputs

`gt.chart(...)` returns a `Chart` with three outputs:

- `chart.show()` — render inside the notebook (Databricks or local Jupyter). A bare
  `gt.chart(...)` as a cell's last expression auto-renders too.
- `chart.to_section()` — return the raw `<section>` embed HTML as a string (for copy-paste
  into a CMS, or programmatic use).
- `chart.to_option()` — return the ECharts `option` dict (mostly useful for debugging and
  tests).

## Configuration

The chart's JavaScript and CSS are loaded from a bundle host, resolved from the
`GT_CHARTS_BUNDLE_URL` environment variable. It defaults to the staging bundle
(`https://mode-embed-staging.netlify.app`).

Pass the **base URL only** — `gt_charts` appends `/gt-embed.js` and `/gt-embed.css` for
you. To point at a local dev server serving the bundle under `/build`:

```python
import os
os.environ["GT_CHARTS_BUNDLE_URL"] = "http://localhost:3000/build"

import gt_charts as gt
gt.chart(series=[{"type": "line", "data": [12, 19]}]).show()
```

The variable is read at render time, so set it **before** calling `show`/`to_section`
(re-run the render cell if you change it).

### Gotcha: `%env` and quotes

The IPython `%env` magic stores everything after `=` literally, **including quotes**:

```python
%env GT_CHARTS_BUNDLE_URL="http://localhost:3000/build"   # ❌ quotes become part of the URL
%env GT_CHARTS_BUNDLE_URL=http://localhost:3000/build      # ✅ no quotes
```

With quotes, the generated tag is `<script src=""http://localhost:3000/build"/...">` —
an empty `src`, so the chart never loads. Prefer `os.environ[...] = "..."` (there the
quotes are Python syntax and don't end up in the value) if you want to avoid the rule.

## How rendering works

`show()` picks the right display path for the environment:

- **Databricks** — uses the notebook's global `displayHTML`, which sandboxes output.
- **Local Jupyter** — wraps the document in a sandboxed `data:` URI iframe so script
  loading matches Databricks' behavior.

In both cases the chart bundle is fetched from `GT_CHARTS_BUNDLE_URL`. If a chart renders
blank locally, check the iframe's devtools Console/Network for a failed bundle request
(wrong path, `404`, or mixed content if the notebook is served over HTTPS).

## Testing

There are two layers:

```bash
uv run pytest                              # the automated unit tests (in tests/)
uv run jupyter lab Test.ipynb              # the manual smoke test notebook
```

The pytest suite covers `chart()` (option stored verbatim, chrome kept out of it) and the
render stage (option → `<section>`) — without a browser. Run it after any change to
`chart.py` or `html.py`.

`Test.ipynb` is the visual end-to-end check: it builds a chart per type and renders it so you
can eyeball the output in a real browser. The first cell points `GT_CHARTS_BUNDLE_URL` at
`http://localhost:3000/build`, so start your local bundle server first — or edit that cell to
target another host (e.g. the staging default). Launch it from the project root:

```bash
uv run jupyter lab Test.ipynb
```

Run the cells top to bottom (Restart Kernel and Run All is a clean check) and confirm the
chart renders. Do not commit `Test.ipynb` output or checkpoints; scratch edits to it stay
local.

## Development

Formatting and linting use [Ruff](https://docs.astral.sh/ruff/):

```bash
uv run ruff format src/    # format
uv run ruff check src/     # lint
```

`.vscode/settings.json` enables format-on-save and import sorting via the Ruff extension
(`charliermarsh.ruff`) — install it from the Marketplace for the editor integration.

## Releasing

The package publishes to **public PyPI** via GitHub Actions
(`.github/workflows/publish.yml`), triggered by a `v*` tag. It's pure-Python with no runtime
dependencies, so one `py3-none-any` wheel installs everywhere (Databricks serverless, local
Quarto) with `pip install gt-charts`.

**One-time setup:**

- Configure [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) for this
  repo — project `gt-charts`, owner `Giving-Tuesday`, repo `gt-charts`, workflow
  `publish.yml`, environment `pypi`. Without it, `uv publish` fails at the OIDC step. (Or set
  a `PYPI_API_TOKEN` secret and add `UV_PUBLISH_TOKEN` to the publish step instead.)
- Point `DEFAULT_BUNDLE_URL` in `src/gt_charts/config.py` at the production bundle host so a
  fresh `import` works without setting `GT_CHARTS_BUNDLE_URL`.

**Cut a release:**

```bash
uv run pytest                 # the publish workflow does NOT run tests — check locally first
uv version 0.1.1              # bump pyproject.toml + uv.lock together (or: uv version --bump patch)
git commit -am "chore(gt-charts): bump version to 0.1.1"
git tag v0.1.1 && git push && git push origin v0.1.1   # tag push triggers the publish
```

A version can be uploaded only once — rehearse on
[TestPyPI](https://packaging.python.org/en/latest/guides/using-testpypi/) first if unsure
(`uv publish --publish-url https://test.pypi.org/legacy/`).

**Pre-releases** use [PEP 440](https://peps.python.org/pep-0440/) suffixes — `0.1.0a1`
(alpha), `0.1.0b1` (beta), `0.1.0rc1` (rc). `pip install gt-charts` skips them by default, so
consumers opt in with `--pre` or an exact pin (`gt-charts==0.1.0b1`).

**Consumers:**

- **Databricks serverless** — an admin adds `gt-charts` (pinned, e.g. `gt-charts==0.1.0`) to
  the default serverless base environment; every notebook then just `import gt_charts as gt`,
  no `%pip` cell. Upgrade the org by bumping the pin.
- **Local Quarto** — `pip install gt-charts`; a chart as a cell's last expression renders
  inline in **HTML** output.
