Skip to main content

lib/pdf/component/stat_card.ex

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