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