Skip to main content

src/sparklinekit@bar.erl

-module(sparklinekit@bar).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sparklinekit/bar.gleam").
-export([with_values/2, with_int_values/2, with_theme/2, with_color/2, with_background_color/2, with_negative_color/2, with_size/3, with_bar_gap/2, with_corner_radius/2, new/1, new_ints/1, to_svg/1, to_png/1]).
-export_type([builder/0, rect/0, layout/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " SVG and PNG bar sparklines.\n"
    "\n"
    " ```gleam\n"
    " import sparklinekit/bar\n"
    " import sparklinekit/theme\n"
    "\n"
    " pub fn revenue() -> String {\n"
    "   bar.new([3.0, 7.0, 2.0, 9.0, 5.0])\n"
    "   |> bar.with_theme(theme.sunset())\n"
    "   |> bar.with_corner_radius(2.0)\n"
    "   |> bar.to_svg\n"
    " }\n"
    " ```\n"
    "\n"
    " Positive and negative values share a zero baseline: positives\n"
    " rise above it, negatives fall below. `with_negative_color`\n"
    " paints the falling bars in a contrasting colour so win/loss\n"
    " charts read at a glance.\n"
).

-opaque builder() :: {builder,
        list(float()),
        sparklinekit@theme:theme(),
        binary(),
        binary(),
        gleam@option:option(binary()),
        integer(),
        integer(),
        float(),
        float()}.

-type rect() :: {rect, float(), float(), float(), float(), float()}.

-type layout() :: {layout, list(rect())}.

-file("src/sparklinekit/bar.gleam", 89).
?DOC(" Replace the data series after construction.\n").
-spec with_values(builder(), list(float())) -> builder().
with_values(Builder, Values) ->
    {builder,
        Values,
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 94).
?DOC(" Replace the data series with a list of `Int`s.\n").
-spec with_int_values(builder(), list(integer())) -> builder().
with_int_values(Builder, Values) ->
    {builder,
        gleam@list:map(Values, fun erlang:float/1),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 102).
?DOC(
    " Apply every colour slot from `theme`. The theme's `negative`\n"
    " colour is used for bars below the zero baseline; calling\n"
    " [`with_negative_color`](#with_negative_color) afterwards overrides\n"
    " just that slot.\n"
).
-spec with_theme(builder(), sparklinekit@theme:theme()) -> builder().
with_theme(Builder, Theme) ->
    {builder,
        erlang:element(2, Builder),
        Theme,
        sparklinekit@theme:foreground(Theme),
        sparklinekit@theme:background(Theme),
        {some, sparklinekit@theme:negative(Theme)},
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 113).
?DOC(" Set the positive-bar fill colour (any CSS colour string).\n").
-spec with_color(builder(), binary()) -> builder().
with_color(Builder, Color) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        Color,
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 119).
?DOC(
    " Set the background rectangle colour. `\"none\"` disables the\n"
    " background.\n"
).
-spec with_background_color(builder(), binary()) -> builder().
with_background_color(Builder, Color) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        Color,
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 124).
?DOC(" Use a separate colour for bars below the zero baseline.\n").
-spec with_negative_color(builder(), binary()) -> builder().
with_negative_color(Builder, Color) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        {some, Color},
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 268).
-spec render_rect_svg(rect(), binary(), float()) -> binary().
render_rect_svg(Rect, Fill, Radius) ->
    R = begin
        _pipe = Radius,
        _pipe@1 = gleam@float:min(_pipe, erlang:element(4, Rect) / 2.0),
        _pipe@2 = gleam@float:min(_pipe@1, erlang:element(5, Rect) / 2.0),
        gleam@float:max(_pipe@2, +0.0)
    end,
    Radius_attr = case R > +0.0 of
        false ->
            <<""/utf8>>;

        true ->
            <<<<" rx=\""/utf8, (sparklinekit@internal@format:coord(R))/binary>>/binary,
                "\""/utf8>>
    end,
    <<<<<<<<<<<<<<<<<<<<<<<<"<rect x=\""/utf8,
                                                    (sparklinekit@internal@format:coord(
                                                        erlang:element(2, Rect)
                                                    ))/binary>>/binary,
                                                "\" y=\""/utf8>>/binary,
                                            (sparklinekit@internal@format:coord(
                                                erlang:element(3, Rect)
                                            ))/binary>>/binary,
                                        "\" width=\""/utf8>>/binary,
                                    (sparklinekit@internal@format:coord(
                                        erlang:element(4, Rect)
                                    ))/binary>>/binary,
                                "\" height=\""/utf8>>/binary,
                            (sparklinekit@internal@format:coord(
                                erlang:element(5, Rect)
                            ))/binary>>/binary,
                        "\" fill=\""/utf8>>/binary,
                    Fill/binary>>/binary,
                "\""/utf8>>/binary,
            Radius_attr/binary>>/binary,
        "/>"/utf8>>.

-file("src/sparklinekit/bar.gleam", 450).
-spec effective_range(float(), float()) -> {float(), float()}.
effective_range(Lo, Hi) ->
    case {Lo >= +0.0, Hi =< +0.0} of
        {true, _} ->
            {+0.0, Hi};

        {_, true} ->
            {Lo, +0.0};

        {_, _} ->
            {Lo, Hi}
    end.

-file("src/sparklinekit/bar.gleam", 458).
-spec baseline_y(float(), float(), float()) -> float().
baseline_y(Height_f, Y_min, Span) ->
    case Span =< +0.0 of
        true ->
            Height_f;

        false ->
            Height_f - ((case Span of
                +0.0 -> +0.0;
                -0.0 -> -0.0;
                Gleam@denominator -> (+0.0 - Y_min) / Gleam@denominator
            end) * Height_f)
    end.

-file("src/sparklinekit/bar.gleam", 465).
-spec y_coord(float(), float(), float(), float()) -> float().
y_coord(Value, Y_min, Span, Height_f) ->
    case Span =< +0.0 of
        true ->
            Height_f;

        false ->
            Height_f - ((case Span of
                +0.0 -> +0.0;
                -0.0 -> -0.0;
                Gleam@denominator -> (Value - Y_min) / Gleam@denominator
            end) * Height_f)
    end.

-file("src/sparklinekit/bar.gleam", 472).
-spec positive_or_one(integer()) -> integer().
positive_or_one(Value) ->
    case Value < 1 of
        true ->
            1;

        false ->
            Value
    end.

-file("src/sparklinekit/bar.gleam", 130).
?DOC(
    " Set the viewBox / pixel dimensions. Non-positive values are\n"
    " normalised to `1`.\n"
).
-spec with_size(builder(), integer(), integer()) -> builder().
with_size(Builder, Width, Height) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        positive_or_one(Width),
        positive_or_one(Height),
        erlang:element(9, Builder),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 479).
-spec non_negative_float(float()) -> float().
non_negative_float(Value) ->
    case Value < +0.0 of
        true ->
            +0.0;

        false ->
            Value
    end.

-file("src/sparklinekit/bar.gleam", 141).
?DOC(
    " Set the gap between adjacent bars in user units. The per-bar\n"
    " width is derived from the total width, bar count, and this gap.\n"
    " Negative values are clamped to `0.0`.\n"
).
-spec with_bar_gap(builder(), float()) -> builder().
with_bar_gap(Builder, Gap) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        non_negative_float(Gap),
        erlang:element(10, Builder)}.

-file("src/sparklinekit/bar.gleam", 149).
?DOC(
    " Set the corner radius in user units. The renderer clamps the\n"
    " radius to half the smaller side of each individual bar so the\n"
    " shape stays a rectangle / capsule rather than turning into a\n"
    " circle.\n"
).
-spec with_corner_radius(builder(), float()) -> builder().
with_corner_radius(Builder, Radius) ->
    {builder,
        erlang:element(2, Builder),
        erlang:element(3, Builder),
        erlang:element(4, Builder),
        erlang:element(5, Builder),
        erlang:element(6, Builder),
        erlang:element(7, Builder),
        erlang:element(8, Builder),
        erlang:element(9, Builder),
        non_negative_float(Radius)}.

-file("src/sparklinekit/bar.gleam", 486).
-spec escape_attribute(binary()) -> binary().
escape_attribute(Value) ->
    _pipe = Value,
    _pipe@1 = gleam@string:replace(_pipe, <<"&"/utf8>>, <<"&amp;"/utf8>>),
    _pipe@2 = gleam@string:replace(_pipe@1, <<"\""/utf8>>, <<"&quot;"/utf8>>),
    _pipe@3 = gleam@string:replace(_pipe@2, <<"<"/utf8>>, <<"&lt;"/utf8>>),
    gleam@string:replace(_pipe@3, <<">"/utf8>>, <<"&gt;"/utf8>>).

-file("src/sparklinekit/bar.gleam", 223).
-spec background_rect(integer(), integer(), binary()) -> gleam@string_tree:string_tree().
background_rect(Width, Height, Background) ->
    case (Background =:= <<"none"/utf8>>) orelse (Background =:= <<""/utf8>>) of
        true ->
            gleam@string_tree:new();

        false ->
            _pipe = gleam@string_tree:new(),
            _pipe@1 = gleam@string_tree:append(
                _pipe,
                <<"<rect x=\"0\" y=\"0\" width=\""/utf8>>
            ),
            _pipe@2 = gleam@string_tree:append(
                _pipe@1,
                erlang:integer_to_binary(Width)
            ),
            _pipe@3 = gleam@string_tree:append(_pipe@2, <<"\" height=\""/utf8>>),
            _pipe@4 = gleam@string_tree:append(
                _pipe@3,
                erlang:integer_to_binary(Height)
            ),
            _pipe@5 = gleam@string_tree:append(_pipe@4, <<"\" fill=\""/utf8>>),
            _pipe@6 = gleam@string_tree:append(
                _pipe@5,
                escape_attribute(Background)
            ),
            gleam@string_tree:append(_pipe@6, <<"\"/>"/utf8>>)
    end.

-file("src/sparklinekit/bar.gleam", 494).
-spec parse_string_colour(binary(), binary()) -> sparklinekit@internal@color:rgba().
parse_string_colour(Value, Theme_fallback) ->
    case sparklinekit@internal@color:parse_hex(Value) of
        {ok, Rgba} ->
            Rgba;

        {error, _} ->
            sparklinekit@internal@color:parse_or(
                Theme_fallback,
                {rgba, 0, 0, 0, 255}
            )
    end.

-file("src/sparklinekit/bar.gleam", 501).
-spec parse_string_colour_or(binary(), sparklinekit@internal@color:rgba()) -> sparklinekit@internal@color:rgba().
parse_string_colour_or(Value, Fallback) ->
    case sparklinekit@internal@color:parse_hex(Value) of
        {ok, Rgba} ->
            Rgba;

        {error, _} ->
            Fallback
    end.

-file("src/sparklinekit/bar.gleam", 68).
?DOC(
    " Start a new bar sparkline builder.\n"
    "\n"
    " Defaults: 200x40 viewBox, `currentColor` fill, the default\n"
    " theme's negative colour (`#EF4444`) for bars below the zero\n"
    " baseline, 1.0px gap between bars, no rounded corners, no\n"
    " background.\n"
).
-spec new(list(float())) -> builder().
new(Values) ->
    Base = sparklinekit@theme:default(),
    {builder,
        Values,
        Base,
        sparklinekit@theme:foreground(Base),
        sparklinekit@theme:background(Base),
        {some, sparklinekit@theme:negative(Base)},
        200,
        40,
        1.0,
        +0.0}.

-file("src/sparklinekit/bar.gleam", 84).
?DOC(" Start a builder from a list of `Int` values.\n").
-spec new_ints(list(integer())) -> builder().
new_ints(Values) ->
    new(gleam@list:map(Values, fun erlang:float/1)).

-file("src/sparklinekit/bar.gleam", 403).
?DOC(
    " Position a 1-pixel hairline anchored at the baseline, growing\n"
    " *into* the canvas. Positive (or zero) values prefer to extend\n"
    " above the baseline; negative values prefer below. Either\n"
    " preference flips when the baseline itself sits within\n"
    " `min_visible_height` of the corresponding canvas edge — for\n"
    " example, all-negative data pins the baseline to the top edge,\n"
    " so a zero-value hairline must grow downward (into the canvas)\n"
    " rather than upward (outside it).\n"
).
-spec hairline_at_baseline(float(), float(), float()) -> {float(), float()}.
hairline_at_baseline(Value, Baseline_y, Height_f) ->
    Above_fits = Baseline_y >= 1.0,
    Below_fits = (Height_f - Baseline_y) >= 1.0,
    case {Value >= +0.0, Above_fits, Below_fits} of
        {true, true, _} ->
            {Baseline_y - 1.0, 1.0};

        {true, false, _} ->
            {Baseline_y, 1.0};

        {false, _, true} ->
            {Baseline_y, 1.0};

        {false, _, false} ->
            {Baseline_y - 1.0, 1.0}
    end.

-file("src/sparklinekit/bar.gleam", 379).
?DOC(
    " Resolve the SVG/PNG rect dimensions for a single bar in a\n"
    " non-degenerate (mixed-sign or simple positive) layout. Falls back\n"
    " to a 1-pixel hairline anchored at the baseline when the raw rect\n"
    " height would be zero, so a value of exactly zero still leaves a\n"
    " visible mark.\n"
).
-spec rect_dimensions(float(), float(), float(), float()) -> {float(), float()}.
rect_dimensions(Value, Y_value, Baseline_y, Height_f) ->
    {Rect_y, Rect_h} = case Value >= +0.0 of
        true ->
            {Y_value, Baseline_y - Y_value};

        false ->
            {Baseline_y, Y_value - Baseline_y}
    end,
    case Rect_h > 1.0 of
        true ->
            {Rect_y, Rect_h};

        false ->
            hairline_at_baseline(Value, Baseline_y, Height_f)
    end.

-file("src/sparklinekit/bar.gleam", 301).
-spec compute_layout(list(float()), integer(), integer(), float()) -> layout().
compute_layout(Values, Width, Height, Gap) ->
    Values@1 = gleam@list:map(
        Values,
        fun sparklinekit@internal@scale:clamp_finite/1
    ),
    Width_f = erlang:float(Width),
    Height_f = erlang:float(Height),
    Count = erlang:length(Values@1),
    Step = case erlang:float(Count) of
        +0.0 -> +0.0;
        -0.0 -> -0.0;
        Gleam@denominator -> Width_f / Gleam@denominator
    end,
    Bar_w = case (Step - Gap) > +0.0 of
        true ->
            Step - Gap;

        false ->
            Step
    end,
    {Lo, Hi} = sparklinekit@internal@scale:min_max(Values@1),
    {Y_min, Y_max} = effective_range(Lo, Hi),
    Span = Y_max - Y_min,
    case (Lo =:= Hi) orelse (Span =< +0.0) of
        true ->
            Half = Height_f / 2.0,
            {Rects, _} = gleam@list:fold(
                Values@1,
                {[], 0},
                fun(Acc, Value) ->
                    {Rs, I} = Acc,
                    X = erlang:float(I) * Step,
                    {[{rect, X, Half, Bar_w, Half, Value} | Rs], I + 1}
                end
            ),
            {layout, lists:reverse(Rects)};

        false ->
            Baseline_y = baseline_y(Height_f, Y_min, Span),
            {Rects@1, _} = gleam@list:fold(
                Values@1,
                {[], 0},
                fun(Acc@1, Value@1) ->
                    {Rs@1, I@1} = Acc@1,
                    X@1 = erlang:float(I@1) * Step,
                    Y_value = y_coord(Value@1, Y_min, Span, Height_f),
                    {Safe_y, Safe_h} = rect_dimensions(
                        Value@1,
                        Y_value,
                        Baseline_y,
                        Height_f
                    ),
                    {[{rect, X@1, Safe_y, Bar_w, Safe_h, Value@1} | Rs@1],
                        I@1 + 1}
                end
            ),
            {layout, lists:reverse(Rects@1)}
    end.

-file("src/sparklinekit/bar.gleam", 242).
-spec rect_body(
    list(float()),
    integer(),
    integer(),
    binary(),
    binary(),
    float(),
    float()
) -> gleam@string_tree:string_tree().
rect_body(Values, Width, Height, Color, Negative_color, Gap, Radius) ->
    case Values of
        [] ->
            gleam@string_tree:new();

        _ ->
            Layout = compute_layout(Values, Width, Height, Gap),
            Pos_fill = escape_attribute(Color),
            Neg_fill = escape_attribute(Negative_color),
            gleam@list:fold(
                erlang:element(2, Layout),
                gleam@string_tree:new(),
                fun(Tree, Rect) ->
                    Fill = case erlang:element(6, Rect) < +0.0 of
                        true ->
                            Neg_fill;

                        false ->
                            Pos_fill
                    end,
                    gleam@string_tree:append(
                        Tree,
                        render_rect_svg(Rect, Fill, Radius)
                    )
                end
            )
    end.

-file("src/sparklinekit/bar.gleam", 154).
?DOC(" Render the builder to a self-contained `<svg>` element string.\n").
-spec to_svg(builder()) -> binary().
to_svg(Builder) ->
    {builder,
        Values,
        _,
        Color,
        Background,
        Negative_color,
        Width,
        Height,
        Gap,
        Radius} = Builder,
    Negative_color@1 = case Negative_color of
        {some, C} ->
            C;

        none ->
            Color
    end,
    Bg_layer = background_rect(Width, Height, Background),
    Bars = rect_body(
        Values,
        Width,
        Height,
        Color,
        Negative_color@1,
        Gap,
        Radius
    ),
    _pipe = gleam@string_tree:new(),
    _pipe@1 = gleam@string_tree:append(
        _pipe,
        <<"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\""/utf8>>
    ),
    _pipe@2 = gleam@string_tree:append(_pipe@1, erlang:integer_to_binary(Width)),
    _pipe@3 = gleam@string_tree:append(_pipe@2, <<"\" height=\""/utf8>>),
    _pipe@4 = gleam@string_tree:append(
        _pipe@3,
        erlang:integer_to_binary(Height)
    ),
    _pipe@5 = gleam@string_tree:append(_pipe@4, <<"\" viewBox=\"0 0 "/utf8>>),
    _pipe@6 = gleam@string_tree:append(_pipe@5, erlang:integer_to_binary(Width)),
    _pipe@7 = gleam@string_tree:append(_pipe@6, <<" "/utf8>>),
    _pipe@8 = gleam@string_tree:append(
        _pipe@7,
        erlang:integer_to_binary(Height)
    ),
    _pipe@9 = gleam@string_tree:append(
        _pipe@8,
        <<"\" preserveAspectRatio=\"none\">"/utf8>>
    ),
    _pipe@10 = gleam_stdlib:iodata_append(_pipe@9, Bg_layer),
    _pipe@11 = gleam_stdlib:iodata_append(_pipe@10, Bars),
    _pipe@12 = gleam@string_tree:append(_pipe@11, <<"</svg>"/utf8>>),
    unicode:characters_to_binary(_pipe@12).

-file("src/sparklinekit/bar.gleam", 418).
-spec paint_bars(
    sparklinekit@internal@raster:canvas(),
    list(float()),
    integer(),
    integer(),
    sparklinekit@internal@color:rgba(),
    sparklinekit@internal@color:rgba(),
    float(),
    float()
) -> sparklinekit@internal@raster:canvas().
paint_bars(Canvas, Values, Width, Height, Fg, Neg, Gap, Radius) ->
    Layout = compute_layout(Values, Width, Height, Gap),
    gleam@list:fold(
        erlang:element(2, Layout),
        Canvas,
        fun(C, Rect) ->
            Colour = case erlang:element(6, Rect) < +0.0 of
                true ->
                    Neg;

                false ->
                    Fg
            end,
            case erlang:element(5, Rect) =< +0.0 of
                true ->
                    C;

                false ->
                    sparklinekit@internal@raster:fill_rounded_rect(
                        C,
                        erlang:element(2, Rect),
                        erlang:element(3, Rect),
                        erlang:element(4, Rect),
                        erlang:element(5, Rect),
                        Radius,
                        Colour
                    )
            end
        end
    ).

-file("src/sparklinekit/bar.gleam", 197).
?DOC(
    " Render the builder to PNG bytes (8-bit RGBA truecolor). The\n"
    " viewBox dimensions double as the pixel size.\n"
    "\n"
    " The PNG IDAT payload is written using DEFLATE's uncompressed\n"
    " \"store\" blocks (no Huffman coding) so the encoder stays pure\n"
    " Gleam with zero FFI. As a result the output is roughly\n"
    " `width * height * 4` bytes regardless of how uniform the image\n"
    " is — for visual size context, prefer SVG.\n"
).
-spec to_png(builder()) -> bitstring().
to_png(Builder) ->
    {builder,
        Values,
        Theme,
        Color,
        Background,
        Negative_color,
        Width,
        Height,
        Gap,
        Radius} = Builder,
    Fg = parse_string_colour(Color, sparklinekit@theme:foreground(Theme)),
    Neg = case Negative_color of
        {some, C} ->
            parse_string_colour(C, sparklinekit@theme:negative(Theme));

        none ->
            Fg
    end,
    Bg = case Background of
        <<"none"/utf8>> ->
            {rgba, 0, 0, 0, 0};

        Other ->
            parse_string_colour_or(Other, {rgba, 0, 0, 0, 0})
    end,
    Canvas = sparklinekit@internal@raster:new(Width, Height, Bg),
    Canvas@1 = paint_bars(Canvas, Values, Width, Height, Fg, Neg, Gap, Radius),
    sparklinekit@internal@png:encode(
        sparklinekit@internal@raster:to_grid(Canvas@1),
        Width,
        Height
    ).