Chart

A comprehensive charting library providing Line, Bar, Area, Pie, Radar, Candlestick, and Sankey charts for data visualization. The charts feature smooth animations, customizable styling, tooltips, legends, and automatic theming that adapts to your application’s theme.

Install

#Import

use gpui_kit::component::chart::{
    LineChart, BarChart, AreaChart, PieChart, RadarChart, CandlestickChart, SankeyChart,
};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { LineInterpolation, BarAlign, ChartPoint, SeriesPoint, StackPoint, RadarPoint, CandlePoint, SankeyNode, SankeyLink, ChartSwatch, ChartLegend, ChartFormat, LineChart, AreaChart, BarChart, PieChart, RadarChart, CandlestickChart, SankeyChart } from "chart.slint";
use crate::components::chart::{
use crate::components::group_box::{self, GroupBox};
use crate::components::theme::{self, Tokens};
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment as Axis, Element, Length, Task, Theme};

#Usage

Each sample under Chart Types is a chart the gallery is already running. The heading is the chart type and the subtitle on that card. The code is the source of that card, so the sample is the example on screen.

#Chart Types

#LineChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A line joins its points in the order given. `Linear` is the default and
// `Step` holds each value until the next point, Slint's `step after`. The
// tone is one of the theme's five chart colours, resolved when iced draws
// so the series moves with the theme rather than being frozen at build time.
let lines = column![
    LineChart::new(months()).dots(true).tone(1),
    LineChart::new(conversion())
        .interpolation(Interpolation::Step)
        .tone(2),
]
.spacing(theme::SPACING_MD);

A line chart displays data points connected by straight line segments, perfect for showing trends over time.

#LineChart — Curved

LineChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.mrr)
    .stroke(accent)
    .name("MRR")
    .y_axis(true)
    .y_tick_format(money)
    .x_tick_count(4)
    .tooltip_value(|_, value| money(value).into())
    .id("line-chart"),
LineChart {
    points: months;
    stroke: Theme.chart-2;
}

#LineChart — Linear

LineChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.conversion)
    .stroke(deep)
    .linear()
    .name("Conversion %")
    .id("line-chart-linear"),
LineChart {
    points: conversion;
    interpolation: LineInterpolation.linear;
    stroke: Theme.chart-2;
}

#LineChart — Step after

LineChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.subscriptions)
    .stroke(accent)
    .step_after()
    .name("Subscriptions")
    .id("line-chart-step-after"),
LineChart {
    points: months;
    interpolation: LineInterpolation.step;
    stroke: Theme.chart-3;
}

#LineChart — Dots

LineChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.deploys)
    .dot()
    .stroke(cx.theme().chart_5)
    .name("Deploys")
    .id("line-chart-dots"),
LineChart {
    points: deploys;
    dots: true;
    stroke: Theme.chart-2;
}

#Tick Control

LineChart also takes y_domain and point_count; see Pinned Axis and Unfinished Series under AreaChart.

#Axes and Guides

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The value axis draws `y_ticks` labelled grid lines, both ends included,
// in a gutter that widens to fit the widest label. The x axis labels every
// `tick_margin`-th point, `max_value` pins the top for a series that never
// nears zero, and `grid(false)` leaves the plot bare.
let axes = row![
    LineChart::new(months()).tone(1).y_ticks(5),
    LineChart::new(intraday())
        .tone(2)
        .y_ticks(3)
        .max_value(130.0)
        .grid(false),
]
.spacing(theme::SPACING_LG);

LineChart and AreaChart share these. y_axis shows tick labels at the y ticks, in a gutter left of the plot by default, which widens to fit the widest label, or over the plot with y_axis_label_placement(AxisLabelPlacement::Inside). y_tick_count sets how many ticks there are, evenly spaced from the baseline to the top edge with both ends included; they place the horizontal grid lines too, and each label reads the value the scale puts at its height. The default of 5 is the grid the charts have always drawn. y_tick_format writes the label text from that value.

x_tick_count labels only that many x values, spread evenly from the first to the last, instead of every tick_margin-th; with point_count set they spread over every point the axis is laid out for, so they stay put as the data grows. grid_columns adds vertical grid lines, grid_dashed(false) draws the grid solid, reference_line marks a value with a dashed line across the plot, drawn darker than the grid, and y_padding sets the space kept above the highest value and below the lowest, 10px and 0 by default.

#BarChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Bars grow out of the zero line, so a negative value extends below it
// rather than being clamped. `mixed` takes one tone per bar, `rounded`
// rounds the corners, and `show_labels` writes each value over its bar. The
// legend names the tones under the plot.
let bars = column![
    BarChart::new(regions())
        .mixed(true)
        .rounded(true)
        .show_labels(true),
    ChartLegend::new().items(["N. America", "Europe", "APAC", "LatAm"]),
    BarChart::new(cash()).rounded(true).tone(1),
]
.spacing(theme::SPACING_SM);

A bar chart uses rectangular bars to show comparisons among categories. Bars can be oriented vertically or horizontally via the alignment option.

#BarChart — Rounded

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.revenue)
    .name("Revenue")
    .fill(move |_, _, _, _| accent)
    .corner_radii(rounded_tip())
    .value_axis(true)
    .value_tick_count(3)
    .value_tick_format(money)
    .grid_dashed(false)
    .band_tick_count(6)
    .id("bar-chart"),
BarChart {
    points: months;
    rounded: true;
    fill: Theme.chart-2;
}

#BarChart — Mixed shades

BarChart::new(data.regions.clone())
    .band(|d| d.region.clone())
    .value(|d| d.revenue)
    .name("Revenue")
    .label(|d| money(d.revenue))
    .fill(move |d, _, _, _| shade(mid, color_index(&d.region)))
    .corner_radii(rounded_tip())
    .padding_inner(0.6)
    .padding_outer(0.1)
    .id("bar-chart-mixed"),
BarChart {
    points: regions;
    mixed: true;
    rounded: true;
    show-labels: true;
}
ChartLegend {
    points: regions;
}

#BarChart — Stacked

StackedBarChart::new(days)
BarChart {
    stacks: devices;
    stacked: true;
    series-a: "Desktop";
    series-b: "Mobile";
    series-c: "Tablet";
    series-d: "Watch";
}
ChartLegend {
    points: [
        { label: "Desktop", value: 0 },
        { label: "Mobile", value: 0 },
        { label: "Tablet", value: 0 },
        { label: "Watch", value: 0 }
    ];
}

#BarChart — Fully rounded

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.signups)
    .name("Signups")
    .label(|d| compact(d.signups))
    .fill(move |_, _, _, _| accent)
    .corner_radii(px(8.))
    .id("bar-chart-rounded"),
BarChart {
    points: signups;
    rounded: true;
    show-labels: true;
    fill: Theme.chart-2;
}

#BarChart — Bottom aligned

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.orders)
    .name("Orders")
    .label(|d| compact(d.orders))
    .fill(move |_, _, _, _| accent)
    .id("bar-chart-bottom"),
BarChart {
    points: orders;
    alignment: BarAlign.bottom;
    show-labels: true;
}

#BarChart — Top aligned

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.refunds)
    .name("Refunds")
    .label(|d| compact(d.refunds))
    .fill(move |_, _, _, _| mid)
    .alignment(BarAlignment::Top)
    .id("bar-chart-top"),
BarChart {
    points: refunds;
    alignment: BarAlign.top;
    show-labels: true;
    fill: Theme.chart-3;
}

#BarChart — Left aligned

BarChart::new(data.products.clone())
    .band(|d| d.product.clone())
    .value(|d| d.sales)
    .name("Units")
    .label(|d| compact(d.sales))
    .fill(move |_, _, _, _| accent)
    .alignment(BarAlignment::Left)
    .id("bar-chart-left"),
BarChart {
    points: products;
    alignment: BarAlign.left;
    show-labels: true;
}

#BarChart — Right aligned

BarChart::new(data.pages.clone())
    .band(|d| d.page.clone())
    .value(|d| d.views)
    .name("Views")
    .label(|d| compact(d.views))
    .fill(move |_, _, _, _| deep)
    .alignment(BarAlignment::Right)
    .id("bar-chart-right"),
BarChart {
    points: pages;
    alignment: BarAlign.right;
    show-labels: true;
    fill: Theme.chart-4;
}

#BarChart — Negative

BarChart::new(data.cash_flow.clone())
    .band(|d| d.month.clone())
    .value(|d| d.revenue)
    .name("Net")
    .label(|d| money(d.revenue))
    .fill(
        move |d, _, _, _| {
            if d.revenue >= 0. { positive } else { negative }
        },
    )
    .label_color(move |d| if d.revenue >= 0. { positive } else { negative })
    .value_axis(true)
    .tooltip_title(|d| format!("{} 2025", d.month).into())
    .tooltip_value(|_, value| money(value).into())
    .tooltip_value_color(
        move |_, value| {
            if value >= 0. { positive } else { negative }
        },
    )
    .id("bar-chart-negative"),
BarChart {
    points: cash;
}
HorizontalLayout {
    spacing: 12px;
    Rectangle {
        y: 4px;
        width: 8px;
        height: 8px;
        border-radius: 2px;
        background: Theme.success;
    }
    Text {
        text: "Surplus";
        color: Theme.muted-foreground;
        font-size: Theme.font-size-xs;
    }
    Rectangle {
        y: 4px;
        width: 8px;
        height: 8px;
        border-radius: 2px;
        background: Theme.danger;
    }
    Text {
        text: "Deficit";
        color: Theme.muted-foreground;
        font-size: Theme.font-size-xs;
    }
}

#BarChart — Gradient

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.downloads)
    .name("Downloads")
    .label(|d| compact(d.downloads))
    .fill(move |_, _, _, alignment| bar_shading(accent, alignment))
    .band_tick_count(4)
    .id("bar-chart-gradient-bottom"),
BarChart {
    points: downloads;
    gradient: true;
    show-labels: true;
}

#BarChart — Gradient, top aligned

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.refunds)
    .name("Refunds")
    .label(|d| compact(d.refunds))
    .alignment(BarAlignment::Top)
    .fill(move |_, _, _, alignment| bar_shading(deep, alignment))
    .id("bar-chart-gradient-top"),
BarChart {
    points: refunds;
    alignment: BarAlign.top;
    gradient: true;
    fill: Theme.chart-4;
    show-labels: true;
}

#BarChart — Gradient, horizontal

BarChart::new(data.pages.clone())
    .band(|d| d.page.clone())
    .value(|d| d.views)
    .name("Views")
    .label(|d| compact(d.views))
    .alignment(BarAlignment::Left)
    .fill(move |_, _, _, alignment| bar_shading(accent, alignment))
    .id("bar-chart-gradient-left"),
BarChart {
    points: pages;
    alignment: BarAlign.left;
    gradient: true;
    show-labels: true;
}

#BarChart — Gradient, right aligned

BarChart::new(data.products.clone())
    .band(|d| d.product.clone())
    .value(|d| d.sales)
    .name("Units")
    .label(|d| compact(d.sales))
    .alignment(BarAlignment::Right)
    .fill(move |_, _, _, alignment| bar_shading(deep, alignment))
    .id("bar-chart-gradient-right"),
BarChart {
    points: products;
    alignment: BarAlign.right;
    gradient: true;
    fill: Theme.chart-4;
    show-labels: true;
}

#BarChart — Gradient, rounded

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.sessions)
    .name("Sessions")
    .label(|d| compact(d.sessions))
    .fill(move |_, _, _, alignment| bar_shading(mid, alignment))
    .corner_radii(px(6.))
    .id("bar-chart-gradient-per-bar"),
BarChart {
    points: sessions;
    rounded: true;
    gradient: true;
    fill: Theme.chart-3;
    show-labels: true;
}

#BarChart — Diagonal gradient

BarChart::new(data.metrics.clone())
    .band(|d| d.month.clone())
    .value(|d| d.orders)
    .name("Orders")
    .label(|d| compact(d.orders))
    .fill(move |_, bar, chart, _| {
        // Project the bar's corners onto the chart's
        // bottom-left → top-right diagonal so each bar
        // shows the slice of a chart-wide diagonal
        // gradient corresponding to its own footprint.
        let w = chart.size.width.max(f32::EPSILON);
        let h = chart.size.height.max(f32::EPSILON);
        let denom = w * w + h * h;
        let project =
            |x: f32, y: f32| -> f32 { (x * w + (h - y) * h) / denom };
        let lo = project(bar.origin.x, bar.origin.y + bar.size.height);
        let hi = project(bar.origin.x + bar.size.width, bar.origin.y);
        let lerp = |t: f32| Hsla {
            h: c1.h + (c2.h - c1.h) * t,
            s: c1.s + (c2.s - c1.s) * t,
            l: c1.l + (c2.l - c1.l) * t,
            a: c1.a + (c2.a - c1.a) * t,
        };
        linear_gradient(
            45.,
            linear_color_stop(lerp(lo), 0.),
            linear_color_stop(lerp(hi), 1.),
        )
    })
    .id("bar-chart-gradient-diagonal"),
BarChart {
    points: orders;
    gradient: true;
    show-labels: true;
    fill: Theme.chart-1;
}

#Bar Chart Gradient Fills

For gradient fills aligned to the bar’s orientation, use fill_gradient. The closure receives the datum, the chart’s full data range, and a chart_to_bar helper that maps a chart-value coordinate to a bar-local gradient position (0.0 is the bar’s base, 1.0 is its tip). The gradient angle is derived from the bar’s BarAlignment so stop-0 sits at the base and stop-1 at the tip.

fill and fill_gradient are mutually exclusive — setting one clears the other.

#Bar Chart Alignment

BarAlignment controls the bar orientation and the side where the baseline sits. Import it from gpui_kit::component::plot::shape.

#Bar Chart Corner Radii

Round the bar rectangles. Pass any value convertible into Corners<Pixels> — use a single px(..) for uniform rounding, or construct Corners manually to round only specific corners (e.g. just the tip end of each bar).

#Bar Chart Negative Values

Bars grow from zero rather than from the edge of the plot, so negative values extend to the opposite side of the zero line. The band-axis line follows zero, and each category label moves to whichever side its own bar leaves empty. No configuration is needed — a data set containing negative values renders this way.

#Bar Chart Value Axis

Show tick labels for the value scale with value_axis, and set how many ticks it carries with value_tick_count. The ticks are evenly spaced from the baseline to the far edge with both ends included, and drive both the grid lines and the tick labels, so the two always agree. tick_margin, by contrast, is a stride over the band-axis categories: tick_margin(2) keeps every second category label.

value_axis_label_placement(AxisLabelPlacement::Inside) draws the labels over the plot beside their grid lines, so the bars keep the room a gutter would take, and value_tick_format writes their text. band_count lays the band axis out for more bands than there is data, so a short series keeps each bar’s width and fills only the leading bands. band_tick_count labels only that many bands, spread evenly from the first to the last (over every band when band_count is set), and grid_dashed(false) draws the grid solid.

#Bar Chart Labels and Spacing

label_color colors each bar’s label text, so a count can take its bar’s color instead of the foreground. padding_inner and padding_outer set the gap between bars and before the first and after the last, as shares of a band; they default to 0.4 and 0.2. min_length draws every bar at least that many pixels long, so an empty bucket still shows a stub on the baseline.

A stub grows the way its bar’s value would: away from the zero line, to the negative side for a negative value and to the positive side for zero. Vertical bars with a label keep a line of text clear above the tallest bar, so its label stays inside the chart.

#AreaChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// An area fills the region between its line and the baseline at 35% of the
// series tone. It takes the same joins and the same pinned axis as a line,
// so a series still in progress holds room for the points still to come.
let areas = column![
    AreaChart::new(visitors()).tone(1),
    AreaChart::new(intraday())
        .interpolation(Interpolation::Step)
        .tone(3)
        .max_value(130.0),
]
.spacing(theme::SPACING_MD);

An area chart displays quantitative data visually, similar to a line chart but with the area below the line filled.

#AreaChart — Stacked

AreaChart::new(data.daily_devices.clone())
    .x(|d| d.date.clone())
    .y(|d| d.desktop)
    .stroke(accent)
    .fill(area_gradient(accent))
    .name("Desktop")
    .y(|d| d.mobile)
    .stroke(deep)
    .fill(area_gradient(deep))
    .name("Mobile")
    .tick_margin(8)
    .id("area-chart-stacked"),
AreaChart {
    series: visitors;
    stacked: true;
    series-a: "Desktop";
    series-b: "Mobile";
    fill: Theme.chart-1;
    fill-b: Theme.chart-2;
    tick-margin: 2;
}
ChartLegend {
    points: [
        { label: "Desktop", value: 0 },
        { label: "Mobile", value: 0 }
    ];
}

#AreaChart — Flat fill

AreaChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.active_users)
    .stroke(accent)
    .fill(accent.opacity(0.3))
    .name("Active users")
    .id("area-chart"),
AreaChart {
    points: months;
    fill: Theme.chart-2;
}

#AreaChart — Linear

AreaChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.sessions)
    .stroke(deep)
    .fill(deep.opacity(0.3))
    .linear()
    .name("Sessions")
    .id("area-chart-linear"),
AreaChart {
    points: sessions;
    interpolation: LineInterpolation.linear;
    fill: Theme.chart-2;
}

#AreaChart — Step after

GPUINo GPUI version of this example yet.
AreaChart {
    points: storage;
    interpolation: LineInterpolation.step;
    fill: Theme.chart-3;
}

#AreaChart — Gradient

AreaChart::new(data.metrics.clone())
    .x(|d| d.month.clone())
    .y(|d| d.last_year)
    .stroke(cx.theme().chart_1)
    .fill(area_gradient(cx.theme().chart_1))
    .name("2024")
    .y(|d| d.revenue)
    .stroke(accent)
    .fill(area_gradient(accent))
    .name("2025")
    .tooltip_content(|d, _, cx| {
        let change = change_percent(d.revenue, d.last_year);
        let change_color = if change >= 0. {
            cx.theme().chart_bullish
        } else {
            cx.theme().chart_bearish
        };
        let row = |label: &'static str, value: String| {
            h_flex()
                .justify_between()
                .gap_4()
                .child(
                    div().text_color(cx.theme().muted_foreground).child(label),
                )
                .child(value)
        };
        v_flex()
            .gap_1()
            .child(div().font_semibold().child(d.month.clone()))
            .child(row("2025", money(d.revenue)))
            .child(row("2024", money(d.last_year)))
            .child(
                row("Change", format!("{change:+.1}%"))
                    .text_color(change_color),
            )
    })
    .id("area-chart-gradient"),
AreaChart {
    series: year-over-year;
    stacked: true;
    series-a: "2024";
    series-b: "2025";
    fill: Theme.chart-1;
    fill-b: Theme.chart-2;
    tick-margin: 2;
}
ChartLegend {
    points: [
        { label: "2024", value: 0 },
        { label: "2025", value: 0 }
    ];
}

#AreaChart — In progress

AreaChart::new(minutes)
    .x(|d| d.time.clone())
    .y(|d| d.price)
    .stroke(accent)
    .fill(area_gradient(accent))
    .linear()
    .y_domain(low - (high - low) / 4., high)
    .point_count(total)
    .x_tick_count(4)
    .name("Price")
    .id("area-chart-in-progress"),
AreaChart {
    points: intraday;
    interpolation: LineInterpolation.linear;
    fill: Theme.chart-2;
    max-value: 130;
}

#Pinned Axis and Unfinished Series

By default the y axis fits the data from zero. y_domain pins it to a range instead, so a price or a balance that never nears zero is not pressed flat against the top. point_count lays the x axis out for more points than the data has, so a series still in progress, such as today’s intraday prices, fills only the leading part. LineChart takes both as well.

A pinned range keeps the 10px of headroom the default leaves above the highest value, and the series are clipped to the plot, so a value outside the range stops at its edge. Nothing is drawn when min equals max, so widen a flat series before passing it in. A natural curve can swing past its highest and lowest points; prefer linear when the range is fitted tightly to the data.

The i-th item of data sits on the i-th point, so the data has to be contiguous from the first point: a missing item shifts every later one a point to the left.

#PieChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A pie sweeps clockwise from the top, one palette tone per slice. `donut`
// leaves a hole, `pad_angle` opens a gap between slices, and `show_labels`
// names each slice at the ring's midpoint.
let pies = row![
    PieChart::new(traffic()).donut(true).pad_angle(0.02),
    PieChart::new(regions())
        .show_labels(true)
        .outer_radius(38.0),
]
.spacing(theme::SPACING_LG);

A pie chart displays data as slices of a circular chart, ideal for showing proportions.

#PieChart — Basic

PieChart::new(data.traffic_sources.clone())
    .value(|d| d.visitors as f32)
    .outer_radius(90.)
    .color(move |d| shade(mid, color_index(&d.source)))
    .name("Visitors")
    .id("pie-chart"),
PieChart {
    points: traffic;
}
ChartLegend {
    points: traffic;
}

#PieChart — Donut

div()
    .relative()
    .size_full()
    .child(
        PieChart::new(data.browsers.clone())
            .value(|d| d.share as f32)
            .inner_radius(58.)
            .outer_radius(90.)
            .color(move |d| shade(mid, color_index(&d.browser)))
            .name("Share")
            .id("pie-chart-donut"),
    )
    // The headline figure sits in the hole of the ring.
    .child(
        v_flex()
            .absolute()
            .inset_0()
            .items_center()
            .justify_center()
            .child(
                div()
                    .text_2xl()
                    .font_semibold()
                    .child(format!("{:.0}%", leader.share)),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(cx.theme().muted_foreground)
                    .child(leader.browser.clone()),
            ),
    ),
PieChart {
    points: browsers;
    donut: true;
}
ChartLegend {
    points: browsers;
}

#PieChart — Gap

PieChart::new(data.plans.clone())
    .value(|d| d.accounts as f32)
    .inner_radius(56.)
    .outer_radius(90.)
    .pad_angle(4. / 100.)
    .color(move |d| shade(mid, color_index(&d.plan)))
    .name("Accounts")
    .id("pie-chart-pad-angle"),
PieChart {
    points: plans;
    donut: true;
    pad-angle: 0.02;
}
ChartLegend {
    points: plans;
}

#PieChart — Labels

PieChart::new(data.regions.clone())
    .value(|d| d.revenue as f32)
    .inner_radius(48.)
    .outer_radius(76.)
    .color(move |d| shade(mid, color_index(&d.region)))
    .label(|d| d.region.clone())
    .tooltip_name(|d| d.region.clone())
    .name("Revenue")
    .id("pie-chart-label"),
PieChart {
    points: regions;
    donut: true;
    show-labels: true;
    inner-radius: 22;
    outer-radius: 38;
}

#RadarChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A radar closes a polygon around one vertex per dimension. `multiple`
// draws the second series, `dots` marks the first series' vertices, and
// `lines_only` drops the fill so only the ring grid and the outline remain.
let radars = row![
    RadarChart::new(scores()).multiple(true).dots(true),
    RadarChart::new(scores())
        .lines_only(true)
        .grid_levels(5)
        .tone(3),
]
.spacing(theme::SPACING_LG);

A radar chart displays multivariate data as closed polygons around a center, ideal for comparing multiple series across several dimensions.

#RadarChart — Basic

RadarChart::new(data.product_scores.clone())
    .label(|d| d.dimension.clone())
    .value(|d| d.alpha)
    .stroke(accent)
    .fill(accent.opacity(0.3))
    .name("Alpha")
    .max_value(100.)
    .tooltip_value(|_, _, value| format!("{value:.0} / 100").into())
    .id("radar-chart"),
RadarChart {
    points: scores;
}

#RadarChart — Two series

RadarChart::new(data.product_scores.clone())
    .label(|d| d.dimension.clone())
    .value(|d| d.alpha)
    .stroke(accent)
    .fill(accent.opacity(0.25))
    .name("Alpha")
    .value(|d| d.beta)
    .stroke(deep)
    .fill(deep.opacity(0.25))
    .name("Beta")
    .max_value(100.)
    .id("radar-chart-multiple"),
RadarChart {
    points: scores;
    multiple: true;
}
ChartLegend {
    points: [
        { label: "Alpha", value: 0 },
        { label: "Beta", value: 0 }
    ];
}

#RadarChart — Dots

RadarChart::new(data.product_scores.clone())
    // An element label: the dimension name over a grade badge.
    .label({
        let muted_foreground = cx.theme().muted_foreground;
        let badge_radius = cx.theme().radius_full();

        move |d: &ProductScore| {
            let grade = match d.alpha {
                v if v >= 85. => "A",
                v if v >= 70. => "B",
                _ => "C",
            };

            v_flex()
                .items_center()
                .gap_1()
                .child(
                    div()
                        .text_xs()
                        .text_color(muted_foreground)
                        .child(d.dimension.clone()),
                )
                .child(
                    h_flex()
                        .justify_center()
                        .size_6()
                        .rounded(badge_radius)
                        .bg(accent.opacity(0.1))
                        .text_sm()
                        .font_weight(FontWeight::SEMIBOLD)
                        .text_color(accent)
                        .child(grade),
                )
                .into_any_element()
        }
    })
    .value(|d| d.alpha)
    .name("Alpha")
    .stroke(accent)
    .fill(accent.opacity(0.25))
    .max_value(100.)
    .dot()
    // A badge label is far taller than a line of text, so
    // pull the ring in to leave it room.
    .outer_radius(64.)
    .id("radar-chart-dots"),
RadarChart {
    points: scores;
    dots: true;
}

#RadarChart — Outline

RadarChart::new(data.product_scores.clone())
    .label(|d| d.dimension.clone())
    .value(|d| d.beta)
    .name("Beta")
    .stroke(deep)
    .fill(gpui_kit::transparent_black())
    .max_value(100.)
    .grid_levels(5)
    .id("radar-chart-lines-only"),
RadarChart {
    points: scores;
    lines-only: true;
    grid-levels: 5;
    stroke: Theme.chart-4;
}

#Element Labels

label accepts either a string or a custom element. Return element.into_any_element() to render anything you like around the outer ring — an icon, several lines, per-dimension colors.

Each label is measured at its natural size and pushed radially outward from its dimension, so even a tall one clears the outer ring. Element labels style themselves, so .label_color() does not apply to them, and they supply no tooltip title (a string label does).

The ring is not shrunk to make room: the default outer radius is 40% of the chart’s height, so a label much taller than a line of text needs a smaller .outer_radius() to keep it inside the chart’s bounds.

#CandlestickChart

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Each candle is a wick from the session's low to its high and a body from
// its open to its close, coloured by whether the close beat the open, over
// a price range the caller can pin with `min_price` and `max_price`.
let candle_chart = CandlestickChart::new(candles())
    .min_price(90.0)
    .max_price(130.0)
    .tick_margin(2);

A candlestick chart displays financial data using OHLC (Open, High, Low, Close) values, perfect for visualizing stock prices and market trends.

#CandlestickChart — Daily

CandlestickChart::new(data.stock_prices.clone())
    .x(|d| d.date.clone())
    .open(|d| d.open)
    .high(|d| d.high)
    .low(|d| d.low)
    .close(|d| d.close)
    .body_width_ratio(body_width_ratio)
    .tick_margin(tick_margin)
    .tooltip_value(|_, _, value| format!("${value:.2}").into())
    .id(id),
CandlestickChart {
    points: candles;
    min-price: 90;
    max-price: 130;
    tick-margin: 2;
}

#CandlestickChart — Narrow bodies

CandlestickChart::new(data.stock_prices.clone())
    .x(|d| d.date.clone())
    .open(|d| d.open)
    .high(|d| d.high)
    .low(|d| d.low)
    .close(|d| d.close)
    .body_width_ratio(body_width_ratio)
    .tick_margin(tick_margin)
    .tooltip_value(|_, _, value| format!("${value:.2}").into())
    .id(id),
CandlestickChart {
    points: candles;
    body-width: 0.5;
    min-price: 90;
    max-price: 130;
    tick-margin: 2;
}

#CandlestickChart — Wide bodies

CandlestickChart::new(data.stock_prices.clone())
    .x(|d| d.date.clone())
    .open(|d| d.open)
    .high(|d| d.high)
    .low(|d| d.low)
    .close(|d| d.close)
    .body_width_ratio(body_width_ratio)
    .tick_margin(tick_margin)
    .tooltip_value(|_, _, value| format!("${value:.2}").into())
    .id(id),
CandlestickChart {
    points: candles;
    body-width: 1.0;
    min-price: 90;
    max-price: 130;
    tick-margin: 2;
}

#CandlestickChart — Wider ticks

GPUINo GPUI version of this example yet.
CandlestickChart {
    points: candles;
    min-price: 90;
    max-price: 130;
    tick-margin: 3;
}

#CandlestickChart — Every tenth label

CandlestickChart::new(data.stock_prices.clone())
    .x(|d| d.date.clone())
    .open(|d| d.open)
    .high(|d| d.high)
    .low(|d| d.low)
    .close(|d| d.close)
    .body_width_ratio(body_width_ratio)
    .tick_margin(tick_margin)
    .tooltip_value(|_, _, value| format!("${value:.2}").into())
    .id(id),
CandlestickChart {
    points: candles;
    min-price: 90;
    max-price: 130;
    tick-margin: 10;
}

#Candlestick Chart Colors

A candle that closed above its open is drawn in the theme’s chart.bullish color and one that closed at or below it in chart.bearish. Markets that read a rise as red swap them:

#SankeyChart

A sankey diagram visualizes flows between nodes, ideal for financial statements, energy flows, and traffic analysis. The layout algorithm mirrors d3-sankey.

#SankeyChart — FY 2025

let chart = SankeyChart::new(nodes.clone(), links.clone())
                    .id(("sankey-chart", index))
                    .node_align(SankeyAlign::Center)
                    .node_padding(40.)
                    .value_scale(SankeyValueScale::Sqrt)
                    .node_color(|d: &TslaNode| d.color);
                // The first chart shows fully custom three-line labels with
                // the year-over-year change; the other keeps the default
                // value/name lines.
                let chart = if index == 0 {
                    let up = cx.theme().success;
                    let down = cx.theme().danger;
                    let muted = cx.theme().muted_foreground;
                    // `labels` draws the node text but never reaches the tooltip,
                    // so the tooltip needs its own name and value.
                    chart
                        .tooltip_name(|d: &TslaNode| d.name.clone())
                        .tooltip_value(|d: &TslaNode, _| {
                            format!("${:.2}B", d.value / 1_000_000_000.).into()
                        })
                        .labels(move |d: &TslaNode, _| {
                            let mut lines = vec![SankeyLabel::new(format!(
                                "${:.2}B",
                                d.value / 1_000_000_000.
                            ))];
                            if let Some(growth) = d.growth {
                                let arrow = if growth >= 0. { "▲" } else { "▼" };
                                lines.push(
                                    SankeyLabel::new(format!("{} {:+.2}%", arrow, growth))
                                        .color(if growth >= 0. { up } else { down }),
                                );
                            }
                            lines.push(SankeyLabel::new(d.name.clone()).color(muted));
                            lines
                        })
                } else {
                    chart
                        .node_label(|d| d.name.clone())
                        .value_label(|d, _| format!("${:.2}B", d.value / 1_000_000_000.).into())
                };
SankeyChart {
    nodes: fy25-nodes;
    links: fy25-links;
}

#SankeyChart — FY 2024

let chart = SankeyChart::new(nodes.clone(), links.clone())
                    .id(("sankey-chart", index))
                    .node_align(SankeyAlign::Center)
                    .node_padding(40.)
                    .value_scale(SankeyValueScale::Sqrt)
                    .node_color(|d: &TslaNode| d.color);
                // The first chart shows fully custom three-line labels with
                // the year-over-year change; the other keeps the default
                // value/name lines.
                let chart = if index == 0 {
                    let up = cx.theme().success;
                    let down = cx.theme().danger;
                    let muted = cx.theme().muted_foreground;
                    // `labels` draws the node text but never reaches the tooltip,
                    // so the tooltip needs its own name and value.
                    chart
                        .tooltip_name(|d: &TslaNode| d.name.clone())
                        .tooltip_value(|d: &TslaNode, _| {
                            format!("${:.2}B", d.value / 1_000_000_000.).into()
                        })
                        .labels(move |d: &TslaNode, _| {
                            let mut lines = vec![SankeyLabel::new(format!(
                                "${:.2}B",
                                d.value / 1_000_000_000.
                            ))];
                            if let Some(growth) = d.growth {
                                let arrow = if growth >= 0. { "▲" } else { "▼" };
                                lines.push(
                                    SankeyLabel::new(format!("{} {:+.2}%", arrow, growth))
                                        .color(if growth >= 0. { up } else { down }),
                                );
                            }
                            lines.push(SankeyLabel::new(d.name.clone()).color(muted));
                            lines
                        })
                } else {
                    chart
                        .node_label(|d| d.name.clone())
                        .value_label(|d, _| format!("${:.2}B", d.value / 1_000_000_000.).into())
                };
SankeyChart {
    nodes: fy24-nodes;
    links: fy24-links;
}

#Basic Sankey Chart

The value label is drawn above the name label. Its closure receives the node’s computed throughput (the larger of incoming and outgoing flow).

#Sankey Chart Styling

Link ribbons are filled with a horizontal gradient from the source node color to the target node color.

#Custom Labels

For full control over the label lines, use labels — one SankeyLabel per line, top to bottom, each with its own color and font size. It takes precedence over node_label/value_label when set. For example, a financial-statement label with a year-over-year change line:

Line color defaults to the theme foreground and font size to 10; the chart keeps handling placement, alignment and margin reservation. A first/last-column label wider than its reserved margin is truncated with a trailing ellipsis rather than drawn outside the plot, so break or shorten long labels yourself if you want the full text on multiple lines.

#Compressing Large Value Ranges

Node heights are linear in flow value by default, so a large value range (e.g. 200:1) leaves the small flows nearly invisible and the dominant flow oversized. Set value_scale(SankeyValueScale::Sqrt) to compress the range — the component sizes nodes by the square root of the value, so small flows stay visible without pre-transforming the data, and labels still receive the raw values:

Every node stays exactly filled by its ribbons under either scale, so children always match their parent’s height.

#Hover and Tooltips

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// Nothing opts in: a chart hit-tests the pointer, draws the guide and a mark
// on the datum under it, and shows that datum's name and value in a tooltip
// that flips toward the centre near an edge. The tooltip fades in and out,
// and every series draws in over one second on the first paint. The state
// lives in the widget tree, so the element itself stays stateless.
let hover = column![
    LineChart::new(visitors()).dots(true).tone(0),
    BarChart::new(regions()).mixed(true).rounded(true),
]
.spacing(theme::SPACING_MD);

Every chart hit-tests the cursor, shows a tooltip for the datum under it, and emphasizes that datum the way the chart’s kind calls for. Nothing opts in:

LineChart::new(data)
    .x(|d| d.date.clone())
    .y(|d| d.value)
    .name("Desktop") // The series name in the tooltip row
SlintNo Slint version of this example yet.
ChartOn hover
LineChart, AreaChartA crosshair and a dot per series glide along the line to the hovered point; the dot grows a halo.
BarChartA highlight band the width of a bar slides to the hovered bar, and the other bars fade behind it.
PieChartThe hovered slice lifts out of the ring and the others fade; the tooltip shows the value and its share, via tooltip_value if one is set.
RadarChartA dot per series glides along the polygon to the hovered spoke.
CandlestickChartA highlight band slides to the hovered candle; the tooltip lists open, high, low and close.
SankeyChartThe links of the hovered node keep their color while the rest fade; the tooltip shows the node’s name and throughput, via tooltip_name / tooltip_value if set.

The tooltip box follows the cursor, flipping toward the center of the plot near each edge. AreaChart and RadarChart take one .name() per series, called after the matching .y() / .value().

A tooltip row reads as a swatch, a name and a value. PieChart::tooltip_name and SankeyChart::tooltip_name set that name from the datum under the cursor — the slice’s name, the node’s name — which for a chart showing one number per datum is what the row wants. Unset, a pie falls back to name, the single name the whole series carries, and a sankey’s row carries no name at all: a swatch and a number with a gap between them.

name cannot stand in for it. It says what the numbers measure, the same for every slice, so it can never say which slice the tooltip is about. Titling the tooltip from label cannot either, since label also draws the leader lines around the ring, and a sankey’s node_label likewise writes beside the node.

PieChart::tooltip_value replaces the text of its row, which the default writes as the raw value and its share. Set it wherever the raw number is not what a reader should see — a value that is already a ratio reads as 0.35 (35.0%) otherwise, and a chart drawn from adjusted values, such as a floor that keeps a hairline slice visible, would report the adjustment as though it were the datum:

PieChart::new(holdings)
    .value(|d| d.ratio.max(MIN_VISIBLE))     // Drawn with a floor
    .tooltip_name(|d| d.name.clone())        // Named without leader lines
    .tooltip_value(|d, _, _| pct(d.ratio))   // Read as it truly is
SlintNo Slint version of this example yet.

#Tooltip Content

LineChart, AreaChart, BarChart, RadarChart and CandlestickChart title their tooltip with the hovered x, band or dimension value and write each row’s value as the raw number. tooltip_title and tooltip_value replace that text from the datum under the cursor, and tooltip_value_color colors each row’s value, such as green or red by its sign. Both closures receive the datum and the value the row reads; on AreaChart, RadarChart and CandlestickChart, which show several rows, they also receive the row’s index between the two — the series in the order they were added, or open, high, low and close for a candlestick:

BarChart::new(flows)
    .band(|d| d.month.clone())
    .value(|d| d.net)
    .tooltip_title(|d| format!("{} 2025", d.month).into())
    .tooltip_value(|_, value| format!("${value:.2}").into())
    .tooltip_value_color(move |_, value| if value >= 0. { gain } else { loss })
SlintNo Slint version of this example yet.

For a layout the title and rows cannot express, such as a table, tooltip_content draws the box’s content from the datum. The chart’s hover marks — crosshair, dots, highlight band — and where the box sits stay the chart’s, and the three text options no longer apply:

AreaChart::new(data)
    .x(|d| d.month.clone())
    .y(|d| d.last_year)
    .y(|d| d.revenue)
    .tooltip_content(|d, _, _| {
        v_flex()
            .child(d.month.clone())
            .child(format!("2025: {}", d.revenue))
            .child(format!("2024: {}", d.last_year))
    })
SlintNo Slint version of this example yet.

#Identity

All of it is keyed on an ElementId, which a chart takes from the source location it was constructed at — unique for a chart written out once, which is nearly every chart. Where one construction site renders several charts as siblings, name them apart, or they share one hover state and one cache:

shares.iter().enumerate().map(|(i, share)| PieChart::new(share.clone()).id(("share", i)))
SlintNo Slint version of this example yet.

A GlobalElementId is the whole id stack, so siblings that already carry an id of their own — rows drawn by List or uniform_list, say — separate the charts beneath them without help.

#Turning it off

interactive(false) takes the whole layer away, hitbox included, the way Highcharts’ enableMouseTracking or ECharts’ silent does. Reach for it in two places: a chart that only decorates, and a chart something else is drawn over:

AreaChart::new(placeholder).interactive(false) // A skeleton, a thumbnail
AreaChart::new(range).interactive(false)       // A backdrop under drag handles
SlintNo Slint version of this example yet.

The second matters because a plain hitbox does not block the one behind it: an element painted over an interactive chart is hovered and so is the chart, so the crosshair keeps tracking under it. The chart has to stand down.

A chart that is off keeps its id, so it still draws in and keeps its cache.

#Motion

The emphasis is animated with the styled layer’s motion tokens (cx.theme().motion_tokens()), which the theme projects onto gpui-base as its PlotMotion: pointers — crosshair, band, dots — follow the hovered datum on a fast spring, a pie slice lifts on the control spring, and the whole overlay fades in when the cursor lands on a datum and out after it leaves. The motion honors the operating system’s reduced-motion preference, under which every value adopts its target at once.

#Appear

The data draws in the first time a chart is painted, over 1000 ms on easeOutQuart, Chart.js’ default curve: lines, areas, candlesticks and a sankey diagram are revealed from the left the way ECharts, Highcharts and Recharts draw them, bars grow out of the zero line together, a pie sweeps clockwise from its first slice, and a radar grows out of its center. Axes, grid lines and tick labels are there from the first frame, and the tooltip waits until the data is whole.

The appear runs once per id. New data paints in place, so a chart fed live quotes does not draw in again on every tick. To replay it when the chart starts showing something else, such as another symbol or period, hand it a key:

LineChart::new(candles).appear_key((&symbol, period))
SlintNo Slint version of this example yet.

A chart that stops being painted forgets its appear, so one in a virtual list draws in again every time it scrolls back into view. Wrap the list in a PlotAppearScope to have each chart draw in once and show whole when it returns; name the scope after what the list shows, such as a conversation or a watchlist, so moving to another one, or closing the view, draws its charts in afresh:

PlotAppearScope::new(("rows", list_id), list(state, render_row).flex_1())
SlintNo Slint version of this example yet.

Where a chart should never draw in, turn the appear off:

LineChart::new(intraday).interactive(false).appear(false)
SlintNo Slint version of this example yet.

Reduced motion skips the appear.

#Caching

A chart also keeps its heavy geometry across frames, since it repaints on every frame it is on screen: line and area strokes and pie slices stay tessellated while their projected points are unchanged, and a sankey diagram keeps its placement while its data, settings and size are unchanged. This cache hangs off the same id, so charts sharing one share the cache and thrash it — another reason to name siblings apart. A pie or radar being drawn in is a new shape on every frame, so it tessellates afresh until the appear ends.

#Custom Plots

A custom [Plot] opts in by hand — Plot::id defaults to None there. The trait, PlotElement and hover tracking come from gpui_kit::base::plot, so a plot written against that path works here unchanged. Return an id from Plot::id, resolve the datum under the cursor in Plot::tooltip_state, and build the overlay in Plot::tooltip. To draw in, keep the PlotAppear that Plot::appear hands over on every frame and paint with its progress. The Tooltip returned there animates the hover on its own, the same way the built-in charts do: the whole overlay fades with the hover, the crosshair and dots glide to each hovered datum on the pointer spring, adopting it on the frame the cursor lands, and a dot’s halo grows as the hover fades in. A crosshair glides along the axis it marks only, so a line that also follows the cursor keeps up with it. Pass the data point itself; the tooltip does the rest:

fn tooltip(&self, state: &TooltipState, cursor: Point<Pixels>, bounds: Bounds<Pixels>, _: &mut Window, cx: &mut App) -> Option<AnyElement> {
    Some(
        Tooltip::new(cursor, bounds.size)
            .cross_line(CrossLine::new(state.cross_line).band(px(24.)))
            .title("Title")
            .row(cx.theme().chart_1, "Series", "42")
            .into_any_element(),
    )
}
SlintNo Slint version of this example yet.

A row’s value takes a color with value_color, which colors the row added last, such as a change by its sign; call it right after that row. plain_row adds a row without a swatch, for a figure no series on the plot draws, such as a total or a ratio; beside series rows its label lines up with theirs:

Tooltip::new(cursor, bounds.size)
    .title("Apr 5")
    .row(desktop, "Desktop", "373")
    .row(mobile, "Mobile", "187")
    .plain_row("Total", "560")
    .plain_row("Change", "+12%")
    .value_color(gain)
SlintNo Slint version of this example yet.

To emphasize the plot’s own graphics as well — fade the bars around the hovered one, lift a slice — implement Plot::hover, which runs each frame before tooltip and paint with the hovered [PlotHover]. It carries the TooltipState and lingers after the cursor leaves while hover.progress() eases back to zero, so sample the motion there and keep the result on self. hover.glide follows a position on the same spring the tooltip uses; hand the result to the crosshair and turn the tooltip’s own glide off with Tooltip::glide(false), so it springs once:

fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) {
    self.band_center =
        hover.map(|hover| hover.glide(("my-plot", "band"), hover.state().cross_line.x, window, cx));
}
SlintNo Slint version of this example yet.

#Data Structures

#Example Data Types

// Time series data
#[derive(Clone)]
struct DailyDevice {
    pub date: String,
    pub desktop: f64,
    pub mobile: f64,
}

// Category data with styling
#[derive(Clone)]
struct MonthlyDevice {
    pub month: String,
    pub desktop: f64,
    pub color_alpha: f32,
}

impl MonthlyDevice {
    pub fn color(&self, base_color: Hsla) -> Hsla {
        base_color.alpha(self.color_alpha)
    }
}

// Financial data
#[derive(Clone)]
struct StockPrice {
    pub date: String,
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: u64,
}

// Sankey flow: nodes are referenced by index (from gpui_kit::component::plot::shape)
pub struct SankeyLink {
    pub source: usize,
    pub target: usize,
    pub value: f64,
}
SlintNo Slint version of this example yet.

#Chart Configuration

#Container Setup

fn chart_container(
    title: &str,
    chart: impl IntoElement,
    center: bool,
    cx: &mut Context<ChartStory>,
) -> impl IntoElement {
    v_flex()
        .flex_1()
        .h_full()
        .border_1()
        .border_color(cx.theme().border)
        .rounded(cx.theme().radius_lg)
        .p_4()
        .child(
            div()
                .when(center, |this| this.text_center())
                .font_semibold()
                .child(title.to_string()),
        )
        .child(
            div()
                .when(center, |this| this.text_center())
                .text_color(cx.theme().muted_foreground)
                .text_sm()
                .child("Data period label"),
        )
        .child(div().flex_1().py_4().child(chart))
        .child(
            div()
                .when(center, |this| this.text_center())
                .font_semibold()
                .text_sm()
                .child("Summary statistic"),
        )
        .child(
            div()
                .when(center, |this| this.text_center())
                .text_color(cx.theme().muted_foreground)
                .text_sm()
                .child("Additional context"),
        )
}
SlintNo Slint version of this example yet.

#Theme Integration

// Charts automatically use theme colors
let chart = LineChart::new(data)
    .x(|d| d.date.clone())
    .y(|d| d.value)
    .stroke(cx.theme().chart_1); // Uses theme chart colors

// Available theme chart colors (`chart.1` … `chart.5` in the theme file):
// cx.theme().chart_1 … cx.theme().chart_5
SlintNo Slint version of this example yet.

#Customization Options

#Color Schemes

// Theme-based colors (recommended)
LineChart::new(data)
    .x(|d| d.x.clone())
    .y(|d| d.y)
    .stroke(cx.theme().chart_1)

// Custom color palette
let colors = [
    cx.theme().success,
    cx.theme().warning,
    cx.theme().destructive,
    cx.theme().info,
    cx.theme().chart_1,
];

BarChart::new(data)
    .band(|d| d.category.clone())
    .value(|d| d.value)
    .fill(|d, _, _, _| colors[d.category_index % colors.len()])
SlintNo Slint version of this example yet.

#Responsive Design

// Container with responsive sizing
div()
    .flex_1()
    .min_h(px(300.))
    .max_h(px(600.))
    .w_full()
    .child(
        LineChart::new(data)
            .x(|d| d.x.clone())
            .y(|d| d.y)
    )
SlintNo Slint version of this example yet.

#Grid and Axis Styling

Charts automatically include:

  • Grid lines with dashed appearance, in the theme’s chart.grid color (a translucent border when a theme leaves it unset)
  • X-axis labels with smart positioning
  • Y-axis scaling starting from zero
  • Responsive tick spacing based on tick_margin

#Performance Considerations

#Large Datasets

// For large datasets, consider data sampling
let sampled_data: Vec<_> = data
    .iter()
    .step_by(5) // Show every 5th point
    .cloned()
    .collect();

LineChart::new(sampled_data)
    .x(|d| d.date.clone())
    .y(|d| d.value)
    .tick_margin(3) // Reduce tick density
SlintNo Slint version of this example yet.

#Memory Optimization

// Use efficient data accessors
LineChart::new(data)
    .x(|d| d.date.clone()) // Clone only when necessary
    .y(|d| d.value)        // Direct field access
SlintNo Slint version of this example yet.

#Integration Examples

#With State Management

struct ChartComponent {
    data: Vec<DataPoint>,
    chart_type: ChartType,
    time_range: TimeRange,
}

impl ChartComponent {
    fn render_chart(&self, cx: &mut Context<Self>) -> impl IntoElement {
        match self.chart_type {
            ChartType::Line => LineChart::new(self.filtered_data())
                .x(|d| d.date.clone())
                .y(|d| d.value)
                .into_any_element(),
            ChartType::Bar => BarChart::new(self.filtered_data())
                .band(|d| d.date.clone())
                .value(|d| d.value)
                .into_any_element(),
            ChartType::Area => AreaChart::new(self.filtered_data())
                .x(|d| d.date.clone())
                .y(|d| d.value)
                .into_any_element(),
        }
    }

    fn filtered_data(&self) -> Vec<DataPoint> {
        self.data
            .iter()
            .filter(|d| self.time_range.contains(&d.date))
            .cloned()
            .collect()
    }
}
SlintNo Slint version of this example yet.

#Real-time Updates

struct LiveChart {
    data: Vec<DataPoint>,
    max_points: usize,
}

impl LiveChart {
    fn add_data_point(&mut self, point: DataPoint) {
        self.data.push(point);
        if self.data.len() > self.max_points {
            self.data.remove(0); // Remove oldest point
        }
    }

    fn render(&self, cx: &mut Context<Self>) -> impl IntoElement {
        LineChart::new(self.data.clone())
            .x(|d| d.timestamp.clone())
            .y(|d| d.value)
            .linear()
            .dot()
    }
}
SlintNo Slint version of this example yet.

#API Reference

#LineInterpolation

curve, linear, step

#BarAlign

bottom, top, left, right

#ChartPoint

NameKindTypeDefault
labelfieldstring
valuefieldfloat

#SeriesPoint

NameKindTypeDefault
labelfieldstring
afieldfloat
bfieldfloat

#StackPoint

NameKindTypeDefault
labelfieldstring
afieldfloat
bfieldfloat
cfieldfloat
dfieldfloat

#RadarPoint

NameKindTypeDefault
labelfieldstring
alphafieldfloat
betafieldfloat

#CandlePoint

NameKindTypeDefault
labelfieldstring
openfieldfloat
highfieldfloat
lowfieldfloat
closefieldfloat

#SankeyNode

NameKindTypeDefault
namefieldstring
valuefieldfloat
columnfieldint
slotfieldint
tonefieldint
NameKindTypeDefault

#ChartSwatch

Shared palette. Index wraps after five theme chart colors.

NameKindTypeDefault
toneinint
alphainfloat1

#ChartLegend

NameKindTypeDefault
pointsin[ChartPoint]

#LineChart

Line through up to twelve points. Curve is a smooth cubic; step is step-after.

NameKindTypeDefault
pointsin[ChartPoint]
interpolationinLineInterpolationLineInterpolation.curve
dotsinbool
strokeincolorTheme.chart-2
tick-margininint1
max-valueinfloat
plot-heightinlength140px

#AreaChart

NameKindTypeDefault
pointsin[ChartPoint]
seriesin[SeriesPoint]
stackedinbool
interpolationinLineInterpolationLineInterpolation.curve
fillincolorTheme.chart-2
fill-bincolorTheme.chart-4
series-ainstring
series-binstring
tick-margininint1
max-valueinfloat
plot-heightinlength140px

#BarChart

NameKindTypeDefault
pointsin[ChartPoint]
stacksin[StackPoint]
stackedinbool
alignmentinBarAlignBarAlign.bottom
roundedinbool
mixedinbool
gradientinbool
show-labelsinbool
value-axisinbool
tick-margininint1
fillincolorTheme.chart-2
bar-widthinlength22px
plot-heightinlength140px
series-ainstring"A"
series-binstring"B"
series-cinstring"C"
series-dinstring"D"

#PieChart

NameKindTypeDefault
pointsin[ChartPoint]
donutinbool
show-labelsinbool
pad-angleinfloat
inner-radiusinfloat0
outer-radiusinfloat46
plot-sizeinlength180px

#RadarChart

NameKindTypeDefault
pointsin[RadarPoint]
multipleinbool
dotsinbool
lines-onlyinbool
grid-levelsinint4
strokeincolorTheme.chart-1
stroke-bincolorTheme.chart-2
series-ainstring"Alpha"
series-binstring"Beta"
max-valueinfloat100
plot-sizeinlength200px

#CandlestickChart

NameKindTypeDefault
pointsin[CandlePoint]
body-widthinfloat0.8
tick-margininint5
bullishincolorTheme.success
bearishincolorTheme.danger
min-priceinfloat
max-priceinfloat
plot-heightinlength140px

#SankeyChart

NameKindTypeDefault
nodesin[SankeyNode]
node-widthinlength10px
plot-heightinlength220px