Skip to main content

lib/pdf/component/timeline.ex

defmodule Pdf.Component.Timeline do
  @moduledoc """
  Timeline component for PDF documents.

  Renders a vertical timeline with dots, connecting line, and event entries.
  Useful for CVs, project history, and changelogs.

  ## Examples

      doc |> Pdf.Component.Timeline.render({50, 700}, %{}, [
        %{date: "2026", title: "Launch", description: "Product released"},
        %{date: "2025", title: "Beta", description: "Beta testing phase"},
        %{date: "2024", title: "Founded", description: "Company started"}
      ])
  """

  @default_font "Helvetica"
  @default_font_size 10
  @default_color {0.1, 0.1, 0.1}
  @default_date_color {0.45, 0.45, 0.45}
  @default_line_color {0.8, 0.8, 0.8}
  @default_dot_color {0.2, 0.5, 0.9}
  @default_dot_size 6
  @default_row_height 50
  @default_date_width 60

  @doc """
  Render a timeline at `{x, y}`.

  ## Style options

  - `:font` — font name (default `"Helvetica"`)
  - `:font_size` — text size (default `10`)
  - `:color` — title/description color
  - `:date_color` — date text color
  - `:line_color` — vertical line color
  - `:dot_color` — dot fill color
  - `:dot_size` — dot diameter (default `6`)
  - `:row_height` — height per entry (default `50`)
  - `:date_width` — width reserved for dates (default `60`)

  ## Events format

  List of maps: `%{date: "2026", title: "Event", description: "Details"}`
  """
  def render(doc, {x, y}, style \\ %{}, events) do
    font = Map.get(style, :font, @default_font)
    font_size = Map.get(style, :font_size, @default_font_size)
    color = Map.get(style, :color, @default_color)
    date_color = Map.get(style, :date_color, @default_date_color)
    line_color = Map.get(style, :line_color, @default_line_color)
    dot_color = Map.get(style, :dot_color, @default_dot_color)
    dot_size = Map.get(style, :dot_size, @default_dot_size)
    row_height = Map.get(style, :row_height, @default_row_height)
    date_width = Map.get(style, :date_width, @default_date_width)

    dot_r = dot_size / 2
    line_x = x + date_width + dot_r
    text_x = line_x + dot_size + 8
    count = length(events)

    # Vertical line
    first_y = y - dot_r
    last_y = y - (count - 1) * row_height - dot_r

    doc =
      doc
      |> Pdf.save_state()
      |> Pdf.set_stroke_color(line_color)
      |> Pdf.set_line_width(1.5)
      |> Pdf.line({line_x, first_y}, {line_x, last_y})
      |> Pdf.stroke()
      |> Pdf.restore_state()

    # Events
    events
    |> Enum.with_index()
    |> Enum.reduce(doc, fn {event, i}, d ->
      ey = y - i * row_height
      dot_cy = ey - dot_r

      # Dot
      d =
        d
        |> Pdf.save_state()
        |> Pdf.set_fill_color(dot_color)
        |> Pdf.rounded_rectangle(
          {line_x - dot_r, dot_cy - dot_r},
          {dot_size, dot_size},
          dot_r
        )
        |> Pdf.fill()
        |> Pdf.restore_state()

      # Date
      date = Map.get(event, :date, "")
      d =
        d
        |> Pdf.set_font(font, font_size - 1)
        |> Pdf.set_fill_color(date_color)
        |> Pdf.text_at({x, dot_cy - 2}, date)

      # Title
      title = Map.get(event, :title, "")
      d =
        d
        |> Pdf.set_font(font, font_size, bold: true)
        |> Pdf.set_fill_color(color)
        |> Pdf.text_at({text_x, ey - 2}, title)

      # Description
      desc = Map.get(event, :description, "")
      if desc != "" do
        d
        |> Pdf.set_font(font, font_size - 1)
        |> Pdf.set_fill_color(date_color)
        |> Pdf.text_at({text_x, ey - 16}, desc)
      else
        d
      end
    end)
  end
end