-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>>, <<"&"/utf8>>),
_pipe@2 = gleam@string:replace(_pipe@1, <<"\""/utf8>>, <<"""/utf8>>),
_pipe@3 = gleam@string:replace(_pipe@2, <<"<"/utf8>>, <<"<"/utf8>>),
gleam@string:replace(_pipe@3, <<">"/utf8>>, <<">"/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
).