epessine/axis
Axis is a modern, ultra-simple and flexible Laravel library for drawing charts entirely in PHP—no JavaScript boilerplate. It's built for Livewire and supports Chart.js, ApexCharts and Highcharts out of the box, with no configuration hell. Just install and draw.
Architecture fit. Axis is a translation layer, not a renderer. It takes a fluent PHP builder (Chart::chartjs()->bar()->labels(...)->series(...)) and emits native configuration for Chart.js, ApexCharts, or Highcharts through a Blade render ({{ $chart }}). For a Laravel monolith this is the correct seam: it removes hand-maintained JS wiring from the critical path without adding a build step, a rendering engine, or a proprietary runtime. The genuinely differentiated piece is the #[Axis] Livewire attribute — charts survive Livewire re-renders with instance and state intact instead of being torn down and rebuilt. That specific problem is what teams currently hand-roll badly, and it is where the package earns its keep.
Abstraction ceiling. Two escape hatches bound the risk: raw native config arrays (Chart::chartjs([...])) and Script::from() for scriptable JS-function properties. In practice the abstraction is a convenience over a subset of each library's surface, not a complete model of it. The number that matters is escape-hatch rate: if more than roughly 25–30% of a roadmap's charts need raw config, you are paying an indirection tax to write JS config inside PHP strings and the value proposition inverts.
Integration feasibility. Low friction. composer require, optionally publish config, render in Blade or add the attribute in a Livewire component. The compatibility matrix is broad and current — Laravel 10–13, Livewire 3–4, PHP 8.2–8.5 — and v0.8.0 shipped ahead of ecosystem majors, which is a positive signal on maintenance intent but also means much of that matrix is untested by a real user base (0 dependents). A single Laravel developer can produce a working chart in under an hour; that is the whole pitch and it holds.
Technical risks, ranked.
series param refactor, v0.6.0 IsFluent → AsAxisChart trait rename). Caret constraints are unsafe; pin exact versions.#[Axis] implies reliance on Livewire internals. A Livewire major is the most likely single-event breakage, and it lands on the framework upgrade path you cannot avoid.Script::from() injection surface. It writes JS functions into config. If any chart config is influenced by user or tenant input, that is an XSS vector. Hard rule required: never interpolate untrusted data into Script::from().Key questions to resolve before committing.
Stack fit. Strong only under specific conditions: Laravel with server-rendered Blade and/or Livewire, a PHP-centric team, no existing design-system chart component, and charting surfaces that are internal or low-stakes. If the frontend is Inertia/React/Vue, or a house charting standard already exists, Axis creates a second divergent charting path and the trade turns negative. Confirm this filter first — it decides everything downstream.
Library choice stays separable. Adopt Axis and the Chart.js/ApexCharts/Highcharts decision in the same breath, but keep them decoupled in the plan. Default to Chart.js on license grounds; treat ApexCharts and Highcharts as opt-in per surface with explicit approval for Highcharts' commercial terms. This preserves the ability to drop Axis and keep the library, or swap libraries without touching the wrapper seam.
Dependency hygiene.
"epessine/axis": "0.8.0", not ^0.8). Given minor-version breaking changes, non-negotiable.composer.json and verify the package's claimed matrix in your own CI job rather than trusting the README.Isolation seam. Wrap all chart construction behind one internal helper or facade (App\Support\Charts\ChartFactory) plus a single Blade component. Every chart goes through it. That makes the dependency a one-file swap if you fork or replace it, gives you one place to enforce the "no untrusted data in Script::from()" rule, and gives you one place for cross-cutting concerns (empty states, telemetry, default palette). Keep every chart's configuration expressible as a plain array so exit is copy-paste.
Migration path. No migration — greenfield adoption. The realistic path is to prove on one net-new surface and only retrofit existing charts where a surface is already being rewritten. Do not schedule a "convert our charts to Axis" epic; the ROI is in new work, and retrofits are where you discover the abstraction gaps.
Sequencing.
Maintenance. Ongoing cost is low in volume and high in concentration. The API surface is small and upgrades are infrequent (roughly one minor per framework major), but you are your own vendor: when a Livewire upgrade breaks #[Axis], there is no one to escalate to. Budget a named owner and a standing half-day per framework major upgrade cycle to validate chart rendering, plus a documented fork/vendor procedure. Weighed against the alternative — 1–3 developer-days per charting surface plus permanent JS maintenance — the arithmetic usually favors Axis for a PHP-heavy team, provided the fork plan exists.
Support. Self-support only. No vendor, no SLA, no security-response commitment. Mitigations: read the source (it is small), subscribe to repo releases and issues, and keep the wrapper seam thin enough that debugging never requires understanding the whole package. Docs exist and are reasonable for a beta, but expect gaps and treat the source as the spec.
Scaling. Rendering is client-side, so scale concerns are payload and round-trip, not compute. Watch three vectors: charts per page (each is an independent library instance with its own canvas and JS cost), dataset size serialized into the Blade payload (large series bloat HTML and the Livewire re-render diff), and Livewire round-trip frequency on filterable dashboards (a date-range picker re-emitting five charts can dominate perceived latency). Mitigations are conventional — server-side aggregation and downsampling, lazy-load below-the-fold charts, debounce filters, paginate drill-downs — but they are decisions someone must make, and Axis does not make them for you.
Failure modes. The dangerous ones are silent: version skew between the PHP package and the loaded JS library, a Script::from() payload that parses server-side but throws client-side, or a Livewire re-render that orphans a chart instance. All three present as a blank div with a clean server log. Countermeasures: a Playwright smoke test asserting canvas presence on your top charts, client-side error reporting wired into your existing frontend telemetry, and a consistent empty-state in the shared Blade component so a failed chart never masquerades as "no data."
Ramp-up. Near zero for anyone who writes Laravel. The mental model is one fluent builder plus one Blade include, and a mid-level developer is productive in a day. The skill that actually needs training is judgment: knowing when to drop to raw config, and knowing never to feed user input into Script::from(). Encode both in the factory seam and a short internal README rather than in tribal knowledge.
How can I help you explore Laravel packages today?