-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, <<<<<<<<<<<<<<<<<<<<<<<<">/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, <<"> ), _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 `` 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, <<"> ), _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, <<""/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 ).