日本語

Chart

Bar, stacked bar, horizontal bar, line, area, and donut charts of the rows of a list signal, on a scale the browser fits to the values.

Result

chart.rs
use hypersig::html::div;
use hypersig::style::pattern::stack;
use hypersig::style::value::rem;
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, Chart, DonutChart, Tabs, button, token};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum Kind {
    Bar,
    Stacked,
    Horizontal,
    Donut,
}

#[derive(Signals, Serialize, Deserialize)]
pub struct Month {
    name: String,
    desktop: i32,
    mobile: i32,
}

#[derive(Signals, Serialize, Deserialize)]
pub struct Browser {
    name: String,
    visitors: i32,
}

#[derive(Signals, Serialize, Deserialize)]
pub struct Traffic {
    kind: Kind,
    months: Vec<Month>,
    browsers: Vec<Browser>,
}

pub fn page() -> Page<Traffic> {
    // Each kind of chart draws the same two series of the same months.
    let visitors = |chart: Chart<Traffic, Month>| {
        chart
            .series(
                "Desktop",
                Month::desktop(),
                token::chart_1(),
            )
            .series(
                "Mobile",
                Month::mobile(),
                token::chart_2(),
            )
    };
    let label = "Visitors";
    let names = [
        L { en: "Jan", ja: "1 月" },
        L { en: "Feb", ja: "2 月" },
        L { en: "Mar", ja: "3 月" },
        L { en: "Apr", ja: "4 月" },
        L { en: "May", ja: "5 月" },
        L { en: "Jun", ja: "6 月" },
    ];
    let months = names
        .into_iter()
        .zip([(186, 80), (305, 200), (237, 120), (73, 190), (209, 130), (214, 140)])
        .map(|(name, (desktop, mobile))| Month {
            name: site.t(name).to_owned(),
            desktop,
            mobile,
        })
        .collect();
    let browsers = ["Chrome", "Safari", "Firefox", "Edge"]
        .into_iter()
        .map(str::to_owned)
        .chain(["Other"
            .to_owned()])
        .zip([275, 200, 187, 173, 90])
        .map(|(name, visitors)| Browser { name, visitors })
        .collect();
    let grow = Traffic::months().update_where(
        |month| month.index().equals(3),
        Month::desktop(),
        |month| month.at(Month::desktop()) + 400,
    );
    let tabs = Tabs::new(label, Traffic::kind())
        .tab(
            Kind::Bar,
            "Bar",
            visitors(Chart::bar(label, Traffic::months(), Month::name())).render(),
        )
        .tab(
            Kind::Stacked,
            "Stacked",
            visitors(Chart::stacked_bar(label, Traffic::months(), Month::name())).render(),
        )
        .tab(
            Kind::Horizontal,
            "Horizontal",
            visitors(Chart::horizontal_bar(label, Traffic::months(), Month::name())).render(),
        )
        .tab(
            Kind::Donut,
            "Donut",
            DonutChart::new(
                "Visitors by browser",
                Traffic::browsers(),
                Browser::name(),
                Browser::visitors(),
            )
            .render(),
        );
    Page::new(
        label,
        Traffic {
            kind: Kind::Bar,
            months,
            browsers,
        },
    )
    .child(
        div()
            .css(stack(rem(1)))
            .child(
                div().child(
                    button(ButtonVariant::Outline, ButtonSize::Sm)
                        .on_click(grow)
                        .text("Add 400 desktop visitors in April"),
                ),
            )
            .child(tabs),
    )
}

Accessibility

The drawing is an <svg> with viewBox="0 0 100 100" and preserveAspectRatio="none", so it stretches to its box, and its lines keep their width with vector-effect="non-scaling-stroke". The axis labels, the legend, and the box that shows a row's values under the pointer are HTML, which does not stretch. A DonutChart draws each row as a circle with pathLength="100", whose stroke-dasharray is the row's share of the total in hundredths.

The drawing is hidden from assistive technology. A screen reader reads a visually hidden <table> of the same rows, captioned with the chart's name, and the values under the pointer have no keyboard equivalent beyond that table.

A series reads a number field of the row type, so a field of another struct or a text field does not compile. The scale runs from a round number at or below the smallest value, or 0, to one at or above the largest, with grid lines on a step of 1, 2, 2.5, or 5 times a power of ten. The browser computes it with max_of, min_of, and let_in, so a statement or a patch that changes the rows refits it, and a negative value draws its bar down from the line at 0. Each bar and grid line computes the scale from every row, so the work of a change grows with the square of the number of rows.

API

ItemSummary
Chart::barA bar chart named label of rows, labeled by category.
Chart::stacked_barA stacked bar chart named label of rows, labeled by category.
Chart::horizontal_barA horizontal bar chart named label of rows, labeled by category.
Chart::lineA line chart named label of rows, labeled by category.
Chart::areaAn area chart named label of rows, labeled by category.
seriesAdds a series named name that draws the number field field in color.
max_labelsLabels at most count rows along the axis of the categories, 12 until set.
formatWrites the values and the axis labels in format.
DonutChart::newA donut chart named label of rows, with slices of value labeled by category.
token::chart_1The first series of a chart.