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 $state or $derived values 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 TypeComponentData
Area chartAreaChartUser registrations over time (monthly)
Line chartLineChartContent creation trends (pages per month)
Donut chartPieChartPage status distribution (published / draft / archived)
Bar chartBarChartTop 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:

TokenUsage
--chart-1Primary series (signups, published content)
--chart-2Secondary series (drafts)
--chart-3Tertiary series (content creation)
--chart-4Quaternary series (archived content)
--chart-5Quinary 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") + "%"}
/>
PropTypeDefaultDescription
valuenumberrequiredTarget number to animate to
durationnumber800Animation duration in milliseconds
formatFn(n: number) => stringn.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

  1. Add the server query in +page.server.ts — use Drizzle ORM with sql template literals for aggregations
  2. Import the chart component from layerchart (e.g., BarChart, AreaChart, PieChart)
  3. Transform the data using $derived to map server data into the shape LayerChart expects
  4. Define a ChartConfig with labels and var(--chart-N) color tokens
  5. Wrap in Chart.Container and 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 axes
  • d3-array — extent(), max(), sum() for data calculations
  • d3-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

FilePurpose
src/routes/(app)/analytics/+page.svelteAnalytics page with all chart components
src/routes/(app)/analytics/+page.server.tsServer-side data aggregation queries
src/lib/components/animated-counter.svelteAnimated number counter for KPI cards
src/lib/components/ui/chart/shadcn-svelte chart wrapper (Container, Tooltip, ChartConfig)
src/app.cssChart color tokens (--chart-1 through --chart-5)
vite.config.tsSSR 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