defmodule Pdf.Component.StatCard do
@moduledoc """
Stat card component for PDF documents.
Renders a dashboard-style KPI card with a large number/value,
a label, and optional trend indicator. Useful for reports and dashboards.
## Examples
doc |> Pdf.Component.StatCard.render({50, 700}, %{
value: "$12,450",
label: "Monthly Revenue",
width: 150
})
doc |> Pdf.Component.StatCard.render({50, 700}, %{
value: "98.5%",
label: "Uptime",
trend: "+2.1%",
trend_color: {0.2, 0.7, 0.3},
accent_color: {0.2, 0.5, 0.9}
})
"""
@default_width 150
@default_height 90
@default_background {1.0, 1.0, 1.0}
@default_accent_color {0.2, 0.5, 0.9}
@default_value_color {0.1, 0.1, 0.1}
@default_label_color {0.5, 0.5, 0.5}
@default_border_radius 6
@default_font "Helvetica"
@doc """
Render a stat card at `{x, y}` (top-left).
## Style options
- `:value` — the main number/text (required)
- `:label` — description below the value
- `:trend` — optional trend string (e.g. "+5.2%")
- `:trend_color` — color for the trend text
- `:width` — card width (default `150`)
- `:height` — card height (default `90`)
- `:background` — card background (default white)
- `:accent_color` — top accent bar color (default blue)
- `:value_color` — value text color
- `:label_color` — label text color
- `:border_radius` — corner radius (default `6`)
- `:border` — border width (default `0.5`)
- `:border_color` — border color
"""
def render(doc, {x, y}, style \\ %{}) do
value = Map.get(style, :value, "0")
label = Map.get(style, :label, "")
trend = Map.get(style, :trend)
trend_color = Map.get(style, :trend_color, {0.2, 0.7, 0.3})
width = Map.get(style, :width, @default_width)
height = Map.get(style, :height, @default_height)
bg = Map.get(style, :background, @default_background)
accent = Map.get(style, :accent_color, @default_accent_color)
value_color = Map.get(style, :value_color, @default_value_color)
label_color = Map.get(style, :label_color, @default_label_color)
radius = Map.get(style, :border_radius, @default_border_radius)
border_w = Map.get(style, :border, 0.5)
border_color = Map.get(style, :border_color, {0.88, 0.88, 0.88})
font = Map.get(style, :font, @default_font)
by = y - height
accent_h = 4
# Background
doc =
doc
|> Pdf.save_state()
|> Pdf.set_fill_color(bg)
|> Pdf.rounded_rectangle({x, by}, {width, height}, radius)
|> Pdf.fill()
|> Pdf.restore_state()
# Top accent bar
doc =
doc
|> Pdf.save_state()
|> Pdf.set_fill_color(accent)
|> Pdf.rectangle({x + 1, y - accent_h}, {width - 2, accent_h})
|> Pdf.fill()
|> Pdf.restore_state()
# Border
doc =
if border_w > 0 do
doc
|> Pdf.save_state()
|> Pdf.set_stroke_color(border_color)
|> Pdf.set_line_width(border_w)
|> Pdf.rounded_rectangle({x, by}, {width, height}, radius)
|> Pdf.stroke()
|> Pdf.restore_state()
else
doc
end
# Value (large)
value_size = if String.length(value) > 8, do: 18, else: 22
doc =
doc
|> Pdf.set_font(font, value_size, bold: true)
|> Pdf.set_fill_color(value_color)
|> Pdf.text_at({x + 12, y - accent_h - value_size - 6}, value)
# Trend (next to value)
doc =
if trend do
value_w = String.length(value) * value_size * 0.55
doc
|> Pdf.set_font(font, 10)
|> Pdf.set_fill_color(trend_color)
|> Pdf.text_at({x + 12 + value_w + 6, y - accent_h - value_size - 4}, trend)
else
doc
end
# Label
doc
|> Pdf.set_font(font, 9)
|> Pdf.set_fill_color(label_color)
|> Pdf.text_at({x + 12, by + 12}, label)
end
end