This release is a pre-release and may not be stable for production use.
shiny-charts
Interactive charts for everyday Shiny for Python dashboards, available as an experimental beta for development and staging pilots. It has its own small semantic API and uses a bundled, modular Apache ECharts runtime. Plotly is not a runtime dependency.
The switching case is native theme integration, fewer repeated layout decisions, consistent interaction events, and a smaller browser bundle. The controlled FinOps comparison (recorded for 0.2.0a1) measured faster cold starts and reactive updates on this machine after profiling; the initial prototype had updates near parity. That comparison showed no websocket-size advantage for full reactive renders; proxy transport is measured separately, and performance depends on the workload. The beta supports common dashboard charts; Plotly's broader feature catalogue remains outside its scope.
Install the beta
python -m pip install "shiny-charts==0.4.0b1"
Requires Python >=3.10, Shiny >=1.5, and htmltools >=0.6. The wheel includes the browser runtime; Node is not required to use it. Pandas is optional.
Pin the exact beta version for reproducible pilots. This is an experimental release, not a general-availability stability promise. Validate required workflows, accessibility, and performance with representative data before production adoption. Supported chart families are line, area, bar, scatter, and histogram. R Shiny and Shinylive are outside this beta's supported scope. See the scope and API notes below.
Run the native FinOps app
uv sync --locked
uv run uvicorn examples.finops_app:app --host 127.0.0.1 --port 8056
Open native FinOps. It runs with Shiny and shiny-charts alone; Plotly is imported only by the separate comparison route. The four cards preserve workload/period filtering, request inspection, brush counts, histogram intervals, light/dark themes, persistent views, and exports. Dense scatterplots explicitly choose Canvas when the request-count setting is at least 10,000; smaller ones use SVG.
See the migration example and beta API contract. The beta API contract applies to 0.4.0b1.
Run the second adoption app
uv run uvicorn examples.operations_app:app --host 127.0.0.1 --port 8057
Open service operations. This synthetic dashboard exercises long queue names, daily gaps, missing response/resolution pairs, a compact area card, queue filtering, selected record identity and a rolling observation window. Append 100 observations, revise measurements, or enable live batches every three seconds. It uses proxies for data updates and full renders for deliberate scope/dataset changes.
See incremental updates. Obtain a controller with
proxy = renderer.proxy(); use await proxy.append(records, max_rows=...) or
await proxy.replace_data(records). Appends send only new rows; the browser still
redraws retained data. Full renders supersede old messages, and the update input
reports application or rejection. Histograms use full renders to recompute bins.
Try the comparison
From this directory:
uv sync
uv run python -m uvicorn examples.app:app --host 127.0.0.1 --port 8055
Open shiny-charts and styled shiny-plotly. Both routes use the same synthetic FinOps data, Shiny controls, chart families, workload colors, request identifiers, success metadata, and filters. Plotly is deliberately styled to fit the dashboard. Try workload bar clicks, request inspection, rectangle selection, a theme change, zooming, hiding a series, and a simulated reactive update. The demo also supports 500, 2,400, 10,000, and 100,000 requests; the cold-start comparison uses 2,400 with SVG in both engines. At larger sizes the native scatter uses Canvas and Plotly still uses SVG, so those dashboard timings do not compare identical renderers.
Install the package in another environment with pip install /path/to/shiny-charts.
The checked-in browser bundle makes Node unnecessary for Python users. To change the
frontend, run npm ci && npm run build first.
Minimal Core app
from shiny import App, ui
from shiny_charts import chart, output_chart, render_chart
app_ui = ui.page_fluid(
ui.input_dark_mode(),
ui.card(
ui.card_header("Monthly revenue"),
output_chart("revenue", height="320px"),
full_screen=True,
),
)
def server(input, output, session):
@render_chart
def revenue():
return (
chart.bar(
{"month": ["Jan", "Feb", "Mar"], "revenue": [12, 18, 24]},
x="month", y="revenue",
)
.format(y="currency:EUR")
.labels(y="Revenue")
)
app = App(app_ui, server)
Outputs without an explicit height fill their Shiny card; fixed-height outputs work
outside fillable layouts. Core modules resolve output IDs through Shiny's namespace.
In Express, use @render_chart directly inside with ui.card():; an output is
created automatically. See examples/express_app.py, runnable
with uv run shiny run examples/express_app.py.
Chart API
Builders accept a pandas DataFrame, an iterable of record dictionaries, or a mapping of column names to equally sized lists. Pandas is optional. Only the encoded fields, keys, requested tooltip fields, and band bounds are sent to the browser.
| Builder | Encodings / options |
|---|---|
chart.line(data, x=, y=) |
color, key, tooltip, x_type |
chart.area(data, x=, y=) |
color, key, tooltip, stacked, x_type |
chart.bar(data, x=, y=) |
color, key, tooltip, stacked, orientation="horizontal"; categorical x |
chart.scatter(data, x=, y=) |
color, key, tooltip; numeric x/y |
chart.histogram(data, x=) |
bins=20, optional range=(min, max); server binning |
Use color="workload" for grouped series. Category-to-color hashing keeps assignments
stable when groups are filtered or reordered. Hash collisions are possible; use
.palette({"Email": 1, "Chat": 2}) for distinct named roles in the host palette. Numeric and missing y values are accepted;
missing observations produce line/band gaps. Date and datetime x columns infer a time
axis; string dates need x_type="time". Datetimes normalize to UTC, naive datetimes
are treated as UTC, and axis dates display in UTC. Explicit time strings should use
ISO 8601 with an offset. Numeric x values infer a value axis for lines and areas.
The fluent operations return a new chart:
plot = (
chart.line(daily, x="day", y="cost", color="workload")
.format(y="currency:USD:3")
.labels(x="Day", y="Cost / request", title="Daily request cost")
.band(lower="low", upper="high", label="95% interval")
.rule(y=0.045, label="Budget")
.view("cost-v1")
)
| Operation | Behavior |
|---|---|
.format(x=, y=) |
number[:digits], percent[:digits], currency:USD[:digits]; 0–6 digits |
.palette({"Email": 1, "Chat": 2}) |
Named groups use one-based inherited palette slots 1–6; others hash |
.labels(x=, y=, title=) |
Axis labels and accessible chart/table description |
.band(lower=, upper=, label=) |
Line/area interval using source columns, including intervals crossing zero |
.rule(y=, label=) |
Horizontal reference line |
.view(revision="default", zoom=True) |
Preserve zoom/hidden series while revision stays the same; change it to reset |
.select(brush=False) |
Enable point/category/bin clicks; rectangle brush is scatter-only |
.renderer("svg") |
SVG default; opt into "canvas" explicitly |
Percent formats expect fractions (0.25 displays as 25%). Bar/histogram axes include zero; lines and scatterplots use data extents. Zoom uses Ctrl + mouse wheel so a dashboard remains scrollable. SVG export follows the SVG renderer; Canvas exports PNG. Every chart includes CSV export and a keyboard-accessible table preview of its first 250 displayed records. CSV includes all displayed records. Histogram CSV contains bins, not the unaggregated requests.
Shiny interaction
from shiny import reactive
# In server():
@reactive.effect
@reactive.event(input.requests_selection)
def selected():
event = input.requests_selection()
if event["kind"] == "records":
selected_request.set(event["keys"][0])
For output_chart("requests"), available inputs are requests_click,
requests_selection, requests_view, and requests_legend. Point clicks also emit
a selection. All payloads carry the chart's view revision.
| Selection kind | Payload |
|---|---|
records |
keys, x, y, group, original displayed row index |
category |
x, y, group, row, empty keys |
bin |
range: {min, max, upper_inclusive}, aggregated count |
range |
range: {x: [min,max], y: [min,max]}, count, keys, truncated |
clear |
Empty keys |
Pass a non-null unique key column for record identity across filtering. Brush counts
include only visible series and non-missing points. At most 10,000 record keys are sent;
count remains exact and truncated reports the limit. Bins include their lower edge
and exclude their upper edge, except the final bin includes the upper edge. View events
report x-axis bounds or {reset: true}; legend events report hidden group names.
Theme integration
The chart reads computed styles from its actual output element. It inherits the font, Bootstrap body/secondary text, background, and border colors; transparent chart surfaces fit their containing card. Ancestor theme/class/style changes redraw in the browser. Dark mode and themes scoped to individual cards are supported.
Optional overrides live on any ancestor:
.my-dashboard {
--chart-font: Georgia, serif;
--chart-fg: #243044;
--chart-muted: #596779;
--chart-grid: #dce4ed;
--chart-border: #dce4ed;
--chart-surface: #ffffff;
--chart-color-1: #087eac;
/* --chart-color-2 through --chart-color-6 */
}
The library does not inject the demo's Manrope font or app palette. Those belong to
the host dashboard. [data-bs-theme] selects theme mode; system preference supplies
the fallback when there is no explicit theme.
Migrating from shiny-plotly
Replace output_plotly/render_plotly with output_chart/render_chart, then rebuild
the figure using the semantic builders. Replace customdata IDs and nested Plotly click
payloads with key= and the normalized event inputs. Put dashboard theme values in CSS,
and use .view() where you previously used uirevision.
There is no automatic conversion of go.Figure or Plotly Express figures and no
Plotly compatibility layer. Start with one ordinary chart card and retain shiny-plotly
for specialized charts. ECharts options are kept inside the frontend adapter; an
arbitrary engine-options escape hatch is not part of this prototype's public API.
Verification and evidence
uv sync
npm ci
npm run build
npm test
uv run pytest -q
uv run ruff check src examples tests bench scripts
uv build
uv run python scripts/check_wheel.py
uv run python scripts/check_floor.py --python 3.10
Browser tests need Chromium (uv run playwright install chromium). Tests cover real
Shiny Core/Express rendering, scoped themes, modules, click/brush identity, persistent
views, table selection, CSV/image export, resizing, empty/error/None recovery, mobile
overflow, and automated accessibility checks. Automated checks are not a claim of full
screen-reader chart accessibility.
With the comparison server running, reproduce the local measurement:
uv run python bench/run.py --rounds 5
uv run python bench/write_report.py
The generated decision report records medians, environment,
methodology, code-size comparison, and limits. Raw samples are in bench/results.json.
These reports and review screenshots are local working material and excluded from commits.
Profile the large scatter path independently:
uv run python bench/profile_large.py
uv run python bench/write_profile_report.py
This profile separates Python builder/packing/JSON costs from actual SVG/Canvas completion for 10,000 and 100,000 points. It checks painted point counts and reports post-GC heap/DOM size. It is separate from the four-card cold-start benchmark. See the generated large-data report. Full updates still send complete chart specs; a smaller renderer bundle does not imply smaller event traffic or instant updates with large data.
Compare full versus proxy updates independently:
uv run python bench/updates.py
The generated incremental transport report measures received websocket bytes and completed Canvas updates for identical rolling keyed scatters. This compares two shiny-charts update paths, separately from Plotly timings.
Scope and next investment
Implemented: line, area, grouped/stacked bars, scatter, histogram, interval bands, horizontal rules, SVG/Canvas, linked filtering, theme inheritance, and exports. Missing: 3D, maps, facets, arbitrary mixed axes/layers, lasso, server resampling, point-level engine updates, automatic figure migration, and mature localization. R and Shinylive have not been validated. Normal reactive renders send complete specs; proxy appends send only new records while retaining the rendered definition. Long category labels and extreme numeric scales need broader layout testing.
The beta includes native FinOps and service-operations adoption apps, an explicit API/event contract, ordered data proxies, migration examples, dependency-floor and browser checks, and compatibility CI. The next external validation is a dashboard with actual user data and manual accessibility testing before production adoption. A custom Plotly partial bundle could narrow the cold-start advantage. See CONTRIBUTING.md for local checks.
MIT package; bundled ECharts/zrender retain their licenses and notices. See THIRD_PARTY.md.
Adopt in Portfolio pulse
make portfolio
Open Portfolio pulse. This local migration of an existing
Python Shiny app retains its financial calculations, four filters and registered
URL bookmarks. Its data is illustrative. The deployed original remains unchanged.
The adoption development group installs shinyhub-bookmarks==0.5.2, which needs
Shiny >=1.8; the chart library still supports Shiny >=1.5 and does not depend on
that SDK. See the adoption notes.
Bar and area builders now accept tooltip=["field"], like line and scatter.
Data tables use the encoded axis formats; CSV keeps raw values. Custom hosts can
set --chart-hover and --chart-focus alongside the existing chart color tokens.
make profile-dashboard
This measures completed warm updates in Portfolio pulse and the four-card service
operations app at desktop/mobile widths. It is a local workload measurement,
not a Plotly comparison or a production throughput guarantee. Generated
bench/report-dashboard.md and bench/results-dashboard.json stay local.
Metadata
Release files for shiny-charts 0.4.0b1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| shiny_charts-0.4.0b1.tar.gz | 555.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shiny_charts-0.4.0b1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 805.3 kB
Release files / shiny_charts-0.4.0b1.tar.gz
| Download URL | shiny_charts-0.4.0b1.tar.gz |
|---|---|
| Size | 555.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b094d07f5a63945449e4ae19d6c0954859e75f787486a7a8673dacf779e09b80
|
|
BLAKE2b-256 checksum How to use checksums |
fca87d9f55cfbf25729143deaf0a5ad488313ac5d8fd3bce11c0bef58435378d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / shiny_charts-0.4.0b1-py3-none-any.whl
| Download URL | shiny_charts-0.4.0b1-py3-none-any.whl |
|---|---|
| Size | 249.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
14aa27daca3151532f3d6e37fe4d330aa651572080cab1a73a3fb9de652f9bd3
|
|
BLAKE2b-256 checksum How to use checksums |
1c6243f0166a5d165a837c30fe1af7d7da35a18058a3b074dab369ea36d5e15c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|