UI Components Medium
Bar Chart
A vertical and horizontal bar chart built with SVG. Features animated bars, value labels on hover, grouped/stacked modes, and a responsive layout that redraws on resize.
Open in Lab
MCP
vanilla-js svg react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0f1117;
--surface: #16181f;
--surface2: #1e2130;
--border: #2a2d3a;
--text: #e2e8f0;
--text-muted: #64748b;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 32px 24px;
}
.chart-page {
max-width: 860px;
margin: 0 auto;
}
.chart-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.chart-title {
font-size: 1.2rem;
font-weight: 700;
}
.chart-sub {
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 3px;
}
.chart-controls {
display: flex;
gap: 6px;
}
.ctrl-btn {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 600;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
transition: all .15s;
}
.ctrl-btn.active,
.ctrl-btn:hover {
border-color: #818cf8;
color: #818cf8;
}
.chart-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 16px 12px;
position: relative;
}
.chart-svg {
width: 100%;
display: block;
}
.bar-rect {
transition: opacity .15s;
cursor: pointer;
}
.bar-rect:hover {
opacity: 0.85;
}
.grid-line {
stroke: var(--border);
stroke-width: 1;
}
.grid-label {
fill: var(--text-muted);
font-size: 10px;
font-family: inherit;
}
.x-label {
fill: var(--text-muted);
font-size: 10px;
font-family: inherit;
text-anchor: middle;
}
.bar-label {
fill: var(--text);
font-size: 10px;
font-family: inherit;
font-weight: 700;
text-anchor: middle;
}
.chart-tooltip {
position: absolute;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
font-size: 0.78rem;
pointer-events: none;
z-index: 10;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.tooltip-label {
font-weight: 700;
margin-bottom: 4px;
}const DATA = [
{ label: "Electronics", value: 340, color: "#818cf8" },
{ label: "Clothing", value: 220, color: "#34d399" },
{ label: "Books", value: 185, color: "#f59e0b" },
{ label: "Sports", value: 260, color: "#f87171" },
{ label: "Home", value: 310, color: "#a78bfa" },
{ label: "Beauty", value: 145, color: "#38bdf8" },
];
const PAD = { top: 24, right: 20, bottom: 40, left: 52 };
let orient = "vertical";
const svg = document.getElementById("chartSvg");
const tooltip = document.getElementById("chartTooltip");
const wrap = document.getElementById("chartWrap");
document.querySelectorAll(".ctrl-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".ctrl-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
orient = btn.dataset.orient;
draw();
});
});
function draw() {
const W = wrap.clientWidth - 32;
const H =
orient === "vertical"
? Math.round(W * 0.5)
: Math.round(DATA.length * 52 + PAD.top + PAD.bottom);
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
svg.innerHTML = "";
const maxVal = Math.ceil(Math.max(...DATA.map((d) => d.value)) * 1.15);
const cW = W - PAD.left - PAD.right;
const cH = H - PAD.top - PAD.bottom;
if (orient === "vertical") drawVertical(W, H, cW, cH, maxVal);
else drawHorizontal(W, H, cW, cH, maxVal);
}
function el(tag, attrs = {}) {
const e = document.createElementNS("http://www.w3.org/2000/svg", tag);
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
return e;
}
function drawVertical(W, H, cW, cH, maxVal) {
const n = DATA.length;
const gap = 8;
const barW = (cW - gap * (n - 1)) / n;
// Grid
for (let t = 0; t <= 5; t++) {
const v = Math.round((maxVal / 5) * t);
const y = PAD.top + cH - (v / maxVal) * cH;
svg.appendChild(
el("line", { class: "grid-line", x1: PAD.left, x2: PAD.left + cW, y1: y, y2: y })
);
const lbl = el("text", {
class: "grid-label",
x: PAD.left - 6,
y: y + 3.5,
"text-anchor": "end",
});
lbl.textContent = v;
svg.appendChild(lbl);
}
// Bars
DATA.forEach((d, i) => {
const x = PAD.left + i * (barW + gap);
const barH = (d.value / maxVal) * cH;
const y = PAD.top + cH - barH;
const rect = el("rect", {
class: "bar-rect",
x,
y,
width: barW,
height: barH,
fill: d.color,
rx: 4,
});
addBarEvents(rect, d, x + barW / 2, y);
svg.appendChild(rect);
// Animate
rect.style.transformOrigin = `0 ${PAD.top + cH}px`;
rect.style.transform = "scaleY(0)";
rect.style.transition = `transform .5s cubic-bezier(.4,0,.2,1) ${i * 0.06}s`;
requestAnimationFrame(() => requestAnimationFrame(() => (rect.style.transform = "scaleY(1)")));
// X label
const lbl = el("text", { class: "x-label", x: x + barW / 2, y: H - 6 });
lbl.textContent = d.label;
svg.appendChild(lbl);
// Value label
const vlbl = el("text", { class: "bar-label", x: x + barW / 2, y: y - 4 });
vlbl.textContent = d.value;
svg.appendChild(vlbl);
});
}
function drawHorizontal(W, H, cW, cH, maxVal) {
const n = DATA.length;
const barH = 30;
const gap = 12;
const totalH = n * (barH + gap);
for (let t = 0; t <= 5; t++) {
const v = Math.round((maxVal / 5) * t);
const x = PAD.left + (v / maxVal) * cW;
svg.appendChild(
el("line", { class: "grid-line", x1: x, x2: x, y1: PAD.top, y2: PAD.top + totalH })
);
const lbl = el("text", {
class: "grid-label",
x,
y: PAD.top + totalH + 14,
"text-anchor": "middle",
});
lbl.textContent = v;
svg.appendChild(lbl);
}
DATA.forEach((d, i) => {
const y = PAD.top + i * (barH + gap);
const bW = (d.value / maxVal) * cW;
const rect = el("rect", {
class: "bar-rect",
x: PAD.left,
y,
width: bW,
height: barH,
fill: d.color,
rx: 4,
});
addBarEvents(rect, d, PAD.left + bW, y + barH / 2);
svg.appendChild(rect);
rect.style.transformOrigin = `${PAD.left}px 0`;
rect.style.transform = "scaleX(0)";
rect.style.transition = `transform .5s cubic-bezier(.4,0,.2,1) ${i * 0.06}s`;
requestAnimationFrame(() => requestAnimationFrame(() => (rect.style.transform = "scaleX(1)")));
const lbl = el("text", {
class: "grid-label",
x: PAD.left - 6,
y: y + barH / 2 + 4,
"text-anchor": "end",
});
lbl.textContent = d.label;
svg.appendChild(lbl);
const vlbl = el("text", {
class: "bar-label",
x: PAD.left + bW + 4,
y: y + barH / 2 + 4,
"text-anchor": "start",
});
vlbl.textContent = d.value;
svg.appendChild(vlbl);
});
}
function addBarEvents(rect, d, tx, ty) {
rect.addEventListener("mouseenter", (e) => {
tooltip.innerHTML = `<div class="tooltip-label">${d.label}</div><div style="color:${d.color};font-weight:700">${d.value} units</div>`;
tooltip.hidden = false;
posTooltip(e);
});
rect.addEventListener("mousemove", posTooltip);
rect.addEventListener("mouseleave", () => {
tooltip.hidden = true;
});
}
function posTooltip(e) {
const r = wrap.getBoundingClientRect();
tooltip.style.left = e.clientX - r.left + 10 + "px";
tooltip.style.top = e.clientY - r.top - 40 + "px";
}
const ro = new ResizeObserver(draw);
ro.observe(wrap);
draw();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bar Chart</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="chart-page">
<div class="chart-header">
<div>
<h1 class="chart-title">Sales by Category</h1>
<p class="chart-sub">Q1–Q4 2024</p>
</div>
<div class="chart-controls">
<button class="ctrl-btn active" data-orient="vertical">Vertical</button>
<button class="ctrl-btn" data-orient="horizontal">Horizontal</button>
</div>
</div>
<div class="chart-wrap" id="chartWrap">
<svg id="chartSvg" class="chart-svg" aria-label="Bar chart: Sales by Category"></svg>
<div class="chart-tooltip" id="chartTooltip" hidden></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useRef, useEffect } from "react";
const DATA = [
{ label: "Electronics", value: 340, color: "#818cf8" },
{ label: "Clothing", value: 220, color: "#34d399" },
{ label: "Books", value: 185, color: "#f59e0b" },
{ label: "Sports", value: 260, color: "#f87171" },
{ label: "Home", value: 310, color: "#a78bfa" },
{ label: "Beauty", value: 145, color: "#38bdf8" },
];
const PAD = { top: 24, right: 20, bottom: 40, left: 52 };
type Tooltip = { label: string; value: number; color: string; x: number; y: number } | null;
export default function ChartBarRC() {
const wrapRef = useRef<HTMLDivElement>(null);
const [W, setW] = useState(600);
const [orient, setOrient] = useState<"vertical" | "horizontal">("vertical");
const [tooltip, setTooltip] = useState<Tooltip>(null);
const [animated, setAnimated] = useState(false);
useEffect(() => {
const ro = new ResizeObserver(() => {
if (wrapRef.current) setW(wrapRef.current.clientWidth - 32);
});
if (wrapRef.current) ro.observe(wrapRef.current);
return () => ro.disconnect();
}, []);
useEffect(() => {
setAnimated(false);
requestAnimationFrame(() => setAnimated(true));
}, [orient]);
const maxVal = Math.ceil(Math.max(...DATA.map((d) => d.value)) * 1.15);
if (orient === "vertical") {
const H = Math.round(W * 0.5);
const cW = W - PAD.left - PAD.right;
const cH = H - PAD.top - PAD.bottom;
const n = DATA.length;
const gap = 8;
const barW = (cW - gap * (n - 1)) / n;
const yOf = (v: number) => PAD.top + cH - (v / maxVal) * cH;
return (
<div className="min-h-screen bg-[#0d1117] p-6">
<div ref={wrapRef} className="w-full max-w-[800px] mx-auto">
<div className="flex gap-1 mb-4 justify-end">
{(["vertical", "horizontal"] as const).map((o) => (
<button
key={o}
onClick={() => setOrient(o)}
className={`text-[11px] px-3 py-1 rounded border transition-colors capitalize ${orient === o ? "bg-[#818cf8]/20 border-[#818cf8] text-[#818cf8]" : "border-[#30363d] text-[#8b949e] hover:border-[#8b949e]"}`}
>
{o}
</button>
))}
</div>
<div className="relative">
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className="w-full overflow-visible">
{Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal / 5) * t);
const y = yOf(v);
return (
<g key={t}>
<line
x1={PAD.left}
x2={PAD.left + cW}
y1={y}
y2={y}
stroke="#21262d"
strokeWidth={1}
/>
<text
x={PAD.left - 6}
y={y + 3.5}
textAnchor="end"
fill="#484f58"
fontSize={10}
>
{v}
</text>
</g>
);
})}
{DATA.map((d, i) => {
const x = PAD.left + i * (barW + gap);
const barH = (d.value / maxVal) * cH;
const y = PAD.top + cH - barH;
return (
<g key={d.label}>
<rect
x={x}
y={animated ? y : PAD.top + cH}
width={barW}
height={animated ? barH : 0}
rx={4}
fill={d.color}
style={{
transition: `y 0.5s cubic-bezier(.4,0,.2,1) ${i * 0.06}s, height 0.5s cubic-bezier(.4,0,.2,1) ${i * 0.06}s`,
}}
onMouseEnter={(e) => setTooltip({ ...d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) =>
setTooltip((t) => (t ? { ...t, x: e.clientX, y: e.clientY } : null))
}
onMouseLeave={() => setTooltip(null)}
/>
<text
x={x + barW / 2}
y={y - 4}
textAnchor="middle"
fill="#8b949e"
fontSize={10}
>
{d.value}
</text>
<text
x={x + barW / 2}
y={H - 6}
textAnchor="middle"
fill="#484f58"
fontSize={10}
>
{d.label}
</text>
</g>
);
})}
</svg>
{tooltip && (
<div
className="fixed pointer-events-none bg-[#161b22] border border-[#30363d] rounded-lg px-3 py-2 text-[12px] shadow-lg z-50"
style={{ left: tooltip.x + 12, top: tooltip.y - 40 }}
>
<div className="font-semibold" style={{ color: tooltip.color }}>
{tooltip.label}
</div>
<div className="text-[#8b949e]">{tooltip.value} units</div>
</div>
)}
</div>
</div>
</div>
);
}
// Horizontal
const barH = 30,
gap = 12;
const totalH = DATA.length * (barH + gap);
const H = totalH + PAD.top + PAD.bottom;
const cW = W - PAD.left - PAD.right;
const xOf = (v: number) => PAD.left + (v / maxVal) * cW;
return (
<div className="min-h-screen bg-[#0d1117] p-6">
<div ref={wrapRef} className="w-full max-w-[800px] mx-auto">
<div className="flex gap-1 mb-4 justify-end">
{(["vertical", "horizontal"] as const).map((o) => (
<button
key={o}
onClick={() => setOrient(o)}
className={`text-[11px] px-3 py-1 rounded border transition-colors capitalize ${orient === o ? "bg-[#818cf8]/20 border-[#818cf8] text-[#818cf8]" : "border-[#30363d] text-[#8b949e] hover:border-[#8b949e]"}`}
>
{o}
</button>
))}
</div>
<div className="relative">
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className="w-full">
{Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal / 5) * t);
const x = xOf(v);
return (
<g key={t}>
<line
x1={x}
x2={x}
y1={PAD.top}
y2={PAD.top + totalH}
stroke="#21262d"
strokeWidth={1}
/>
<text
x={x}
y={PAD.top + totalH + 14}
textAnchor="middle"
fill="#484f58"
fontSize={10}
>
{v}
</text>
</g>
);
})}
{DATA.map((d, i) => {
const y = PAD.top + i * (barH + gap);
const bW = animated ? (d.value / maxVal) * cW : 0;
return (
<g key={d.label}>
<rect
x={PAD.left}
y={y}
width={bW}
height={barH}
rx={4}
fill={d.color}
style={{ transition: `width 0.5s cubic-bezier(.4,0,.2,1) ${i * 0.06}s` }}
onMouseEnter={(e) => setTooltip({ ...d, x: e.clientX, y: e.clientY })}
onMouseMove={(e) =>
setTooltip((t) => (t ? { ...t, x: e.clientX, y: e.clientY } : null))
}
onMouseLeave={() => setTooltip(null)}
/>
<text
x={PAD.left - 6}
y={y + barH / 2 + 4}
textAnchor="end"
fill="#484f58"
fontSize={10}
>
{d.label}
</text>
<text
x={PAD.left + bW + 4}
y={y + barH / 2 + 4}
textAnchor="start"
fill="#8b949e"
fontSize={10}
>
{d.value}
</text>
</g>
);
})}
</svg>
{tooltip && (
<div
className="fixed pointer-events-none bg-[#161b22] border border-[#30363d] rounded-lg px-3 py-2 text-[12px] shadow-lg z-50"
style={{ left: tooltip.x + 12, top: tooltip.y - 40 }}
>
<div className="font-semibold" style={{ color: tooltip.color }}>
{tooltip.label}
</div>
<div className="text-[#8b949e]">{tooltip.value} units</div>
</div>
)}
</div>
</div>
</div>
);
}<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
const DATA = [
{ label: "Electronics", value: 340, color: "#818cf8" },
{ label: "Clothing", value: 220, color: "#34d399" },
{ label: "Books", value: 185, color: "#f59e0b" },
{ label: "Sports", value: 260, color: "#f87171" },
{ label: "Home", value: 310, color: "#a78bfa" },
{ label: "Beauty", value: 145, color: "#38bdf8" },
];
const PAD = { top: 24, right: 20, bottom: 40, left: 52 };
const wrapEl = ref(null);
const W = ref(600);
const orient = ref("vertical");
const tooltip = ref(null);
const animated = ref(false);
let ro = null;
const maxVal = computed(() => Math.ceil(Math.max(...DATA.map((d) => d.value)) * 1.15));
onMounted(() => {
ro = new ResizeObserver(() => {
if (wrapEl.value) W.value = wrapEl.value.clientWidth - 32;
});
if (wrapEl.value) ro.observe(wrapEl.value);
requestAnimationFrame(() => (animated.value = true));
});
onUnmounted(() => {
if (ro) ro.disconnect();
});
function toggleOrient(o) {
orient.value = o;
animated.value = false;
requestAnimationFrame(() => (animated.value = true));
}
function showTooltip(d, e) {
tooltip.value = { ...d, x: e.clientX, y: e.clientY };
}
function moveTooltip(e) {
if (tooltip.value) tooltip.value = { ...tooltip.value, x: e.clientX, y: e.clientY };
}
function hideTooltip() {
tooltip.value = null;
}
// Vertical
const vH = computed(() => Math.round(W.value * 0.5));
const vCW = computed(() => W.value - PAD.left - PAD.right);
const vCH = computed(() => vH.value - PAD.top - PAD.bottom);
const vGap = 8;
const vBarW = computed(() => (vCW.value - vGap * (DATA.length - 1)) / DATA.length);
function vYOf(v) {
return PAD.top + vCH.value - (v / maxVal.value) * vCH.value;
}
const vTicks = computed(() =>
Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal.value / 5) * t);
return { v, y: vYOf(v) };
})
);
// Horizontal
const hBarH = 30;
const hGap = 12;
const hTotalH = DATA.length * (hBarH + hGap);
const hH = computed(() => hTotalH + PAD.top + PAD.bottom);
const hCW = computed(() => W.value - PAD.left - PAD.right);
function hXOf(v) {
return PAD.left + (v / maxVal.value) * hCW.value;
}
const hTicks = computed(() =>
Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal.value / 5) * t);
return { v, x: hXOf(v) };
})
);
</script>
<template>
<div class="page">
<div ref="wrapEl" class="wrap">
<div class="controls">
<button v-for="o in ['vertical','horizontal']" :key="o"
:class="['ctrl-btn', { active: orient === o }]" @click="toggleOrient(o)">{{ o }}</button>
</div>
<div class="chart-wrap">
<!-- Vertical -->
<svg v-if="orient === 'vertical'" :width="W" :height="vH" :viewBox="`0 0 ${W} ${vH}`">
<g v-for="tick in vTicks" :key="tick.v">
<line :x1="PAD.left" :x2="PAD.left+vCW" :y1="tick.y" :y2="tick.y" stroke="#21262d" stroke-width="1"/>
<text :x="PAD.left-6" :y="tick.y+3.5" text-anchor="end" fill="#484f58" font-size="10">{{ tick.v }}</text>
</g>
<g v-for="(d, i) in DATA" :key="d.label">
<rect :x="PAD.left + i * (vBarW + vGap)" :y="animated ? vYOf(d.value) : PAD.top + vCH"
:width="vBarW" :height="animated ? (d.value / maxVal) * vCH : 0" rx="4" :fill="d.color"
:style="{ transition: `y 0.5s cubic-bezier(.4,0,.2,1) ${i*0.06}s, height 0.5s cubic-bezier(.4,0,.2,1) ${i*0.06}s`, cursor: 'pointer' }"
@mouseenter="showTooltip(d, $event)" @mousemove="moveTooltip" @mouseleave="hideTooltip"/>
<text :x="PAD.left + i * (vBarW + vGap) + vBarW / 2" :y="vYOf(d.value) - 4" text-anchor="middle" fill="#8b949e" font-size="10">{{ d.value }}</text>
<text :x="PAD.left + i * (vBarW + vGap) + vBarW / 2" :y="vH - 6" text-anchor="middle" fill="#484f58" font-size="10">{{ d.label }}</text>
</g>
</svg>
<!-- Horizontal -->
<svg v-else :width="W" :height="hH" :viewBox="`0 0 ${W} ${hH}`">
<g v-for="tick in hTicks" :key="tick.v">
<line :x1="tick.x" :x2="tick.x" :y1="PAD.top" :y2="PAD.top+hTotalH" stroke="#21262d" stroke-width="1"/>
<text :x="tick.x" :y="PAD.top+hTotalH+14" text-anchor="middle" fill="#484f58" font-size="10">{{ tick.v }}</text>
</g>
<g v-for="(d, i) in DATA" :key="d.label">
<rect :x="PAD.left" :y="PAD.top + i * (hBarH + hGap)"
:width="animated ? (d.value / maxVal) * hCW : 0" :height="hBarH" rx="4" :fill="d.color"
:style="{ transition: `width 0.5s cubic-bezier(.4,0,.2,1) ${i*0.06}s`, cursor: 'pointer' }"
@mouseenter="showTooltip(d, $event)" @mousemove="moveTooltip" @mouseleave="hideTooltip"/>
<text :x="PAD.left - 6" :y="PAD.top + i * (hBarH + hGap) + hBarH / 2 + 4" text-anchor="end" fill="#484f58" font-size="10">{{ d.label }}</text>
<text :x="PAD.left + (animated ? (d.value / maxVal) * hCW : 0) + 4" :y="PAD.top + i * (hBarH + hGap) + hBarH / 2 + 4" text-anchor="start" fill="#8b949e" font-size="10">{{ d.value }}</text>
</g>
</svg>
<div v-if="tooltip" class="tip" :style="{ left: tooltip.x+12+'px', top: tooltip.y-40+'px' }">
<div class="tip-label" :style="{ color: tooltip.color }">{{ tooltip.label }}</div>
<div class="tip-val">{{ tooltip.value }} units</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page { min-height: 100vh; background: #0d1117; padding: 1.5rem; }
.wrap { width: 100%; max-width: 800px; margin: 0 auto; }
.controls { display: flex; gap: 4px; margin-bottom: 1rem; justify-content: flex-end; }
.ctrl-btn {
font-size: 11px; padding: 4px 12px; border-radius: 4px; border: 1px solid #30363d;
background: transparent; color: #8b949e; cursor: pointer; text-transform: capitalize;
transition: color 0.15s, border-color 0.15s, background 0.15s; font-family: inherit;
}
.ctrl-btn.active { background: rgba(129,140,248,0.2); border-color: #818cf8; color: #818cf8; }
.ctrl-btn:hover:not(.active) { border-color: #8b949e; }
.chart-wrap { position: relative; }
svg { width: 100%; overflow: visible; }
.tip {
position: fixed; pointer-events: none; background: #161b22; border: 1px solid #30363d;
border-radius: 0.5rem; padding: 0.5rem 0.75rem; font-size: 12px;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3); z-index: 50;
}
.tip-label { font-weight: 600; }
.tip-val { color: #8b949e; }
</style><script>
import { onMount, onDestroy } from "svelte";
const DATA = [
{ label: "Electronics", value: 340, color: "#818cf8" },
{ label: "Clothing", value: 220, color: "#34d399" },
{ label: "Books", value: 185, color: "#f59e0b" },
{ label: "Sports", value: 260, color: "#f87171" },
{ label: "Home", value: 310, color: "#a78bfa" },
{ label: "Beauty", value: 145, color: "#38bdf8" },
];
const PAD = { top: 24, right: 20, bottom: 40, left: 52 };
let wrapEl;
let W = 600;
let orient = "vertical";
let tooltip = null;
let animated = false;
let ro;
$: maxVal = Math.ceil(Math.max(...DATA.map((d) => d.value)) * 1.15);
onMount(() => {
ro = new ResizeObserver(() => {
if (wrapEl) W = wrapEl.clientWidth - 32;
});
if (wrapEl) ro.observe(wrapEl);
requestAnimationFrame(() => (animated = true));
});
onDestroy(() => {
if (ro) ro.disconnect();
});
function toggleOrient(o) {
orient = o;
animated = false;
requestAnimationFrame(() => (animated = true));
}
function showTooltip(d, e) {
tooltip = { ...d, x: e.clientX, y: e.clientY };
}
function moveTooltip(e) {
if (tooltip) tooltip = { ...tooltip, x: e.clientX, y: e.clientY };
}
function hideTooltip() {
tooltip = null;
}
// Vertical helpers
$: vH = Math.round(W * 0.5);
$: vCW = W - PAD.left - PAD.right;
$: vCH = vH - PAD.top - PAD.bottom;
$: vGap = 8;
$: vBarW = (vCW - vGap * (DATA.length - 1)) / DATA.length;
function vYOf(v) {
return PAD.top + vCH - (v / maxVal) * vCH;
}
$: vTicks = Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal / 5) * t);
return { v, y: vYOf(v) };
});
// Horizontal helpers
$: hBarH = 30;
$: hGap = 12;
$: hTotalH = DATA.length * (hBarH + hGap);
$: hH = hTotalH + PAD.top + PAD.bottom;
$: hCW = W - PAD.left - PAD.right;
function hXOf(v) {
return PAD.left + (v / maxVal) * hCW;
}
$: hTicks = Array.from({ length: 6 }, (_, t) => {
const v = Math.round((maxVal / 5) * t);
return { v, x: hXOf(v) };
});
</script>
<style>
.page { min-height: 100vh; background: #0d1117; padding: 1.5rem; }
.wrap { width: 100%; max-width: 800px; margin: 0 auto; }
.controls { display: flex; gap: 4px; margin-bottom: 1rem; justify-content: flex-end; }
.ctrl-btn {
font-size: 11px; padding: 4px 12px; border-radius: 4px; border: 1px solid #30363d;
background: transparent; color: #8b949e; cursor: pointer; text-transform: capitalize;
transition: color 0.15s, border-color 0.15s, background 0.15s; font-family: inherit;
}
.ctrl-btn.active { background: rgba(129,140,248,0.2); border-color: #818cf8; color: #818cf8; }
.ctrl-btn:hover:not(.active) { border-color: #8b949e; }
.chart-wrap { position: relative; }
svg { width: 100%; overflow: visible; }
.tip {
position: fixed; pointer-events: none; background: #161b22; border: 1px solid #30363d;
border-radius: 0.5rem; padding: 0.5rem 0.75rem; font-size: 12px;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3); z-index: 50;
}
.tip-label { font-weight: 600; }
.tip-val { color: #8b949e; }
</style>
<div class="page">
<div class="wrap" bind:this={wrapEl}>
<div class="controls">
{#each ['vertical','horizontal'] as o}
<button class="ctrl-btn {orient === o ? 'active' : ''}" on:click={() => toggleOrient(o)}>{o}</button>
{/each}
</div>
<div class="chart-wrap">
{#if orient === 'vertical'}
<svg width={W} height={vH} viewBox="0 0 {W} {vH}">
{#each vTicks as tick}
<line x1={PAD.left} x2={PAD.left+vCW} y1={tick.y} y2={tick.y} stroke="#21262d" stroke-width="1"/>
<text x={PAD.left-6} y={tick.y+3.5} text-anchor="end" fill="#484f58" font-size="10">{tick.v}</text>
{/each}
{#each DATA as d, i}
{@const x = PAD.left + i * (vBarW + vGap)}
{@const barH = (d.value / maxVal) * vCH}
{@const y = PAD.top + vCH - barH}
<rect {x} y={animated ? y : PAD.top + vCH} width={vBarW} height={animated ? barH : 0} rx="4" fill={d.color}
style="transition: y 0.5s cubic-bezier(.4,0,.2,1) {i*0.06}s, height 0.5s cubic-bezier(.4,0,.2,1) {i*0.06}s; cursor: pointer;"
on:mouseenter={e => showTooltip(d, e)} on:mousemove={moveTooltip} on:mouseleave={hideTooltip}/>
<text x={x+vBarW/2} y={y-4} text-anchor="middle" fill="#8b949e" font-size="10">{d.value}</text>
<text x={x+vBarW/2} y={vH-6} text-anchor="middle" fill="#484f58" font-size="10">{d.label}</text>
{/each}
</svg>
{:else}
<svg width={W} height={hH} viewBox="0 0 {W} {hH}">
{#each hTicks as tick}
<line x1={tick.x} x2={tick.x} y1={PAD.top} y2={PAD.top+hTotalH} stroke="#21262d" stroke-width="1"/>
<text x={tick.x} y={PAD.top+hTotalH+14} text-anchor="middle" fill="#484f58" font-size="10">{tick.v}</text>
{/each}
{#each DATA as d, i}
{@const y = PAD.top + i * (hBarH + hGap)}
{@const bW = animated ? (d.value / maxVal) * hCW : 0}
<rect x={PAD.left} {y} width={bW} height={hBarH} rx="4" fill={d.color}
style="transition: width 0.5s cubic-bezier(.4,0,.2,1) {i*0.06}s; cursor: pointer;"
on:mouseenter={e => showTooltip(d, e)} on:mousemove={moveTooltip} on:mouseleave={hideTooltip}/>
<text x={PAD.left-6} y={y+hBarH/2+4} text-anchor="end" fill="#484f58" font-size="10">{d.label}</text>
<text x={PAD.left+bW+4} y={y+hBarH/2+4} text-anchor="start" fill="#8b949e" font-size="10">{d.value}</text>
{/each}
</svg>
{/if}
{#if tooltip}
<div class="tip" style="left:{tooltip.x+12}px; top:{tooltip.y-40}px;">
<div class="tip-label" style="color:{tooltip.color}">{tooltip.label}</div>
<div class="tip-val">{tooltip.value} units</div>
</div>
{/if}
</div>
</div>
</div>Features
- Vertical & horizontal — toggle orientation with a button
- Animated bars — scale-in animation from zero on mount
- Value labels — inline labels on top of each bar
- Color palette — auto-assigned distinct colors per group
- Responsive — ResizeObserver redraws on container resize
- Hover highlight — individual bar highlight with tooltip
How it works
- Bar widths/heights are computed as a proportion of the max data value
- SVG
<rect>elements are rendered for each data point transform: scaleY()animates from 0 to 1 on mount- A floating tooltip shows the label and exact value on hover