forge-charts 0.3.1

Pure-Rust + SVG interactive charts for Leptos CSR. Project-agnostic public API; no JS, no canvas, no Tailwind.
Documentation

forge-charts

crates.io docs.rs license

Pure-Rust + SVG interactive charts for Leptos (CSR). No JS, no canvas, no Tailwind — just leptos, chrono, and web-sys. The public API is project-agnostic: consumers pass accessor closures over their own data type.

What you get

  • AreaChart — multi-series filled area with axes, legend, hover crosshair, per-series hover dots, and a consumer-rendered tooltip card.
  • Modern visuals out of the box: gradient fills, soft axes, translucent rounded tooltip, light + dark mode via CSS variables.
  • Clickable legend dots — native <input type="color"> lets the user re-theme any series at runtime.
  • Animations on mount (scaleY rise from baseline, staggered per series). Respects prefers-reduced-motion.

Install

[dependencies]
forge-charts = "0.2"
leptos       = { version = "0.8", features = ["csr"] }

Or as a git dep during pre-publish:

[dependencies]
forge-charts = { git = "https://github.com/dandush03/forge" }

You also need the bundled stylesheet served alongside your bundle. With Trunk, add to index.html:

<link data-trunk rel="css" href="vendor/forge-charts/charts.css" />

…where vendor/forge-charts/charts.css is a copy (or symlink) of crates/charts/src/charts.css. Alternatively, the CSS is exposed as a &'static str via the CHART_CSS constant for runtime injection through leptos_meta::Stylesheet:

use leptos_meta::Stylesheet;
use forge_charts::CHART_CSS;

view! { <Stylesheet text=CHART_CSS /> }

Quick start

use leptos::prelude::*;
use forge_charts::{AreaChart, Series};

#[derive(Clone)]
struct Datum { date: String, opened: u32, closed: u32 }

#[component]
fn MyChart(data: Signal<Vec<Datum>>) -> impl IntoView {
    view! {
        <AreaChart
            data=data
            x_label=|d: &Datum| d.date.clone()
            y_values=|d: &Datum| vec![f64::from(d.opened), f64::from(d.closed)]
            series=vec![
                Series::area("Opened", "opened"),
                Series::area("Closed", "closed"),
            ]
            height=320
        />
    }
}

Custom tooltip

The chart hands the hovered data-point index back to a closure you provide; render whatever you want inside the tooltip card.

use std::sync::Arc;
use leptos::prelude::*;
use leptos::tachys::view::any_view::IntoAny;
use forge_charts::{AreaChart, Series};

#[component]
fn MyChartWithTooltip(data: Signal<Vec<Datum>>) -> impl IntoView {
    let tooltip = Arc::new(move |idx: usize| {
        let row = data.with(|d| d.get(idx).cloned());
        let Some(r) = row else { return view! { <div /> }.into_any() };
        view! {
            <div class="charts-tooltip-card">
                <div class="charts-tooltip-date">{ r.date }</div>
                <div class="charts-tooltip-row">
                    <span class="charts-tooltip-dot charts-series-opened"></span>
                    <span class="charts-tooltip-label">"Opened"</span>
                    <span class="charts-tooltip-value">{ r.opened }</span>
                </div>
                <div class="charts-tooltip-row">
                    <span class="charts-tooltip-dot charts-series-closed"></span>
                    <span class="charts-tooltip-label">"Closed"</span>
                    <span class="charts-tooltip-value">{ r.closed }</span>
                </div>
            </div>
        }.into_any()
    });

    view! {
        <AreaChart
            data=data
            x_label=|d: &Datum| d.date.clone()
            y_values=|d: &Datum| vec![f64::from(d.opened), f64::from(d.closed)]
            series=vec![
                Series::area("Opened", "opened"),
                Series::area("Closed", "closed"),
            ]
            tooltip=tooltip
        />
    }
}

API surface

Prop Type Description
data Signal<Vec<T>> Data points. Order = X-axis order; no internal sorting.
x_label Fn(&T) -> String Per-point X-axis label.
y_values Fn(&T) -> Vec<f64> Y values per point, one per declared series (positional).
series Vec<Series> Declares each plotted series + its CSS color hook.
height u32 (default 320) Outer container height in CSS pixels.
legend bool (default true) Show the legend chip strip above the chart.
tooltip Option<TooltipSlot> Arc<dyn Fn(usize) -> AnyView + Send + Sync>. When None, the crosshair still tracks the cursor but no tooltip card draws.
class String Extra classes on the outer .charts-root container.

Series::area(name, color_class) builds an area series. name is the legend + tooltip label. color_class is the CSS-class suffix the chart uses for its color hooks (.charts-series-<color_class>).

Theming

The bundled stylesheet exposes a small set of CSS variables you can override per consumer. Set them on :root, on any parent of .charts-root, or via inline style= on the chart itself.

Variable Default (light) Purpose
--charts-fg rgb(17 24 39) Default text color inside the chart.
--charts-fg-muted rgb(107 114 128) Axis labels.
--charts-fg-faint rgb(156 163 175) Crosshair color.
--charts-grid-color rgba(229, 231, 235, 0.7) Gridline color.
--charts-series-<color_class> (none — set per series) Solid color for series stroke + tooltip dot.
--charts-series-<color_class>-soft (none — set per series) Soft variant (≤ 0.5 alpha) for the gradient fill.

Default palette ships with --charts-series-opened (blue) and --charts-series-closed (green). Add more pairs for every color_class you use:

:root {
  --charts-series-amber: hsl(38 92% 50%);
  --charts-series-amber-soft: hsla(38 92% 50% / 0.45);
}

Dark mode is automatic via @media (prefers-color-scheme: dark).

Runtime color override

Each legend dot is a clickable color picker (<input type="color">) backed by an internal RwSignal<HashMap<String, String>>. Picking a new color writes inline CSS variables on .charts-root so the chart re-themes immediately. Choices are per-instance and not persisted to disk — wire your own persistence by reading the override map from your app's state if needed.

License

Dual-licensed under either of

at your option. Contributions intentionally submitted for inclusion in this crate shall be dual-licensed as above, without any additional terms or conditions.