Analytics & Charts
SvelteForge Admin uses LayerChart v2 for all data visualization. LayerChart is a D3-based charting library purpose-built for Svelte — it is fully reactive, composable, and SSR-compatible out of the box. Combined with Svelte 5 runes and SvelteKit server-side data loading, the analytics system delivers fast, interactive charts with zero client-side data fetching.
Why LayerChart for Svelte
Unlike wrapper libraries that port React charting solutions, LayerChart was designed from the ground up for the Svelte ecosystem:
- Built specifically for Svelte — not a React port or framework-agnostic wrapper
- D3-based calculations with Svelte rendering — uses D3 for scales, shapes, and math while Svelte handles the DOM
- Composable chart components — Area, Bar, Pie, Line, and more can be combined freely
- Fully reactive with Svelte 5 runes — chart data updates automatically when
$stateor$derivedvalues change - SSR compatibility — works with SvelteKit's server-side rendering via a simple Vite configuration
Vite Configuration
LayerChart and its companion library svelte-ux must be listed in the ssr.noExternal array in your Vite config. Without this, SvelteKit's SSR will fail to process
these packages:
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
ssr: {
noExternal: ['layerchart', 'svelte-ux']
},
test: {
include: ['src/**/*.test.ts'],
}
}); Additionally, src/app.css includes @source directives so Tailwind CSS v4 scans
LayerChart and svelte-ux for utility classes:
@source "../node_modules/layerchart";
@source "../node_modules/svelte-ux"; Dashboard Charts
The main dashboard at /dashboard includes KPI cards with animated counters and
summary charts. The analytics page at /analytics provides deeper data exploration. Here
is what each chart visualizes:
| Chart Type | Component | Data |
|---|---|---|
| Area chart | AreaChart | User registrations over time (monthly) |
| Line chart | LineChart | Content creation trends (pages per month) |
| Donut chart | PieChart | Page status distribution (published / draft / archived) |
| Bar chart | BarChart | Top authors by page count |
Analytics Page
The /analytics route provides a grid-based layout with four chart panels. All data is
loaded server-side via +page.server.ts and passed to the Svelte 5 component through $props().
Chart Layout
Charts are arranged in a responsive grid using shadcn-svelte Card components:
- Top row (7-column grid): User Signups area chart (4 cols) + Content Creation line chart (3 cols)
- Bottom row (7-column grid): Pages by Status donut chart (3 cols) + Top Authors bar chart (4 cols)
Chart Implementation Patterns
PieChart (Donut) — Critical Pattern
LayerChart's PieChart uses direct props — not a config object. This is
the most common mistake when working with LayerChart:
<!-- CORRECT: Direct props -->
<PieChart
data={statusData}
value="value"
c="key"
cRange={statusColors}
innerRadius={0.6}
legend
>
{#snippet tooltip()}
<Chart.Tooltip nameKey="label" />
{/snippet}
</PieChart>
<!-- WRONG: Config objects do NOT work -->
<PieChart
data={statusData}
props={{ donut: { innerRadius: 0.6 }, legend: { show: true } }}
/> AreaChart / LineChart with Time Scales
<script lang="ts">
import { AreaChart } from "layerchart";
import { scaleUtc } from "d3-scale";
let { data } = $props();
const signupData = $derived(
data.signupsPerMonth.map((d) => ({
date: new Date(d.month),
signups: d.count,
}))
);
</script>
<AreaChart
data={signupData}
x="date"
xScale={scaleUtc()}
series={[{
key: "signups",
label: "User Signups",
color: "var(--chart-1)",
}]}
props={{
xAxis: {
format: (d: Date) =>
d.toLocaleDateString("en-US", { month: "short" }),
},
}}
>
{#snippet tooltip()}
<Chart.Tooltip />
{/snippet}
</AreaChart> BarChart with Band Scale
<script lang="ts">
import { BarChart } from "layerchart";
import { scaleBand } from "d3-scale";
</script>
<BarChart
data={data.topAuthors}
x="name"
xScale={scaleBand().padding(0.3)}
series={[{
key: "pageCount",
label: "Pages",
color: "var(--chart-5)",
}]}
>
{#snippet tooltip()}
<Chart.Tooltip />
{/snippet}
</BarChart> Chart Configuration Objects
SvelteForge uses Chart.ChartConfig from shadcn-svelte to define labels and colors for each
data series:
const statusConfig = {
published: { label: "Published", color: "var(--chart-1)" },
draft: { label: "Draft", color: "var(--chart-2)" },
archived: { label: "Archived", color: "var(--chart-4)" },
} satisfies Chart.ChartConfig; Color Tokens
Chart colors are defined as CSS custom properties in src/app.css using the OKLCH color
space. Both light and dark mode variants are provided:
| Token | Usage |
|---|---|
--chart-1 | Primary series (signups, published content) |
--chart-2 | Secondary series (drafts) |
--chart-3 | Tertiary series (content creation) |
--chart-4 | Quaternary series (archived content) |
--chart-5 | Quinary series (top authors) |
Dark Mode Re-rendering
LayerChart charts must re-render when the color mode changes so they pick up the updated CSS
custom properties. SvelteForge wraps each chart in a {#key mode.current} block using mode-watcher:
<script lang="ts">
import { mode } from "mode-watcher";
</script>
{#key mode.current}
<Chart.Container config={signupConfig} class="h-[300px] w-full">
<AreaChart ... />
</Chart.Container>
{/key} Important: With Svelte 5 and mode-watcher, use mode.current (a runes
object) — not $mode (the legacy store syntax).
Server-Side Data Loading
All chart data is aggregated on the server in +page.server.ts using Drizzle ORM queries
against the SQLite database. This ensures charts render with data on first paint — no loading spinners,
no client-side fetch waterfalls.
// src/routes/(app)/analytics/+page.server.ts
import { db } from "#lib/server/db/index.js";
import { users, pages, notifications } from "#lib/server/db/schema.js";
import { sql, eq } from "drizzle-orm";
export const load: PageServerLoad = async () => {
// User signups per month
const signupsPerMonth = await db
.select({
month: sql<string>`strftime('%Y-%m-01', created_at, 'unixepoch')`,
count: sql<number>`count(*)`,
})
.from(users)
.groupBy(sql`strftime('%Y-%m', created_at, 'unixepoch')`)
.orderBy(sql`strftime('%Y-%m', created_at, 'unixepoch')`);
// Pages by status (for pie/donut chart)
const pagesByStatus = await db
.select({
status: pages.status,
count: sql<number>`count(*)`,
})
.from(pages)
.groupBy(pages.status);
// Top authors by page count (for bar chart)
const topAuthors = await db
.select({
name: users.name,
pageCount: sql<number>`count(${pages.id})`,
})
.from(pages)
.innerJoin(users, eq(pages.authorId, users.id))
.groupBy(users.id)
.orderBy(sql`count(${pages.id}) desc`)
.limit(5);
return { signupsPerMonth, pagesByStatus, topAuthors };
}; Animated Counters
KPI cards on the dashboard use the AnimatedCounter component located at #lib/components/animated-counter.svelte. It animates from zero to the target value
using easeOutExpo easing for a satisfying deceleration effect.
<script lang="ts">
import AnimatedCounter from "#lib/components/animated-counter.svelte";
</script>
<!-- Basic usage -->
<AnimatedCounter value={1234} />
<!-- Custom duration and formatting -->
<AnimatedCounter
value={98.5}
duration={1200}
formatFn={(n) => n.toLocaleString("en-US") + "%"}
/> | Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Target number to animate to |
duration | number | 800 | Animation duration in milliseconds |
formatFn | (n: number) => string | n.toLocaleString() | Custom formatting function for the displayed value |
The component uses $effect (Svelte 5) to trigger the animation whenever value changes, and cleans up with cancelAnimationFrame on teardown.
Extending Analytics
Adding a New Chart
- Add the server query in
+page.server.ts— use Drizzle ORM withsqltemplate literals for aggregations - Import the chart component from
layerchart(e.g.,BarChart,AreaChart,PieChart) - Transform the data using
$derivedto map server data into the shape LayerChart expects - Define a ChartConfig with labels and
var(--chart-N)color tokens - Wrap in
Chart.Containerand include a{#snippet tooltip()}block for hover interactions
Using D3 Utilities
LayerChart works with standard D3 modules. The most commonly used in SvelteForge:
d3-scale—scaleUtc()for time axes,scaleBand()for categorical axesd3-array—extent(),max(),sum()for data calculationsd3-shape— arc generators, curve interpolations for custom chart shapes
pnpm add d3-scale d3-array d3-shape
pnpm add -D @types/d3-scale @types/d3-array @types/d3-shape Key Files
| File | Purpose |
|---|---|
src/routes/(app)/analytics/+page.svelte | Analytics page with all chart components |
src/routes/(app)/analytics/+page.server.ts | Server-side data aggregation queries |
src/lib/components/animated-counter.svelte | Animated number counter for KPI cards |
src/lib/components/ui/chart/ | shadcn-svelte chart wrapper (Container, Tooltip, ChartConfig) |
src/app.css | Chart color tokens (--chart-1 through --chart-5) |
vite.config.ts | SSR noExternal config for layerchart + svelte-ux |
Need More?
Advanced Analytics with DashboardPack
Need 10+ chart types, real-time data streaming, and advanced analytics dashboards? Our premium templates ship with Recharts 3 integration, 5 dashboard layout variants, date range pickers, exportable reports, and drill-down data tables — all production-ready.
- Recharts 3 with 10+ chart types (Radar, Treemap, Funnel, Sankey, and more)
- Real-time data with WebSocket-powered live updates
- 5 dashboard variants: Analytics, E-commerce, CRM, SaaS, and Project Management
- Date range filtering, CSV/PDF export, and scheduled reports