SPB Git

spb/svgarden Public

SVGarden — searchable bank of 74 self-contained SVG+CSS animation snippets (svgarden.dev)

HTML 79.2% Astro 10.3% JavaScript 6% CSS 3.7% Shell 0.8%

feat(snippets): add charts category (6)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 11 h ago (Aug 10, 2026) parent 322762c

Showing 6 changed files with +644 and −0

added snippets/charts/chart-bar-race.html +105 −0
@@ -0,0 +1,105 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-bar-race.html
7 + Desc : Horizontal bars racing to their values with spring stagger
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Bar race
12 +slug: chart-bar-race
13 +category: charts
14 +tags: [chart, bars, stagger, spring]
15 +difficulty: intermediate
16 +techniques: ["--sg-i index variables", "scaleX", "cubic-bezier() overshoot", "animation-delay"]
17 +how_it_works: >
18 + Each bar is drawn at its final width in the markup — the data mapping is plain
19 + linear scaling, width = value × 1.6 units — and then compressed to zero with
20 + transform: scaleX(0), so the animation never has to know the value. The race
21 + feel comes from two ingredients: every row carries its rank in a --sg-i custom
22 + property, and one shared rule computes animation-delay: calc(var(--sg-i) × 18%
23 + of the duration), so adding a sixth bar means adding one line of markup, not
24 + one line of CSS. The overshoot is a cubic-bezier whose second control point
25 + rises above 1, making each bar shoot slightly past its value and settle back
26 + like a spring. Value labels reuse the same delay math plus most of the grow
27 + time, so each number pops in just as its bar arrives.
28 +customizable:
29 + - { var: "--sg-color", label: "Bar color", type: color, default: "#7F77DD" }
30 + - { var: "--sg-size", label: "Width", type: range, min: 160, max: 420, default: 260, unit: px }
31 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.4, max: 2.5, step: 0.1, default: 0.9, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<div class="sg-chart-bar-race" style="--sg-color:#7F77DD; --sg-size:260px; --sg-duration:0.9s;">
35 + <svg viewBox="0 0 240 118" role="img" aria-label="Bar chart: Python 92, Rust 78, Go 64, TypeScript 55, C 38">
36 + <title>Language popularity bar chart</title>
37 + <g class="sg-chart-bar-race-row" style="--sg-i:0">
38 + <text class="sg-chart-bar-race-name" x="48" y="20">Python</text>
39 + <rect class="sg-chart-bar-race-bar" x="54" y="10" width="147" height="12" rx="3" />
40 + <text class="sg-chart-bar-race-val" x="206" y="20">92</text>
41 + </g>
42 + <g class="sg-chart-bar-race-row" style="--sg-i:1">
43 + <text class="sg-chart-bar-race-name" x="48" y="42">Rust</text>
44 + <rect class="sg-chart-bar-race-bar" x="54" y="32" width="125" height="12" rx="3" />
45 + <text class="sg-chart-bar-race-val" x="184" y="42">78</text>
46 + </g>
47 + <g class="sg-chart-bar-race-row" style="--sg-i:2">
48 + <text class="sg-chart-bar-race-name" x="48" y="64">Go</text>
49 + <rect class="sg-chart-bar-race-bar" x="54" y="54" width="102" height="12" rx="3" />
50 + <text class="sg-chart-bar-race-val" x="161" y="64">64</text>
51 + </g>
52 + <g class="sg-chart-bar-race-row" style="--sg-i:3">
53 + <text class="sg-chart-bar-race-name" x="48" y="86">TypeScript</text>
54 + <rect class="sg-chart-bar-race-bar" x="54" y="76" width="88" height="12" rx="3" />
55 + <text class="sg-chart-bar-race-val" x="147" y="86">55</text>
56 + </g>
57 + <g class="sg-chart-bar-race-row" style="--sg-i:4">
58 + <text class="sg-chart-bar-race-name" x="48" y="108">C</text>
59 + <rect class="sg-chart-bar-race-bar" x="54" y="98" width="61" height="12" rx="3" />
60 + <text class="sg-chart-bar-race-val" x="120" y="108">38</text>
61 + </g>
62 + </svg>
63 +</div>
64 +<style>
65 + .sg-chart-bar-race {
66 + width: var(--sg-size);
67 + }
68 + .sg-chart-bar-race svg {
69 + display: block;
70 + width: 100%;
71 + font-family: inherit;
72 + }
73 + .sg-chart-bar-race-name {
74 + font-size: 10px;
75 + text-anchor: end;
76 + fill: currentColor;
77 + opacity: 0.75;
78 + }
79 + .sg-chart-bar-race-bar {
80 + fill: var(--sg-color);
81 + transform-box: fill-box;
82 + transform-origin: left center;
83 + animation: sg-chart-bar-race-grow var(--sg-duration) cubic-bezier(0.3, 1.35, 0.5, 1) both;
84 + animation-delay: calc(var(--sg-i) * var(--sg-duration) * 0.18);
85 + }
86 + .sg-chart-bar-race-row:nth-of-type(2) .sg-chart-bar-race-bar { opacity: 0.85; }
87 + .sg-chart-bar-race-row:nth-of-type(3) .sg-chart-bar-race-bar { opacity: 0.7; }
88 + .sg-chart-bar-race-row:nth-of-type(4) .sg-chart-bar-race-bar { opacity: 0.55; }
89 + .sg-chart-bar-race-row:nth-of-type(5) .sg-chart-bar-race-bar { opacity: 0.4; }
90 + .sg-chart-bar-race-val {
91 + font-size: 10px;
92 + font-weight: 600;
93 + fill: currentColor;
94 + animation: sg-chart-bar-race-fade 0.3s ease both;
95 + animation-delay: calc(var(--sg-i) * var(--sg-duration) * 0.18 + var(--sg-duration) * 0.75);
96 + }
97 + @keyframes sg-chart-bar-race-grow {
98 + from { transform: scaleX(0); }
99 + to { transform: scaleX(1); }
100 + }
101 + @keyframes sg-chart-bar-race-fade {
102 + from { opacity: 0; }
103 + to { opacity: 1; }
104 + }
105 +</style>
added snippets/charts/chart-donut-conic.html +98 −0
@@ -0,0 +1,98 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-donut-conic.html
7 + Desc : Donut chart from conic-gradient with @property sweep-open
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Conic donut
12 +slug: chart-donut-conic
13 +category: charts
14 +tags: [chart, donut, conic-gradient, property]
15 +difficulty: advanced
16 +techniques: ["@property", "conic-gradient()", "calc()", "css mask"]
17 +how_it_works: >
18 + The whole donut is one conic-gradient: each segment is a pair of hard color
19 + stops, and every stop position is a fraction of a single angle variable —
20 + 38% of the data is simply 0.38 × --sg-chart-donut-sweep. Registering that
21 + variable with @property and syntax '<angle>' is the key move: it tells the
22 + browser the value is a real angle, not an opaque string, so a keyframe from
23 + 0deg to 360deg interpolates smoothly and every segment sweeps open in
24 + proportion at once. The hole is a radial-gradient mask punched through the
25 + middle, which keeps the center transparent on any page background. Browsers
26 + without @property skip the interpolation and simply show the finished donut.
27 +support: >
28 + Sweep-open animation needs @property (Chrome 85+, Safari 16.4+, Firefox 128+)
29 + and segment tints use color-mix(); older browsers show the donut fully drawn.
30 +customizable:
31 + - { var: "--sg-color", label: "Base color", type: color, default: "#7F77DD" }
32 + - { var: "--sg-size", label: "Size", type: range, min: 90, max: 240, default: 150, unit: px }
33 +created: 2026-08-10
34 +-->
35 +<div class="sg-chart-donut-conic" style="--sg-color:#7F77DD; --sg-size:150px;">
36 + <div class="sg-chart-donut-conic-ring" role="img" aria-label="Donut chart of browser share: Chrome 38%, Safari 28%, Edge 20%, Firefox 14%"></div>
37 + <ul class="sg-chart-donut-conic-legend">
38 + <li>Chrome · 38%</li>
39 + <li>Safari · 28%</li>
40 + <li>Edge · 20%</li>
41 + <li>Firefox · 14%</li>
42 + </ul>
43 +</div>
44 +<style>
45 + @property --sg-chart-donut-sweep {
46 + syntax: '<angle>';
47 + inherits: false;
48 + initial-value: 360deg;
49 + }
50 + .sg-chart-donut-conic {
51 + display: flex;
52 + align-items: center;
53 + gap: 1.1em;
54 + font-family: inherit;
55 + }
56 + .sg-chart-donut-conic-ring {
57 + width: var(--sg-size);
58 + aspect-ratio: 1;
59 + border-radius: 50%;
60 + --sg-chart-donut-sweep: 360deg;
61 + background: conic-gradient(
62 + color-mix(in srgb, var(--sg-color) 100%, transparent) 0 calc(var(--sg-chart-donut-sweep) * 0.38),
63 + color-mix(in srgb, var(--sg-color) 65%, transparent) 0 calc(var(--sg-chart-donut-sweep) * 0.66),
64 + color-mix(in srgb, var(--sg-color) 40%, transparent) 0 calc(var(--sg-chart-donut-sweep) * 0.86),
65 + color-mix(in srgb, var(--sg-color) 22%, transparent) 0 var(--sg-chart-donut-sweep),
66 + transparent var(--sg-chart-donut-sweep) 360deg
67 + );
68 + -webkit-mask: radial-gradient(closest-side, transparent 61%, #000 62%);
69 + mask: radial-gradient(closest-side, transparent 61%, #000 62%);
70 + animation: sg-chart-donut-conic-open 1.1s cubic-bezier(0.25, 0.8, 0.3, 1);
71 + }
72 + @keyframes sg-chart-donut-conic-open {
73 + from { --sg-chart-donut-sweep: 0deg; }
74 + }
75 + .sg-chart-donut-conic-legend {
76 + list-style: none;
77 + margin: 0;
78 + padding: 0;
79 + font-size: 13px;
80 + display: grid;
81 + gap: 0.35em;
82 + }
83 + .sg-chart-donut-conic-legend li {
84 + display: flex;
85 + align-items: center;
86 + gap: 0.5em;
87 + }
88 + .sg-chart-donut-conic-legend li::before {
89 + content: '';
90 + width: 10px;
91 + height: 10px;
92 + border-radius: 3px;
93 + background: color-mix(in srgb, var(--sg-color) 100%, transparent);
94 + }
95 + .sg-chart-donut-conic-legend li:nth-child(2)::before { background: color-mix(in srgb, var(--sg-color) 65%, transparent); }
96 + .sg-chart-donut-conic-legend li:nth-child(3)::before { background: color-mix(in srgb, var(--sg-color) 40%, transparent); }
97 + .sg-chart-donut-conic-legend li:nth-child(4)::before { background: color-mix(in srgb, var(--sg-color) 22%, transparent); }
98 +</style>
added snippets/charts/chart-heat-calendar.html +95 −0
@@ -0,0 +1,95 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-heat-calendar.html
7 + Desc : GitHub-style heat calendar cascading in diagonally
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Heat calendar
12 +slug: chart-heat-calendar
13 +category: charts
14 +tags: [chart, heatmap, calendar, cascade]
15 +difficulty: intermediate
16 +techniques: ["--sg-i index variables", "nth-of-type indexing", "fill-opacity levels", "css rect geometry"]
17 +how_it_works: >
18 + Intensity is encoded the cheap way: every cell shares one fill (the accent
19 + color) and only fill-opacity changes across four level classes, which keeps
20 + the palette coherent on light and dark pages with zero color math. The
21 + diagonal cascade is coordinate arithmetic in CSS: each row group carries
22 + --sg-r, fourteen nth-of-type rules hand each column its --sg-c, and one rule
23 + computes animation-delay: calc((var(--sg-r) + var(--sg-c)) × step) — cells on
24 + the same anti-diagonal share r + c, so they pop together and the wave travels
25 + corner to corner. Cell geometry (width, height, rx) lives in CSS too, which
26 + is why each rect only needs x, y and a level class.
27 +customizable:
28 + - { var: "--sg-color", label: "Heat color", type: color, default: "#7F77DD" }
29 + - { var: "--sg-size", label: "Width", type: range, min: 160, max: 420, default: 260, unit: px }
30 + - { var: "--sg-duration", label: "Cascade", type: range, min: 0.5, max: 4, step: 0.1, default: 1.4, unit: s }
31 +created: 2026-08-10
32 +-->
33 +<div class="sg-chart-heat-calendar" style="--sg-color:#7F77DD; --sg-size:260px; --sg-duration:1.4s;">
34 + <svg viewBox="0 0 210 96" role="img" aria-label="Activity heat calendar, 14 weeks by 7 days, busiest midweek">
35 + <title>Contribution heat calendar</title>
36 + <text class="sg-chart-heat-calendar-day" x="18" y="23">M</text>
37 + <text class="sg-chart-heat-calendar-day" x="18" y="49">W</text>
38 + <text class="sg-chart-heat-calendar-day" x="18" y="75">F</text>
39 + <g class="sg-chart-heat-calendar-row" style="--sg-r:0"><rect x="22" y="2" class="sg-chart-heat-calendar-l0"/><rect x="35" y="2" class="sg-chart-heat-calendar-l0"/><rect x="48" y="2" class="sg-chart-heat-calendar-l1"/><rect x="61" y="2" class="sg-chart-heat-calendar-l0"/><rect x="74" y="2" class="sg-chart-heat-calendar-l0"/><rect x="87" y="2" class="sg-chart-heat-calendar-l1"/><rect x="100" y="2" class="sg-chart-heat-calendar-l0"/><rect x="113" y="2" class="sg-chart-heat-calendar-l0"/><rect x="126" y="2" class="sg-chart-heat-calendar-l0"/><rect x="139" y="2" class="sg-chart-heat-calendar-l1"/><rect x="152" y="2" class="sg-chart-heat-calendar-l0"/><rect x="165" y="2" class="sg-chart-heat-calendar-l0"/><rect x="178" y="2" class="sg-chart-heat-calendar-l1"/><rect x="191" y="2" class="sg-chart-heat-calendar-l0"/></g>
40 + <g class="sg-chart-heat-calendar-row" style="--sg-r:1"><rect x="22" y="15" class="sg-chart-heat-calendar-l1"/><rect x="35" y="15" class="sg-chart-heat-calendar-l2"/><rect x="48" y="15" class="sg-chart-heat-calendar-l1"/><rect x="61" y="15" class="sg-chart-heat-calendar-l2"/><rect x="74" y="15" class="sg-chart-heat-calendar-l3"/><rect x="87" y="15" class="sg-chart-heat-calendar-l1"/><rect x="100" y="15" class="sg-chart-heat-calendar-l2"/><rect x="113" y="15" class="sg-chart-heat-calendar-l1"/><rect x="126" y="15" class="sg-chart-heat-calendar-l3"/><rect x="139" y="15" class="sg-chart-heat-calendar-l2"/><rect x="152" y="15" class="sg-chart-heat-calendar-l1"/><rect x="165" y="15" class="sg-chart-heat-calendar-l2"/><rect x="178" y="15" class="sg-chart-heat-calendar-l2"/><rect x="191" y="15" class="sg-chart-heat-calendar-l1"/></g>
41 + <g class="sg-chart-heat-calendar-row" style="--sg-r:2"><rect x="22" y="28" class="sg-chart-heat-calendar-l2"/><rect x="35" y="28" class="sg-chart-heat-calendar-l1"/><rect x="48" y="28" class="sg-chart-heat-calendar-l3"/><rect x="61" y="28" class="sg-chart-heat-calendar-l2"/><rect x="74" y="28" class="sg-chart-heat-calendar-l1"/><rect x="87" y="28" class="sg-chart-heat-calendar-l2"/><rect x="100" y="28" class="sg-chart-heat-calendar-l3"/><rect x="113" y="28" class="sg-chart-heat-calendar-l2"/><rect x="126" y="28" class="sg-chart-heat-calendar-l1"/><rect x="139" y="28" class="sg-chart-heat-calendar-l3"/><rect x="152" y="28" class="sg-chart-heat-calendar-l2"/><rect x="165" y="28" class="sg-chart-heat-calendar-l1"/><rect x="178" y="28" class="sg-chart-heat-calendar-l3"/><rect x="191" y="28" class="sg-chart-heat-calendar-l2"/></g>
42 + <g class="sg-chart-heat-calendar-row" style="--sg-r:3"><rect x="22" y="41" class="sg-chart-heat-calendar-l1"/><rect x="35" y="41" class="sg-chart-heat-calendar-l3"/><rect x="48" y="41" class="sg-chart-heat-calendar-l2"/><rect x="61" y="41" class="sg-chart-heat-calendar-l3"/><rect x="74" y="41" class="sg-chart-heat-calendar-l2"/><rect x="87" y="41" class="sg-chart-heat-calendar-l3"/><rect x="100" y="41" class="sg-chart-heat-calendar-l1"/><rect x="113" y="41" class="sg-chart-heat-calendar-l3"/><rect x="126" y="41" class="sg-chart-heat-calendar-l2"/><rect x="139" y="41" class="sg-chart-heat-calendar-l2"/><rect x="152" y="41" class="sg-chart-heat-calendar-l3"/><rect x="165" y="41" class="sg-chart-heat-calendar-l3"/><rect x="178" y="41" class="sg-chart-heat-calendar-l1"/><rect x="191" y="41" class="sg-chart-heat-calendar-l3"/></g>
43 + <g class="sg-chart-heat-calendar-row" style="--sg-r:4"><rect x="22" y="54" class="sg-chart-heat-calendar-l2"/><rect x="35" y="54" class="sg-chart-heat-calendar-l2"/><rect x="48" y="54" class="sg-chart-heat-calendar-l1"/><rect x="61" y="54" class="sg-chart-heat-calendar-l1"/><rect x="74" y="54" class="sg-chart-heat-calendar-l3"/><rect x="87" y="54" class="sg-chart-heat-calendar-l2"/><rect x="100" y="54" class="sg-chart-heat-calendar-l2"/><rect x="113" y="54" class="sg-chart-heat-calendar-l3"/><rect x="126" y="54" class="sg-chart-heat-calendar-l1"/><rect x="139" y="54" class="sg-chart-heat-calendar-l2"/><rect x="152" y="54" class="sg-chart-heat-calendar-l3"/><rect x="165" y="54" class="sg-chart-heat-calendar-l1"/><rect x="178" y="54" class="sg-chart-heat-calendar-l2"/><rect x="191" y="54" class="sg-chart-heat-calendar-l3"/></g>
44 + <g class="sg-chart-heat-calendar-row" style="--sg-r:5"><rect x="22" y="67" class="sg-chart-heat-calendar-l3"/><rect x="35" y="67" class="sg-chart-heat-calendar-l1"/><rect x="48" y="67" class="sg-chart-heat-calendar-l2"/><rect x="61" y="67" class="sg-chart-heat-calendar-l2"/><rect x="74" y="67" class="sg-chart-heat-calendar-l2"/><rect x="87" y="67" class="sg-chart-heat-calendar-l1"/><rect x="100" y="67" class="sg-chart-heat-calendar-l3"/><rect x="113" y="67" class="sg-chart-heat-calendar-l1"/><rect x="126" y="67" class="sg-chart-heat-calendar-l2"/><rect x="139" y="67" class="sg-chart-heat-calendar-l1"/><rect x="152" y="67" class="sg-chart-heat-calendar-l1"/><rect x="165" y="67" class="sg-chart-heat-calendar-l2"/><rect x="178" y="67" class="sg-chart-heat-calendar-l3"/><rect x="191" y="67" class="sg-chart-heat-calendar-l1"/></g>
45 + <g class="sg-chart-heat-calendar-row" style="--sg-r:6"><rect x="22" y="80" class="sg-chart-heat-calendar-l0"/><rect x="35" y="80" class="sg-chart-heat-calendar-l1"/><rect x="48" y="80" class="sg-chart-heat-calendar-l0"/><rect x="61" y="80" class="sg-chart-heat-calendar-l0"/><rect x="74" y="80" class="sg-chart-heat-calendar-l1"/><rect x="87" y="80" class="sg-chart-heat-calendar-l0"/><rect x="100" y="80" class="sg-chart-heat-calendar-l0"/><rect x="113" y="80" class="sg-chart-heat-calendar-l1"/><rect x="126" y="80" class="sg-chart-heat-calendar-l0"/><rect x="139" y="80" class="sg-chart-heat-calendar-l0"/><rect x="152" y="80" class="sg-chart-heat-calendar-l2"/><rect x="165" y="80" class="sg-chart-heat-calendar-l0"/><rect x="178" y="80" class="sg-chart-heat-calendar-l0"/><rect x="191" y="80" class="sg-chart-heat-calendar-l1"/></g>
46 + </svg>
47 +</div>
48 +<style>
49 + .sg-chart-heat-calendar {
50 + width: var(--sg-size);
51 + }
52 + .sg-chart-heat-calendar svg {
53 + display: block;
54 + width: 100%;
55 + font-family: inherit;
56 + }
57 + .sg-chart-heat-calendar-day {
58 + font-size: 7px;
59 + fill: currentColor;
60 + opacity: 0.55;
61 + text-anchor: end;
62 + }
63 + .sg-chart-heat-calendar rect {
64 + width: 10px;
65 + height: 10px;
66 + rx: 2px;
67 + fill: var(--sg-color);
68 + transform-box: fill-box;
69 + transform-origin: center;
70 + animation: sg-chart-heat-calendar-pop 0.35s ease-out both;
71 + animation-delay: calc((var(--sg-r) + var(--sg-c)) * var(--sg-duration) / 26);
72 + }
73 + .sg-chart-heat-calendar rect:nth-of-type(1) { --sg-c: 0; }
74 + .sg-chart-heat-calendar rect:nth-of-type(2) { --sg-c: 1; }
75 + .sg-chart-heat-calendar rect:nth-of-type(3) { --sg-c: 2; }
76 + .sg-chart-heat-calendar rect:nth-of-type(4) { --sg-c: 3; }
77 + .sg-chart-heat-calendar rect:nth-of-type(5) { --sg-c: 4; }
78 + .sg-chart-heat-calendar rect:nth-of-type(6) { --sg-c: 5; }
79 + .sg-chart-heat-calendar rect:nth-of-type(7) { --sg-c: 6; }
80 + .sg-chart-heat-calendar rect:nth-of-type(8) { --sg-c: 7; }
81 + .sg-chart-heat-calendar rect:nth-of-type(9) { --sg-c: 8; }
82 + .sg-chart-heat-calendar rect:nth-of-type(10) { --sg-c: 9; }
83 + .sg-chart-heat-calendar rect:nth-of-type(11) { --sg-c: 10; }
84 + .sg-chart-heat-calendar rect:nth-of-type(12) { --sg-c: 11; }
85 + .sg-chart-heat-calendar rect:nth-of-type(13) { --sg-c: 12; }
86 + .sg-chart-heat-calendar rect:nth-of-type(14) { --sg-c: 13; }
87 + .sg-chart-heat-calendar-l0 { fill-opacity: 0.12; }
88 + .sg-chart-heat-calendar-l1 { fill-opacity: 0.35; }
89 + .sg-chart-heat-calendar-l2 { fill-opacity: 0.6; }
90 + .sg-chart-heat-calendar-l3 { fill-opacity: 0.9; }
91 + @keyframes sg-chart-heat-calendar-pop {
92 + from { opacity: 0; transform: scale(0.4); }
93 + to { opacity: 1; transform: scale(1); }
94 + }
95 +</style>
added snippets/charts/chart-line-draw-tooltip.html +120 −0
@@ -0,0 +1,120 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-line-draw-tooltip.html
7 + Desc : Line chart that draws in, with a snapping crosshair tooltip
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Line chart + crosshair
12 +slug: chart-line-draw-tooltip
13 +category: charts
14 +tags: [chart, line, tooltip, crosshair, js]
15 +difficulty: advanced
16 +techniques: [pathLength, pointer-events, getBoundingClientRect, "svg coordinate mapping"]
17 +how_it_works: >
18 + The polyline's points are the data, pre-mapped into viewBox units (x = 20 +
19 + index × 22, y = 104 − value × 0.9), and pathLength="100" lets the usual dash
20 + trick draw it in without measuring. The interesting part is the tooltip math:
21 + pointer events give you pixel coordinates, but the chart lives in viewBox
22 + units, so the script converts with x = (clientX − rect.left) × 240 ÷
23 + rect.width — the one line that makes the crosshair correct at any rendered
24 + size. It then snaps to the nearest hardcoded data point instead of
25 + interpolating, which is both honest (you can only inspect real samples) and
26 + cheap: a linear scan over ten points per move.
27 +customizable:
28 + - { var: "--sg-color", label: "Line color", type: color, default: "#7F77DD" }
29 + - { var: "--sg-size", label: "Width", type: range, min: 180, max: 460, default: 280, unit: px }
30 +created: 2026-08-10
31 +-->
32 +<div class="sg-chart-line-draw-tooltip" style="--sg-color:#7F77DD; --sg-size:280px;">
33 + <svg viewBox="0 0 240 120" role="img" aria-label="Line chart of monthly values rising from 24 to 82">
34 + <title>Monthly values line chart</title>
35 + <g class="sg-chart-line-draw-tooltip-grid">
36 + <line x1="20" y1="30" x2="218" y2="30" />
37 + <line x1="20" y1="56" x2="218" y2="56" />
38 + <line x1="20" y1="82" x2="218" y2="82" />
39 + <line x1="20" y1="104" x2="218" y2="104" />
40 + </g>
41 + <polyline class="sg-chart-line-draw-tooltip-line" pathLength="100"
42 + points="20,82 42,72 64,77 86,61 108,66 130,52 152,45 174,50 196,37 218,30" />
43 + <g class="sg-chart-line-draw-tooltip-hud">
44 + <line class="sg-chart-line-draw-tooltip-cross" x1="20" y1="14" x2="20" y2="104" />
45 + <circle class="sg-chart-line-draw-tooltip-dot" cx="20" cy="82" r="3.5" />
46 + <text class="sg-chart-line-draw-tooltip-lbl" x="20" y="73">24</text>
47 + </g>
48 + </svg>
49 +</div>
50 +<script>
51 + document.querySelectorAll('.sg-chart-line-draw-tooltip:not([data-sg-init])').forEach(function (root) {
52 + root.setAttribute('data-sg-init', '');
53 + var svg = root.querySelector('svg');
54 + var hud = root.querySelector('.sg-chart-line-draw-tooltip-hud');
55 + var cross = root.querySelector('.sg-chart-line-draw-tooltip-cross');
56 + var dot = root.querySelector('.sg-chart-line-draw-tooltip-dot');
57 + var lbl = root.querySelector('.sg-chart-line-draw-tooltip-lbl');
58 + var pts = [[20,82,24],[42,72,36],[64,77,30],[86,61,48],[108,66,42],[130,52,58],[152,45,66],[174,50,60],[196,37,74],[218,30,82]];
59 + svg.addEventListener('pointermove', function (e) {
60 + var r = svg.getBoundingClientRect();
61 + var x = (e.clientX - r.left) * 240 / r.width;
62 + var best = pts[0];
63 + for (var i = 1; i < pts.length; i++) if (Math.abs(pts[i][0] - x) < Math.abs(best[0] - x)) best = pts[i];
64 + cross.setAttribute('x1', best[0]); cross.setAttribute('x2', best[0]);
65 + dot.setAttribute('cx', best[0]); dot.setAttribute('cy', best[1]);
66 + lbl.setAttribute('x', Math.max(28, Math.min(210, best[0])));
67 + lbl.setAttribute('y', best[1] - 9);
68 + lbl.textContent = best[2];
69 + hud.style.opacity = 1;
70 + });
71 + svg.addEventListener('pointerleave', function () { hud.style.opacity = 0; });
72 + });
73 +</script>
74 +<style>
75 + .sg-chart-line-draw-tooltip {
76 + width: var(--sg-size);
77 + }
78 + .sg-chart-line-draw-tooltip svg {
79 + display: block;
80 + width: 100%;
81 + font-family: inherit;
82 + touch-action: none;
83 + }
84 + .sg-chart-line-draw-tooltip-grid line {
85 + stroke: rgba(128, 128, 128, 0.3);
86 + stroke-width: 1;
87 + stroke-dasharray: 2 4;
88 + }
89 + .sg-chart-line-draw-tooltip-line {
90 + fill: none;
91 + stroke: var(--sg-color);
92 + stroke-width: 2.5;
93 + stroke-linecap: round;
94 + stroke-linejoin: round;
95 + stroke-dasharray: 100;
96 + stroke-dashoffset: 100;
97 + animation: sg-chart-line-draw-tooltip-in 1.2s ease-out forwards;
98 + }
99 + @keyframes sg-chart-line-draw-tooltip-in {
100 + to { stroke-dashoffset: 0; }
101 + }
102 + .sg-chart-line-draw-tooltip-hud {
103 + opacity: 0;
104 + transition: opacity 0.15s ease;
105 + pointer-events: none;
106 + }
107 + .sg-chart-line-draw-tooltip-cross {
108 + stroke: rgba(128, 128, 128, 0.5);
109 + stroke-width: 1;
110 + }
111 + .sg-chart-line-draw-tooltip-dot {
112 + fill: var(--sg-color);
113 + }
114 + .sg-chart-line-draw-tooltip-lbl {
115 + font-size: 10px;
116 + font-weight: 600;
117 + fill: currentColor;
118 + text-anchor: middle;
119 + }
120 +</style>
added snippets/charts/chart-radar-pulse.html +114 −0
@@ -0,0 +1,114 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-radar-pulse.html
7 + Desc : Radar chart whose data area breathes and vertices ping
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Radar pulse
12 +slug: chart-radar-pulse
13 +category: charts
14 +tags: [chart, radar, pulse, polygon]
15 +difficulty: intermediate
16 +techniques: [transform-box, "polar → cartesian mapping", "animation-delay", scale]
17 +how_it_works: >
18 + A radar chart is pure polar-to-cartesian mapping: each axis sits at 72°
19 + intervals (five axes), and a data point at strength v becomes the vertex
20 + (cx + v·R·cos θ, cy + v·R·sin θ) — every polygon here, rings included, was
21 + computed with that one formula. The rings are just the same pentagon at four
22 + radii, so the "grid" costs four polygons instead of trigonometry at runtime.
23 + The living feel comes from transform-box: fill-box on the data polygon, which
24 + makes a tiny scale oscillation breathe around the shape's own centroid rather
25 + than the SVG corner, while the vertex dots reuse one ping animation with
26 + staggered delays so attention sweeps around the chart.
27 +customizable:
28 + - { var: "--sg-color", label: "Area color", type: color, default: "#7F77DD" }
29 + - { var: "--sg-size", label: "Size", type: range, min: 110, max: 300, default: 170, unit: px }
30 + - { var: "--sg-duration", label: "Speed", type: range, min: 1, max: 6, step: 0.5, default: 2.5, unit: s }
31 +created: 2026-08-10
32 +-->
33 +<div class="sg-chart-radar-pulse" style="--sg-color:#7F77DD; --sg-size:170px; --sg-duration:2.5s;">
34 + <svg viewBox="0 0 140 140" role="img" aria-label="Radar chart of five skills: speed 90, power 65, defense 80, stamina 55, accuracy 70 out of 100">
35 + <title>Skill radar chart</title>
36 + <g class="sg-chart-radar-pulse-rings">
37 + <polygon points="70,18 119.5,53.9 100.6,112.1 39.4,112.1 20.5,53.9" />
38 + <polygon points="70,31 107.1,58 92.9,101.6 47.1,101.6 32.9,58" />
39 + <polygon points="70,44 94.7,62 85.3,91 54.7,91 45.3,62" />
40 + <polygon points="70,57 82.4,66 77.6,80.5 62.4,80.5 57.6,66" />
41 + <line x1="70" y1="70" x2="70" y2="18" />
42 + <line x1="70" y1="70" x2="119.5" y2="53.9" />
43 + <line x1="70" y1="70" x2="100.6" y2="112.1" />
44 + <line x1="70" y1="70" x2="39.4" y2="112.1" />
45 + <line x1="70" y1="70" x2="20.5" y2="53.9" />
46 + </g>
47 + <polygon class="sg-chart-radar-pulse-area" points="70,23.2 102.1,59.6 94.5,103.7 53.2,93.1 35.4,58.8" />
48 + <g class="sg-chart-radar-pulse-dots">
49 + <circle cx="70" cy="23.2" r="3" />
50 + <circle cx="102.1" cy="59.6" r="3" />
51 + <circle cx="94.5" cy="103.7" r="3" />
52 + <circle cx="53.2" cy="93.1" r="3" />
53 + <circle cx="35.4" cy="58.8" r="3" />
54 + </g>
55 + <g class="sg-chart-radar-pulse-labels">
56 + <text x="70" y="12">spd</text>
57 + <text x="128" y="52">pwr</text>
58 + <text x="105" y="122">def</text>
59 + <text x="35" y="122">sta</text>
60 + <text x="12" y="52">acc</text>
61 + </g>
62 + </svg>
63 +</div>
64 +<style>
65 + .sg-chart-radar-pulse {
66 + width: var(--sg-size);
67 + }
68 + .sg-chart-radar-pulse svg {
69 + display: block;
70 + width: 100%;
71 + font-family: inherit;
72 + }
73 + .sg-chart-radar-pulse-rings polygon,
74 + .sg-chart-radar-pulse-rings line {
75 + fill: none;
76 + stroke: rgba(128, 128, 128, 0.3);
77 + stroke-width: 1;
78 + }
79 + .sg-chart-radar-pulse-area {
80 + fill: var(--sg-color);
81 + fill-opacity: 0.35;
82 + stroke: var(--sg-color);
83 + stroke-width: 2;
84 + stroke-linejoin: round;
85 + transform-box: fill-box;
86 + transform-origin: center;
87 + animation: sg-chart-radar-pulse-breathe var(--sg-duration) ease-in-out infinite;
88 + }
89 + .sg-chart-radar-pulse-dots circle {
90 + fill: var(--sg-color);
91 + transform-box: fill-box;
92 + transform-origin: center;
93 + animation: sg-chart-radar-pulse-ping var(--sg-duration) ease-in-out infinite;
94 + }
95 + .sg-chart-radar-pulse-dots circle:nth-of-type(2) { animation-delay: calc(var(--sg-duration) * 0.2); }
96 + .sg-chart-radar-pulse-dots circle:nth-of-type(3) { animation-delay: calc(var(--sg-duration) * 0.4); }
97 + .sg-chart-radar-pulse-dots circle:nth-of-type(4) { animation-delay: calc(var(--sg-duration) * 0.6); }
98 + .sg-chart-radar-pulse-dots circle:nth-of-type(5) { animation-delay: calc(var(--sg-duration) * 0.8); }
99 + .sg-chart-radar-pulse-labels text {
100 + font-size: 9px;
101 + fill: currentColor;
102 + opacity: 0.65;
103 + text-anchor: middle;
104 + }
105 + @keyframes sg-chart-radar-pulse-breathe {
106 + 0%, 100% { transform: scale(1); fill-opacity: 0.3; }
107 + 50% { transform: scale(1.045); fill-opacity: 0.5; }
108 + }
109 + @keyframes sg-chart-radar-pulse-ping {
110 + 0%, 55%, 100% { transform: scale(1); opacity: 1; }
111 + 70% { transform: scale(1.9); opacity: 0.4; }
112 + 85% { transform: scale(1); opacity: 1; }
113 + }
114 +</style>
added snippets/charts/chart-sparkline-live.html +112 −0
@@ -0,0 +1,112 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/charts/chart-sparkline-live.html
7 + Desc : Live sparkline — new points stream in, old ones slide out
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Live sparkline
12 +slug: chart-sparkline-live
13 +category: charts
14 +tags: [chart, sparkline, live, streaming, js]
15 +difficulty: advanced
16 +techniques: ["fixed-window data buffer", "points attribute rewrite", "transition retrigger", prefers-reduced-motion]
17 +how_it_works: >
18 + The script keeps a fixed-length array as a sliding window: every tick pushes a
19 + new random-walk value, shifts the oldest out, and rewrites the polyline's
20 + points attribute from scratch — with 22 points that is far cheaper than any
21 + clever incremental DOM surgery. The slide illusion is a transition retrigger
22 + trick: after each rewrite the group is jumped 10 units right with transitions
23 + disabled (the new geometry then overlaps the old frame exactly), and one
24 + requestAnimationFrame later the transition is re-enabled and the group eases
25 + back to zero. The tick delay is re-read from --sg-duration on every cycle via
26 + a setTimeout chain, so the speed slider takes effect on the very next point,
27 + and prefers-reduced-motion stops the stream after the first paint.
28 +customizable:
29 + - { var: "--sg-color", label: "Line color", type: color, default: "#7F77DD" }
30 + - { var: "--sg-size", label: "Width", type: range, min: 140, max: 380, default: 220, unit: px }
31 + - { var: "--sg-duration", label: "Tick every", type: range, min: 0.3, max: 2, step: 0.1, default: 0.7, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<div class="sg-chart-sparkline-live" style="--sg-color:#7F77DD; --sg-size:220px; --sg-duration:0.7s;">
35 + <svg viewBox="0 0 220 60" role="img" aria-label="Live-updating sparkline of streaming values">
36 + <title>Live sparkline</title>
37 + <line class="sg-chart-sparkline-live-base" x1="5" y1="58" x2="215" y2="58" />
38 + <g class="sg-chart-sparkline-live-win">
39 + <polygon class="sg-chart-sparkline-live-area" points="" />
40 + <polyline class="sg-chart-sparkline-live-line" points="" />
41 + </g>
42 + <circle class="sg-chart-sparkline-live-dot" cx="215" cy="30" r="3" />
43 + </svg>
44 +</div>
45 +<script>
46 + document.querySelectorAll('.sg-chart-sparkline-live:not([data-sg-init])').forEach(function (root) {
47 + root.setAttribute('data-sg-init', '');
48 + var line = root.querySelector('.sg-chart-sparkline-live-line');
49 + var area = root.querySelector('.sg-chart-sparkline-live-area');
50 + var win = root.querySelector('.sg-chart-sparkline-live-win');
51 + var dot = root.querySelector('.sg-chart-sparkline-live-dot');
52 + var N = 22, data = [30];
53 + for (var i = 1; i < N; i++) data.push(Math.max(8, Math.min(52, data[i - 1] + Math.random() * 14 - 7)));
54 + function render() {
55 + var pts = data.map(function (v, i) { return (5 + i * 10) + ',' + (58 - v).toFixed(1); }).join(' ');
56 + line.setAttribute('points', pts);
57 + area.setAttribute('points', pts + ' 215,58 5,58');
58 + dot.setAttribute('cy', (58 - data[N - 1]).toFixed(1));
59 + }
60 + render();
61 + if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
62 + function tick() {
63 + data.push(Math.max(8, Math.min(52, data[N - 1] + Math.random() * 14 - 7)));
64 + data.shift();
65 + win.style.transition = 'none';
66 + render();
67 + win.style.transform = 'translateX(10px)';
68 + requestAnimationFrame(function () {
69 + var s = parseFloat(getComputedStyle(root).getPropertyValue('--sg-duration')) || 0.7;
70 + win.style.transition = 'transform ' + s + 's linear';
71 + win.style.transform = 'translateX(0)';
72 + setTimeout(tick, s * 1000);
73 + });
74 + }
75 + tick();
76 + });
77 +</script>
78 +<style>
79 + .sg-chart-sparkline-live {
80 + width: var(--sg-size);
81 + }
82 + .sg-chart-sparkline-live svg {
83 + display: block;
84 + width: 100%;
85 + overflow: hidden;
86 + }
87 + .sg-chart-sparkline-live-base {
88 + stroke: rgba(128, 128, 128, 0.3);
89 + stroke-width: 1;
90 + }
91 + .sg-chart-sparkline-live-line {
92 + fill: none;
93 + stroke: var(--sg-color);
94 + stroke-width: 2;
95 + stroke-linecap: round;
96 + stroke-linejoin: round;
97 + }
98 + .sg-chart-sparkline-live-area {
99 + fill: var(--sg-color);
100 + opacity: 0.12;
101 + }
102 + .sg-chart-sparkline-live-dot {
103 + fill: var(--sg-color);
104 + transform-box: fill-box;
105 + transform-origin: center;
106 + animation: sg-chart-sparkline-live-pulse 1.4s ease-in-out infinite;
107 + }
108 + @keyframes sg-chart-sparkline-live-pulse {
109 + 0%, 100% { transform: scale(1); opacity: 1; }
110 + 50% { transform: scale(1.7); opacity: 0.55; }
111 + }
112 +</style>
113