Packages

Server-side SVG and canvas visualization for Phoenix LiveView, in the shape of D3.

Current section

Files

Jump to
visualize mix.exs
Raw

mix.exs

defmodule Visualize.MixProject do
  use Mix.Project

  # The version between releases carries `-dev`, so no commit claims to be the release it
  # merely follows. `~/Projects/claude-tools/release.exs` moves this attribute — it edits
  # `@version`, not an inline `version:` — and RELEASING.md describes the lifecycle.
  @version "0.2.25"

  def project do
    [
      app: :visualize,
      version: @version,
      elixir: "~> 1.19",
      start_permanent: Mix.env() == :prod,
      elixirc_paths: elixirc_paths(Mix.env()),
      name: "Visualize",
      description:
        "Server-side SVG and canvas visualization for Phoenix LiveView, in the shape of D3.",
      # The public home (#516): GitLab push-mirrors main and the tags here.
      source_url: "https://github.com/dcoai/Visualize",
      package: package(),
      docs: docs(),
      aliases: [
        "vis.verify": &verify_models/1,
        # Every gate CI runs, in CI's order; see scripts/check.exs.
        "vis.check": ["cmd elixir scripts/check.exs"]
      ],
      test_coverage: [summary: [threshold: 0]],
      deps: deps()
    ]
  end

  # Only `lib/` reaches a consumer. `audit/` holds the surface scanner and its mix tasks
  # (dev and test); `test/support` compiles for tests alone.
  defp elixirc_paths(:test), do: ["lib", "audit", "specs", "test/support"]
  defp elixirc_paths(:dev), do: ["lib", "audit", "specs"]
  defp elixirc_paths(_), do: ["lib"]

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:nx, "~> 0.9", optional: true},
      # `Visualize.Chart.to_json/1` and `from_json/1` (spec/14 §11): optional like Nx, so a
      # consumer without Jason builds silently and the two functions return an error value.
      {:jason, "~> 1.4", optional: true},
      # `Data.Table.rows/1` (spec/08 §6) reads Explorer frames, and anything else that
      # implements `Table.Reader`, through the protocol. The package is the protocol and
      # two impls, optional like Nx: without it lists, column maps and tensors still work
      # and a struct source raises at the call (D-52).
      {:table, "~> 0.1.2", optional: true},
      # Explorer frames in the `Data.Table` tests only; scripts/consumer_check.exs proves
      # neither Explorer nor `Table.Reader` reaches a consumer.
      {:explorer, "~> 0.12", only: :test},
      # A time zone database for the zoned time scale's tests (spec/03 §7.5, #447). The library
      # reads the host's `Calendar.TimeZoneDatabase` and takes no tz dependency (D-115); the
      # tests configure this one in test/test_helper.exs. `only:` is load-bearing: test deps
      # are not transitive, and scripts/consumer_check.exs proves `Tz` never reaches a consumer.
      {:tz, "~> 0.28", only: :test},
      # The LiveView components (spec/10 §2-3) compile only when `Phoenix.Component` is
      # loaded. `optional:` is load-bearing, and `only:` would not do: Mix prunes the code
      # path to a dependency's declared deps while compiling it, so a guard over an
      # undeclared Phoenix never fires in a host. A host without LiveView never fetches it;
      # scripts/consumer_check.exs proves that on every pipeline (D-45).
      {:phoenix_live_view, "~> 1.0", optional: true},
      # `mix vis.surface` (spec/12 §2): the source-scan primitives and the golden renderer
      # shared with extc and extla. `only:` is load-bearing: dev/test deps are not transitive,
      # so nothing here reaches a consumer of visualize.
      {:surfex, "~> 0.6.2", only: [:dev, :test], runtime: false},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false},
      # Property tests over the scale protocol (spec/03 §1.3; test/visualize/scale/).
      {:stream_data, "~> 1.4", only: [:dev, :test]},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      # `mix vis.verify` (spec/12 §5): the model of the force simulation. Pinned by tag, with
      # the lock entry committed, so CI and every workstation build the same extla (#441).
      {:extla, extla_source() ++ [only: [:dev, :test], runtime: false]}
    ]
  end

  # A local extla checkout is an explicit opt-in (`EXTLA_PATH=../extla`), never inferred
  # from a sibling directory: that is how an unannounced rename reached the build.
  defp extla_source do
    case System.get_env("EXTLA_PATH") do
      nil -> [git: "git@gitlab.conet.yarina.org:dco-tek/extla.git", tag: "v0.4.0"]
      path -> [path: path]
    end
  end

  # Every model, checked one at a time. `Mix.Task.rerun/2`, not a list alias: Mix remembers
  # a task by name and silently skips it the second time.
  @models ~w(Visualize.Specs.ForceSimulation)
  defp verify_models(_args), do: Enum.each(@models, &Mix.Task.rerun("extla.check", [&1]))

  # The guides ship with the package, as hexdocs extras a reader starts from. The
  # specification, the surface golden and the changelog ship with it too: they are
  # the contract a consumer holds the library to, and the record of what an upgrade brings,
  # not internal documentation. RELEASING.md is deliberately absent — it is a maintainer
  # document, and nothing that ships may link to a file that does not.
  defp package do
    [
      files:
        ~w(lib guides spec mix.exs README.md LICENSE CHANGELOG.md usage-rules.md API_SURFACE.md .formatter.exs),
      licenses: ["MIT"],
      links: %{
        "GitHub" => "https://github.com/dcoai/Visualize",
        "Changelog" => "https://hexdocs.pm/visualize/changelog.html"
      }
    ]
  end

  # The references the specification makes on purpose to functions that are internal
  # (behaviour callbacks; the family modules behind `Geo.Projection` and `CanvasBinary`; the
  # one module that calls the resvg binary, spec/09 §9) or that the decisions log
  # records as removed or as never having existed (D-2, D-13).
  @internal_prefixes [
    "Visualize.Backend.SVG.",
    "Visualize.Backend.Canvas.",
    "Visualize.Backend.CanvasBinary.SVG.",
    "Visualize.Backend.CanvasBinary.PathEncoder.",
    "Visualize.Backend.CanvasBinary.Opcodes.",
    "Visualize.Render.Raster"
  ]
  @recorded_absent [
    "Visualize.Geo.Projection.family/1",
    "Visualize.Render.with_backend/2",
    "Visualize.Scale.scale/2",
    "Visualize.Scale.color/0",
    "Visualize.Scale.scheme/2"
  ]

  @doc false
  def internal_reference?(reference) do
    reference in @recorded_absent or
      Enum.any?(@internal_prefixes, &String.starts_with?(reference, &1))
  end

  # `spec/README.md` gets an explicit filename: two README.md extras otherwise collide.
  defp docs do
    [
      main: "readme",
      # The tag of the version being documented, which the mirror carries: a "View source"
      # link opens the code that was published, not main.
      source_ref: "v#{@version}",
      # The specification refers, on purpose, to internal functions (behaviour callbacks and
      # the family modules behind `Geo.Projection` and `CanvasBinary`) and to functions the
      # decisions log records as removed or as called by code that has never run (D-2, D-13).
      # Those references are the document's content, not broken links.
      skip_code_autolink_to: &__MODULE__.internal_reference?/1,
      extras:
        [
          "README.md",
          # The guides, in reading order, ahead of the reference material: where a newcomer
          # starts, then the declarative layer, then the one-page summary.
          "guides/getting_started.md",
          "guides/liveview.md",
          "guides/designing_charts.md",
          "guides/cheatsheet.cheatmd",
          "usage-rules.md",
          # The changelog ships in `files:`, so the README's Installation section links it
          # as the reference for moving a pinned tag. A link from a shipping document has
          # to resolve in the published docs too, which means being an extra.
          "CHANGELOG.md",
          {"spec/README.md", filename: "specification", title: "Specification index"}
          | Path.wildcard("spec/[0-9]*.md")
        ] ++ ["API_SURFACE.md"],
      # ExDoc lists ungrouped extras first, so the README alone stays ungrouped and the
      # guides follow it, ahead of the reference documents.
      groups_for_extras: [
        Guides: ~r"guides/",
        Reference: ["usage-rules.md", "CHANGELOG.md"],
        Specification: ~r"spec/",
        Conformance: ["API_SURFACE.md"]
      ],
      groups_for_modules: [
        Facade: [Visualize],
        "Declarative chart": ~r"Visualize\.Chart",
        Scales: ~r"Visualize\.Scale",
        Shapes: ~r"Visualize\.Shape",
        "Axes and formatting": [Visualize.Axis, Visualize.Format],
        Layouts: ~r"Visualize\.Layout",
        Geo: [~r"Visualize\.Geo", Visualize.Polygon, ~r"Visualize\.Contour"],
        Utilities: [
          Visualize.Color,
          Visualize.Interpolate,
          Visualize.Ease,
          Visualize.Random,
          Visualize.Data,
          Visualize.Data.Table
        ],
        "IR and rendering": [
          ~r"Visualize\.IR",
          ~r"Visualize\.Backend",
          Visualize.Render,
          Visualize.Incremental,
          Visualize.Benchmark
        ],
        "SVG elements": ~r"Visualize\.SVG",
        LiveView: [~r"Visualize\.Components", ~r"Visualize\.Hooks"]
      ]
    ]
  end
end