Rask
Rask
v0.23.1-alpha.0.440

Components

Chart

A chart of rows, assembled from parts as Flux's is, and drawn as SVG in C# with no chart library. Move the pointer over one: the cursor, the tooltip, the summary and a pie's slice follow it in the browser, with no round trip.

Drawn and behaving as Flux UI's chart does, measured against its live docs.

Line

A line, its axes, a cursor and a tooltip that reads the row under the pointer.

ChartExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartExample : Component
{
    // The numbers on Flux's own page.
    private static readonly Visit[] Visits =
    [
        .. new[] { 171, 227, 269, 223, 249, 251, 300, 246, 162, 115, 176, 144, 184, 269, 259, 267 }
            .Select((visitors, day) => new Visit(new DateOnly(2026, 1, 1).AddDays(day), visitors)),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Visits).Class("aspect-3/1")[
            Ui.ChartSvg[
                Ui.ChartLine.Field((Visit v) => v.Visitors).Class("text-pink-500 dark:text-pink-400"),
                Ui.ChartAxis.X.Field((Visit v) => v.Day)[Ui.ChartAxisLine, Ui.ChartAxisTick],
                Ui.ChartAxis.Y[Ui.ChartAxisGrid, Ui.ChartAxisTick],
                Ui.ChartCursor
            ],
            Ui.ChartTooltip[
                Ui.ChartTooltipHeading.Field((Visit v) => v.Day),
                Ui.ChartTooltipValue.Field((Visit v) => v.Visitors).Label("Visitors")
            ]
        ];

    private sealed record Visit(DateOnly Day, int Visitors);
}
Live result
Visitors

Line with points

A dot on every row, on a time axis, with the value axis written as a unit.

ChartPointsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartPointsExample : Component
{
    private static readonly Reading[] Readings =
    [
        .. new[] { 268, 243, 236, 201, 228, 251, 231, 267, 202, 206, 280, 268, 265, 215, 254, 237, 253, 266, 257, 297, 269 }
            .Select((memory, minute) => new Reading(new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Unspecified).AddMinutes(minute), memory)),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Readings).Class("aspect-3/1")[
            Ui.ChartSvg[
                Ui.ChartLine.Field((Reading r) => r.Memory).Class("text-pink-500"),
                Ui.ChartPoint.Field((Reading r) => r.Memory).Class("text-pink-400"),
                Ui.ChartAxis.X.Field((Reading r) => r.At)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                Ui.ChartAxis.Y.Format(new() { Style = Ui.ChartFormatStyle.Unit, Unit = "megabyte" })[Ui.ChartAxisGrid, Ui.ChartAxisTick]
            ]
        ];

    private sealed record Reading(DateTime At, int Memory);
}
Live result

Area

The area under a straight line, with the value axis on the right in compact dollars.

ChartAreaExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartAreaExample : Component
{
    // A price that climbs faster every minute: worked out, so the drawing is the same on every visit.
    private static readonly Price[] Prices =
    [
        .. Enumerable.Range(0, 21)
            .Select(minute => new Price(new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Unspecified).AddMinutes(minute), 200 + (minute * minute * 7))),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Prices).Class("aspect-3/1")[
            Ui.ChartSvg[
                Ui.ChartLine.Field((Price p) => p.Stock).Class("text-blue-500 dark:text-blue-400").Curve(Ui.ChartCurve.None),
                Ui.ChartArea.Field((Price p) => p.Stock).Class("text-blue-200/50 dark:text-blue-400/30").Curve(Ui.ChartCurve.None),
                Ui.ChartAxis.Y.Position(Ui.Position.Right).TickPrefix("$")
                    .Format(new() { Notation = Ui.ChartFormatNotation.Compact, MaximumFractionDigits = 1 })[Ui.ChartAxisGrid, Ui.ChartAxisTick],
                Ui.ChartAxis.X.Field((Price p) => p.At)[Ui.ChartAxisTick, Ui.ChartAxisLine]
            ]
        ];

    private sealed record Price(DateTime At, double Stock);
}
Live result

Multiple lines

A line and points per series, a percent axis from 0 to 1, and a legend under the drawing.

ChartMultipleLinesExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartMultipleLinesExample : Component
{
    private static readonly Social[] Networks =
    [
        new(new(2021, 6, 1), 0.26, 0.21, 0.31), new(new(2022, 6, 1), 0.19, 0.14, 0.35), new(new(2023, 6, 1), 0.22, 0.09, 0.59),
        new(new(2024, 6, 1), 0.11, 0.16, 0.43), new(new(2025, 6, 1), 0.18, 0.13, 0.36),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Networks)[
            Ui.ChartViewport.Class("aspect-3/1")[
                Ui.ChartSvg[
                    Ui.ChartLine.Field((Social s) => s.Twitter).Class("text-blue-500").Curve(Ui.ChartCurve.None),
                    Ui.ChartPoint.Field((Social s) => s.Twitter).Class("text-blue-500").R(6).StrokeWidth(3),
                    Ui.ChartLine.Field((Social s) => s.Facebook).Class("text-red-500").Curve(Ui.ChartCurve.None),
                    Ui.ChartPoint.Field((Social s) => s.Facebook).Class("text-red-500").R(6).StrokeWidth(3),
                    Ui.ChartLine.Field((Social s) => s.Instagram).Class("text-green-500").Curve(Ui.ChartCurve.None),
                    Ui.ChartPoint.Field((Social s) => s.Instagram).Class("text-green-500").R(6).StrokeWidth(3),
                    Ui.ChartAxis.X.Field((Social s) => s.Day)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                    Ui.ChartAxis.Y.TickStart(0).TickEnd(1)
                        .Format(new() { Style = Ui.ChartFormatStyle.Percent, MaximumFractionDigits = 0 })[Ui.ChartAxisGrid, Ui.ChartAxisTick]
                ]
            ],
            Div.Class("flex justify-center gap-4 pt-4")[
                Ui.ChartLegend.Key("instagram").Label("Instagram")[Ui.ChartLegendIndicator.Class("bg-green-400")],
                Ui.ChartLegend.Key("twitter").Label("Twitter")[Ui.ChartLegendIndicator.Class("bg-blue-400")],
                Ui.ChartLegend.Key("facebook").Label("Facebook")[Ui.ChartLegendIndicator.Class("bg-red-400")]
            ]
        ];

    private sealed record Social(DateOnly Day, double Twitter, double Facebook, double Instagram);
}
Live result
Instagram
Twitter
Facebook

Bar

A bar per row, about eight ticks along the bottom, and a cursor that shades the row's band.

ChartBarExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartBarExample : Component
{
    private static readonly Visit[] Visits =
    [
        .. new[] { 171, 227, 269, 223, 249, 251, 300, 246, 162, 115, 176, 144, 184, 269, 259, 267 }
            .Select((visitors, day) => new Visit(new DateOnly(2026, 1, 1).AddDays(day), visitors)),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Visits).Class("aspect-3/1")[
            Ui.ChartSvg[
                Ui.ChartBar.Field((Visit v) => v.Visitors).Class("text-blue-500").Radius("0").Width("85%"),
                Ui.ChartAxis.X.Field((Visit v) => v.Day).TickCount(8)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                Ui.ChartAxis.Y.Format(new() { UseGrouping = true })[Ui.ChartAxisGrid, Ui.ChartAxisTick],
                Ui.ChartCursor.Type(Ui.ChartCursorType.Area)
            ],
            Ui.ChartTooltip[
                Ui.ChartTooltipHeading.Field((Visit v) => v.Day),
                Ui.ChartTooltipValue.Field((Visit v) => v.Visitors).Label("Visitors")
            ]
        ];

    private sealed record Visit(DateOnly Day, int Visitors);
}
Live result
Visitors

Horizontal

Horizontal() puts the rows down the Y axis: bars on their side.

ChartHorizontalExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartHorizontalExample : Component
{
    private static readonly Sale[] Sales =
    [
        new("Beauty", 342), new("Books", 356), new("Clothes", 432), new("Garden", 398), new("Pets", 487), new("Sports", 548), new("Toys", 643),
    ];

    protected override Component? Render() =>
        Ui.Chart.Horizontal().Value(Sales).Class("aspect-2/1")[
            Ui.ChartSvg[
                Ui.ChartBar.Field((Sale s) => s.Online).Class("text-blue-500").Radius("4 0").Width("70%"),
                Ui.ChartAxis.Y.Field((Sale s) => s.Category)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                Ui.ChartAxis.X[Ui.ChartAxisGrid, Ui.ChartAxisTick],
                Ui.ChartCursor.Type(Ui.ChartCursorType.Area)
            ],
            Ui.ChartTooltip[
                Ui.ChartTooltipHeading.Field((Sale s) => s.Category),
                Ui.ChartTooltipValue.Field((Sale s) => s.Online).Label("Orders")
            ]
        ];

    private sealed record Sale(string Category, int Online);
}
Live result
Orders

Grouped bars

Bars side by side in each row's slot.

ChartGroupedBarsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartGroupedBarsExample : Component
{
    private static readonly Sale[] Sales =
    [
        new("Beauty", 342, 215, 108), new("Books", 356, 256, 215), new("Clothes", 432, 284, 176), new("Garden", 398, 190, 143),
        new("Pets", 487, 301, 122), new("Sports", 548, 327, 190), new("Toys", 643, 412, 236),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Sales)[
            Ui.ChartViewport.Class("aspect-3/1")[
                Ui.ChartSvg[
                    Ui.ChartGroup[
                        Ui.ChartBar.Field((Sale s) => s.Online).Class("text-blue-600"),
                        Ui.ChartBar.Field((Sale s) => s.Retail).Class("text-blue-500"),
                        Ui.ChartBar.Field((Sale s) => s.Wholesale).Class("text-blue-300")
                    ],
                    Ui.ChartAxis.X.Field((Sale s) => s.Category)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                    Ui.ChartAxis.Y[Ui.ChartAxisGrid, Ui.ChartAxisTick]
                ]
            ],
            Div.Class("flex justify-center gap-4 pt-4")[
                Ui.ChartLegend.Key("online").Label("Online")[Ui.ChartLegendIndicator.Class("bg-blue-600")],
                Ui.ChartLegend.Key("retail").Label("Retail")[Ui.ChartLegendIndicator.Class("bg-blue-500")],
                Ui.ChartLegend.Key("wholesale").Label("Wholesale")[Ui.ChartLegendIndicator.Class("bg-blue-300")]
            ]
        ];

    private sealed record Sale(string Category, int Online, int Retail, int Wholesale);
}
Live result
Online
Retail
Wholesale

Stacked bars

Bars on top of one another, the last one rounded.

ChartStackedBarsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartStackedBarsExample : Component
{
    private static readonly Sale[] Sales =
    [
        new("Beauty", 342, 215, 108), new("Books", 356, 256, 215), new("Clothes", 432, 284, 176), new("Garden", 398, 190, 143),
        new("Pets", 487, 301, 122), new("Sports", 548, 327, 190), new("Toys", 643, 412, 236),
    ];

    protected override Component? Render() =>
        Ui.Chart.Value(Sales)[
            Ui.ChartViewport.Class("aspect-3/1")[
                Ui.ChartSvg[
                    Ui.ChartStack.Width("65%")[
                        Ui.ChartBar.Field((Sale s) => s.Online).Class("text-blue-600"),
                        Ui.ChartBar.Field((Sale s) => s.Retail).Class("text-blue-400"),
                        Ui.ChartBar.Field((Sale s) => s.Wholesale).Class("text-blue-300").Radius("4 0")
                    ],
                    Ui.ChartAxis.X.Field((Sale s) => s.Category)[Ui.ChartAxisTick, Ui.ChartAxisLine],
                    Ui.ChartAxis.Y[Ui.ChartAxisGrid, Ui.ChartAxisTick]
                ]
            ],
            Div.Class("flex justify-center gap-4 pt-4")[
                Ui.ChartLegend.Key("online").Label("Online")[Ui.ChartLegendIndicator.Class("bg-blue-600")],
                Ui.ChartLegend.Key("retail").Label("Retail")[Ui.ChartLegendIndicator.Class("bg-blue-400")],
                Ui.ChartLegend.Key("wholesale").Label("Wholesale")[Ui.ChartLegendIndicator.Class("bg-blue-300")]
            ]
        ];

    private sealed record Sale(string Category, int Online, int Retail, int Wholesale);
}
Live result
Online
Retail
Wholesale

Pie and donut

A pie, a donut, and a donut with a figure in its hole. The slice under the pointer is marked.

ChartPieExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartPieExample : Component
{
    private static readonly Share[] Shares = [new("Subscriptions", 52), new("Services", 28), new("Training", 20)];

    protected override Component? Render() =>
        Div.Class("grid grid-cols-3 gap-6")[
            Ui.Chart.Key("pie").Value(Shares)[
                Ui.ChartViewport.Class("aspect-square")[
                    Ui.ChartSvg[Ui.ChartPie.Field((Share s) => s.Value).LabelField((Share s) => s.Label)]
                ]
            ],
            Ui.Chart.Key("donut").Value(Shares)[
                Ui.ChartViewport.Class("aspect-square")[
                    Ui.ChartSvg[Ui.ChartPie.Field((Share s) => s.Value).LabelField((Share s) => s.Label).InnerRadius("60%").Radius(4)]
                ]
            ],
            Ui.Chart.Key("centre").Value(Shares)[
                Ui.ChartViewport.Class("aspect-square")[
                    Ui.ChartSvg[Ui.ChartPie.Field((Share s) => s.Value).LabelField((Share s) => s.Label).InnerRadius("60%").Radius(4)],
                    Div.Class("pointer-events-none absolute inset-0 grid place-items-center")[
                        Div.Class("text-center")[Ui.Heading.Lg["$128k"], Ui.Text["Revenue"]]
                    ]
                ]
            ]
        ];

    private sealed record Share(string Label, int Value);
}
Live result
$128k

Revenue

Slice colours

ColorField lets each row choose its hue, with a legend beside the donut.

ChartSliceColorsExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartSliceColorsExample : Component
{
    private static readonly Share[] Shares =
    [
        new("Subscriptions", 52, Ui.Color.Blue, "bg-blue-500"),
        new("Services", 28, Ui.Color.Emerald, "bg-emerald-500"),
        new("Training", 20, Ui.Color.Amber, "bg-amber-500"),
    ];

    protected override Component? Render() =>
        Ui.Card.Class("max-w-md")[
            Ui.Chart.Value(Shares).Class("flex items-center gap-8")[
                Ui.ChartViewport.Class("aspect-square w-36 shrink-0")[
                    Ui.ChartSvg[
                        Ui.ChartPie.Field((Share s) => s.Value).LabelField((Share s) => s.Label).ColorField((Share s) => s.Color)
                            .InnerRadius("60%").Radius(4).Class("dark:stroke-zinc-800")
                    ]
                ],
                Div.Class("flex-1")[
                    Shares.Select(share => Div.Key(share.Label).Class("flex items-center justify-between")[
                        Ui.ChartLegend.Label(share.Label)[Ui.ChartLegendIndicator.Class(share.Dot)],
                        Ui.Text.Sm.Class("tabular-nums")[$"{share.Value}%"]
                    ])
                ]
            ]
        ];

    private sealed record Share(string Label, int Value, Ui.Color Color, string Dot);
}
Live result
Subscriptions

52%

Services

28%

Training

20%

Live summary

Figures that read the row under the pointer, and the latest row when it leaves.

ChartSummaryExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartSummaryExample : Component
{
    private static readonly UiChartFormat Dollars = new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" };

    // Yesterday's figure is today's, nudged by the hour: worked out, so the drawing is the same on every visit.
    private static readonly Hour[] Today =
    [
        .. new[] { 299, 598, 1247, 1896, 2145, 2394, 2643, 2892, 3041, 3190, 3239 }
            .Select((sales, hour) => new Hour(new DateTime(2026, 1, 1, 9, 0, 0, DateTimeKind.Unspecified).AddHours(hour), sales, sales + (hour % 3 * 150) - 100)),
    ];

    protected override Component? Render() =>
        Ui.Card[
            Ui.Chart.Value(Today).Class("grid gap-6")[
                Ui.ChartSummary.Class("flex gap-12")[
                    Div[
                        Ui.Text["Today"],
                        Ui.Heading.Xl.Class("mt-2 tabular-nums")[Ui.ChartSummaryValue.Field((Hour h) => h.Sales).Format(Dollars)],
                        Ui.Text.Class("mt-2 tabular-nums")[
                            Ui.ChartSummaryValue.Field((Hour h) => h.At)
                                .Format(new() { Hour = Ui.ChartFormatPart.Numeric, Minute = Ui.ChartFormatPart.Numeric, Hour12 = true })
                        ]
                    ],
                    Div[
                        Ui.Text["Yesterday"],
                        Ui.Heading.Lg.Class("mt-2 tabular-nums")[Ui.ChartSummaryValue.Field((Hour h) => h.Yesterday).Format(Dollars)]
                    ]
                ],
                Ui.ChartViewport.Class("aspect-3/1")[
                    Ui.ChartSvg[
                        Ui.ChartLine.Field((Hour h) => h.Yesterday).Class("text-zinc-300 dark:text-white/40").StrokeDasharray("4 4").Curve(Ui.ChartCurve.None),
                        Ui.ChartLine.Field((Hour h) => h.Sales).Class("text-sky-500 dark:text-sky-400").Curve(Ui.ChartCurve.None),
                        Ui.ChartAxis.X.Field((Hour h) => h.At)[Ui.ChartAxisGrid, Ui.ChartAxisTick, Ui.ChartAxisLine],
                        Ui.ChartAxis.Y[Ui.ChartAxisTick],
                        Ui.ChartCursor
                    ]
                ]
            ]
        ];

    private sealed record Hour(DateTime At, decimal Sales, decimal Yesterday);
}
Live result

Today

$3,239.00

7:00 PM

Yesterday

$3,289.00

Sparkline and dashboard stat

Bare numbers need no field; Gutter("0") draws to the edges.

ChartSparklineExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class ChartSparklineExample : Component
{
    protected override Component? Render() =>
        Div.Class("flex items-start gap-8")[
            Ui.Chart.Key("sparkline").Value([15, 18, 16, 19, 22, 25, 28, 25, 29, 28, 32, 35]).Class("aspect-3/1 w-20")[
                Ui.ChartSvg.Gutter("0")[Ui.ChartLine.Class("text-green-500 dark:text-green-400")]
            ],
            Ui.Card.Key("stat").Class("min-w-48 overflow-hidden")[
                Ui.Text["Revenue"],
                Ui.Heading.Xl.Class("mt-2 tabular-nums")["$12,345"],
                Ui.Chart.Value([10, 12, 11, 13, 15, 14, 16, 18, 17, 19, 21, 20]).Class("-mx-6 -mb-6 h-12")[
                    Ui.ChartSvg.Gutter("4 0 0 0")[
                        Ui.ChartLine.Class("text-sky-200 dark:text-sky-400"),
                        Ui.ChartArea.Class("text-sky-100 dark:text-sky-400/30")
                    ]
                ]
            ]
        ];
}
Live result

Revenue

$12,345

Behaviour

Drawn in C#

No chart library: the scales, the ticks and every path are computed during the render, to the numbers Flux's own layout arrives at, for the box the chart has in the browser (scripts/flux/parity-chart.mjs compares the two drawings' geometry, and with --pointer what each does under a pointer). What Flux's own script does after that, measuring and following the pointer, is the runtime's plot hook, by attribute.

The rows

Flux's chart takes an array of rows and its parts name a column with a string. Ui.Chart.Value(rows) takes the typed list, and each part takes a selector whose lambda states the row type once, .Field((Visit v) => v.Visitors), because a part is built before the chart it goes into.

A number of any numeric type plots without a cast; a DateTime, DateOnly or DateTimeOffset on the index axis makes it a time axis; anything else is a name. A chart of bare numbers needs no field at all: Ui.Chart.Value([15, 18, 16])[Ui.ChartSvg.Gutter("0")[Ui.ChartLine]].

Axes

The value axis starts at zero (or the lowest value below it) and steps by a round quarter of its span; TickCount, TickStart, TickEnd, TickValues, TickPrefix, TickSuffix and Position(Ui.Position.Right) are Flux's.

A time axis ticks at the pace of its rows and writes itself for its reach (9:17 AM, 9 AM, Tue 9 AM, Mar 10, Mar, 2026). Labels that would touch give way as Flux's do: dates drop every other one, names turn 45°.

Drawn for its box

As Flux's, the chart fills the box its class gives it (aspect-3/1, h-64) and is drawn for that box: the browser measures the drawing (the runtime's data-rask-measure hook) and the chart is drawn again in its units, when it first appears, 100ms after its box stops changing (a window resized, a phone turned), and when a hidden chart is shown at a new size; a hidden chart keeps its drawing. Its 12px labels are 12px at every width, and the ticks that fit are worked out again.

The first render happens before anything is measured, so it draws nothing: an empty box of the size its class gives it, as Flux's own first paint is, and the drawing arrives with the measurement. Every chart on a page is measured in the same browser task, and the events of one task are answered with one render: a Server page of sixty charts is one frame out and one patch back; in WebAssembly it never leaves the browser. A page prerendered for a WebAssembly app shows the empty boxes until the runtime has started.

How numbers and dates are written

Flux's :format is the options of Intl.NumberFormat and Intl.DateTimeFormat; UiChartFormat carries them under Intl's names, written with .NET formatting in the reader's culture: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }, new() { Notation = Ui.ChartFormatNotation.Compact, MaximumFractionDigits = 1 }, new() { Month = Ui.ChartFormatPart.Short, Day = Ui.ChartFormatPart.Numeric }.

Carried: style (decimal, currency, percent, unit), currency, unit, notation (standard, compact, scientific), minimumFractionDigits, maximumFractionDigits, useGrouping, dateStyle, timeStyle, weekday, year, month, day, hour, minute, second, hour12. A date's parts are written in the order English writes them.

Not carried: compactDisplay: long, currencyDisplay, currencySign, signDisplay, unitDisplay, significant-digit and integer-digit limits, roundingMode, timeZone, timeZoneName, era, fractionalSecondDigits, dayPeriod, hourCycle, calendar, numberingSystem.

Under the pointer

Everything Flux's chart does under the pointer is done in the browser by the runtime's plot hook: no handler, no round trip per move. The row nearest the pointer along the index axis is the active one while the pointer is inside the plot. One cursor moves to it (a dashed line; Ui.ChartCursorType.Area covers the row's band), and one tooltip moves beside it, 15px from the row and from the pointer, flipped to the other side where it would pass the drawing's right or bottom edge. Its heading and values, and every Ui.ChartSummaryValue, read that row; a summary goes back to the latest row when the pointer leaves. The row's Ui.ChartPoints carry data-active.

On a pie the slice under the pointer carries data-active and the others data-inactive (.Class("transition-opacity data-inactive:opacity-40")), the tooltip follows the pointer, and a Ui.ChartTooltipIndicator takes the slice's colour.

Each part is rendered once with what it reads for every row (data-rask-plot-text), so fifty rows cost fifty short lines, not fifty tooltips. node scripts/flux/parity-chart.mjs --pointer walks a pointer over Flux's page and the kit's and compares.

Labels are measured in Inter

Flux sizes a chart's gutters by measuring its tick labels in the browser. The kit carries Inter's advance widths and kerning (UiChartInter, generated by scripts/flux/inter-metrics.mjs) and adds them up as Chromium does. In another face the labels still fit; the gutters are those Inter would have needed.

Reference

Ui.Chart

flux:chart

The chart: its rows, and the parts that draw them. Sized by its class, aspect-3/1 or h-64.

Step
Takes
What it does
.Value(…)a list of rows, or of numbersThe rows, one point, bar or slice each: Flux's wire:model and :value.
.Horizontal()boolPuts the rows down the Y axis and their values along the X axis: bars on their side.

Ui.ChartSvg

flux:chart.svg

The drawing: the lines, areas, points, bars, pies and axes inside it.

Step
Takes
What it does
.Gutter(…)stringThe space kept clear inside each edge, as a CSS padding: "12 0 12 8". 8 all round when unset; "0" for a sparkline.

Ui.ChartViewport

flux:chart.viewport

The drawing's own box, when a legend or a summary shares the chart.

No steps of its own: what goes inside it is its content.

Ui.ChartLine

flux:chart.line

A line through one value of every row.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it reads from a row. The row itself when the chart holds bare numbers.
.Curve(…)
Ui.ChartCurve
Smooth
None

Each is a step of its own too, unless an HTML tag has its name.

How the points are joined. Smooth when unset.
.StrokeDasharray(…)stringThe line's dashes: "4 4".

Ui.ChartArea

flux:chart.area

The area under a line, filled.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it reads from a row. The row itself when the chart holds bare numbers.
.Curve(…)
Ui.ChartCurve
Smooth
None

Each is a step of its own too, unless an HTML tag has its name.

How the points are joined. Smooth when unset.

Ui.ChartPoint

flux:chart.point

A dot on every row's value. The hovered row's carry data-active.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it reads from a row.
.R(…)DoubleThe dot's radius, in pixels. 4 when unset.
.StrokeWidth(…)DoubleThe ring in the surface colour that lifts a dot off its line.

Ui.ChartBar

flux:chart.bar

A bar for every row: alone, or in a group or a stack.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it reads from a row.
.Radius(…)stringHow round its corners are: "4", or "4 0" for the value end and then the base.
.Width(…)stringHow much of its slot it fills, as a percentage or in pixels: "85%".

Ui.ChartGroup

flux:chart.group

Bars side by side in each row's slot.

Step
Takes
What it does
.Width(…)stringHow much of a row's slot the group fills: "90%".

Ui.ChartStack

flux:chart.stack

Bars one on top of another in each row's slot.

Step
Takes
What it does
.Width(…)stringHow much of a row's slot the stack fills: "65%".

Ui.ChartPie

flux:chart.pie

A slice per row, or a donut. The hovered slice carries data-active, the others data-inactive.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsThe number that sets each slice's size.
.LabelField(…)a selector: (Visit v) => v.VisitorsWhat each slice is called, in the tooltip.
.ColorField(…)a selector: (Visit v) => v.VisitorsA Ui.Color each row chooses for its slice. The built-in palette in turn when unset.
.InnerRadius(…)stringThe donut's hole, in pixels or as a share of the radius: "60%".
.Radius(…)DoubleHow round each slice's corners are, in pixels.

Ui.ChartAxis

flux:chart.axis

An axis, holding its ticks, grid, line and marks. .X and .Y say which.

Step
Takes
What it does
.Axis(…)
Ui.ChartAxisAxis
X
Y

Each is a step of its own too, unless an HTML tag has its name.

Which axis it is.
.Field(…)a selector: (Visit v) => v.VisitorsWhat names each row along the index axis. A date or time makes it a time axis.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How the tick labels are written: Intl's options, under Intl's names.
.Position(…)
Ui.Position
Default
Top
Right
Bottom
Left

Each is a step of its own too, unless an HTML tag has its name.

Which side of the plot it sits on: Right for a value axis on the right.
.TickCount(…)intAbout how many ticks. Five on a value axis; every row on a band axis.
.TickStart(…)DoubleWhere the value axis starts. Zero, or the lowest value below it, when unset.
.TickEnd(…)DoubleWhere the value axis ends. The tick just past the highest value when unset.
.TickValues(…)a list of numbersThe ticks themselves, in place of the round ones the axis would choose.
.TickPrefix(…)stringWritten before every tick label: "$".
.TickSuffix(…)stringWritten after every tick label: "MB".

Ui.ChartAxisTick

flux:chart.axis.tick

The axis's labels.

Step
Takes
What it does
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How they are written. The axis's own format when unset.

Ui.ChartAxisGrid

flux:chart.axis.grid

A line across the plot at every tick.

Step
Takes
What it does
.StrokeWidth(…)DoubleHow thick the lines are.
.StrokeDasharray(…)stringTheir dashes: "4 4".

Ui.ChartAxisLine

flux:chart.axis.line

The axis's own line along the plot's edge.

Step
Takes
What it does
.StrokeWidth(…)DoubleHow thick it is.

Ui.ChartAxisMark

flux:chart.axis.mark

A short mark at every tick.

Step
Takes
What it does
.Position(…)
Ui.Position
Default
Top
Right
Bottom
Left

Each is a step of its own too, unless an HTML tag has its name.

Which side of the plot they sit on. The axis's own when unset.
.StrokeWidth(…)DoubleHow thick they are.

Ui.ChartZeroLine

flux:chart.zero-line

A line at zero, where values run below it.

Step
Takes
What it does
.StrokeWidth(…)DoubleHow thick it is.

Ui.ChartCursor

flux:chart.cursor

What marks the row under the pointer: one, moved by the browser.

Step
Takes
What it does
.Type(…)
Ui.ChartCursorType
Line
Area

Each is a step of its own too, unless an HTML tag has its name.

A dashed line through the row, or its whole band shaded. A line when unset.
.StrokeWidth(…)DoubleHow thick the line is.
.StrokeDasharray(…)stringIts dashes: "4 4".

Ui.ChartTooltip

flux:chart.tooltip

The box beside the row under the pointer: one, moved by the browser.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsThe field it shows, when it holds no parts of its own.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How that field is written.

Ui.ChartTooltipHeading

flux:chart.tooltip.heading

The tooltip's first line: the row's name or date.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it shows.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How it is written.

Ui.ChartTooltipValue

flux:chart.tooltip.value

A value of the row under the pointer, with its label.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsThe number shown.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How it is written.
.Label(…)stringWhat comes before the number: a series' name.
.LabelField(…)a selector: (Visit v) => v.VisitorsA label read from the row instead: the hovered slice's name, on a pie.
.Prefix(…)stringWritten before the number: "$".
.Suffix(…)stringWritten after the number: "%".

Ui.ChartTooltipIndicator

flux:chart.tooltip.indicator

A dot beside a value, in the series' colour or the hovered slice's.

No steps of its own: what goes inside it is its content.

Ui.ChartSummary

flux:chart.summary

Figures beside the chart that read the row under the pointer, and the latest row otherwise.

No steps of its own: what goes inside it is its content.

Ui.ChartSummaryValue

flux:chart.summary.value

One figure of a summary.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsWhat it shows.
.Fallback(…)stringWhat it shows when there is no row to read.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How it is written.

Ui.ChartLegend

flux:chart.legend

One entry of a legend: a series' name and its indicator.

Step
Takes
What it does
.Field(…)a selector: (Visit v) => v.VisitorsThe field the entry stands for.
.Label(…)stringThe series' name.
.Format(…)a UiChartFormat: new() { Style = Ui.ChartFormatStyle.Currency, Currency = "USD" }How the field's value is written.

Ui.ChartLegendIndicator

flux:chart.legend.indicator

The legend entry's dot; its class gives the colour.

No steps of its own: what goes inside it is its content.