レーダーチャート
SVGポリゴンで描く多角形レーダーチャート。2系列を重ねて中心から広がるアニメで表示し、スキルや評価の多軸比較に向きます。
ライブデモ
使用例(お題: アイドルグループ Sakura)
この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- Sakura:メンバー紹介ページ。能力をレーダーチャートで2名比較 -->
<section class="sr-stage">
<header class="sr-head">
<div class="sr-brand"><span class="sr-petal"></span> Sakura</div>
<span class="sr-tag">MEMBER STATS</span>
</header>
<div class="sr-body">
<div class="sr-chart">
<svg id="radar" class="sr-radar" viewBox="0 0 300 300" role="img" aria-label="メンバー能力のレーダーチャート">
<defs>
<linearGradient id="radarFillA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ff5e98" stop-opacity="0.55" />
<stop offset="100%" stop-color="#ffb3cd" stop-opacity="0.3" />
</linearGradient>
</defs>
<g id="radarGrid" class="sr-radar__grid"></g>
<g id="radarAxes" class="sr-radar__axes"></g>
<polygon id="radarPolyB" class="sr-radar__poly-b"></polygon>
<polygon id="radarPolyA" class="sr-radar__poly-a" fill="url(#radarFillA)"></polygon>
<g id="radarDots"></g>
<g id="radarLabels" class="sr-radar__labels"></g>
</svg>
</div>
<div class="sr-side">
<p class="sr-side__name">桜井 ひなの</p>
<p class="sr-side__role">センター ・ Vo / Dance</p>
<ul class="sr-legend">
<li><span class="dot a"></span>ひなの</li>
<li><span class="dot b"></span>グループ平均</li>
</ul>
<p class="sr-vote">今月のセンター投票 <b>12,840</b> 票</p>
</div>
</div>
</section>
CSS
/* Sakura:メンバー能力比較(SVGレーダーチャートが主役) */
:root {
--col-a: #ff5e98; /* 本人 */
--col-b: #c97a93; /* 平均 */
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Yu Gothic", "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
background:
radial-gradient(600px 380px at 30% 0%, #fff0f6 0%, transparent 60%),
linear-gradient(160deg, #ffe3ee 0%, #fff7fb 100%);
color: #6b3a4d;
}
.sr-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }
.sr-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.sr-brand { display: flex; align-items: center; gap: 9px; font-weight: 800; font-size: 16px; color: #d24f7f; }
.sr-petal {
width: 14px; height: 14px; background: #ffd1e0;
border-radius: 50% 0 50% 50%; transform: rotate(45deg);
box-shadow: 0 0 8px #ff8fb5;
}
.sr-tag {
font-size: 11px; color: #d24f7f; letter-spacing: 0.16em; font-weight: 700;
border: 1px solid rgba(255,143,181,0.5); padding: 4px 11px; border-radius: 14px;
}
.sr-body { flex: 1; display: flex; align-items: center; gap: 18px; }
/* レーダー本体 */
.sr-chart { flex: 0 0 auto; display: grid; place-items: center; }
.sr-radar { display: block; width: 232px; height: 232px; overflow: visible; }
.sr-radar__grid polygon { fill: none; stroke: rgba(210,79,127,0.16); stroke-width: 1; }
.sr-radar__axes line { stroke: rgba(210,79,127,0.2); stroke-width: 1; }
.sr-radar__labels text { fill: #b56b85; font-size: 11px; font-weight: 700; }
.sr-radar__poly-a {
stroke: var(--col-a); stroke-width: 2.5; stroke-linejoin: round;
filter: drop-shadow(0 4px 10px rgba(255,94,152,0.4));
}
.sr-radar__poly-b {
fill: rgba(201,122,147,0.12);
stroke: var(--col-b); stroke-width: 2; stroke-dasharray: 5 4; stroke-linejoin: round;
}
#radarDots circle { fill: #fff; stroke: var(--col-a); stroke-width: 2.5; }
/* 右側のメンバー情報 */
.sr-side { flex: 1; }
.sr-side__name { margin: 0; font-size: 19px; font-weight: 800; color: #d24f7f; }
.sr-side__role { margin: 3px 0 14px; font-size: 12px; color: #b56b85; letter-spacing: 0.04em; }
.sr-legend { list-style: none; display: flex; gap: 16px; margin: 0 0 14px; padding: 0; font-size: 12px; color: #8a5a6b; }
.sr-legend li { display: flex; align-items: center; gap: 6px; }
.sr-legend .dot { width: 11px; height: 11px; border-radius: 3px; }
.sr-legend .dot.a { background: var(--col-a); }
.sr-legend .dot.b { background: var(--col-b); }
.sr-vote {
margin: 0; font-size: 12px; color: #8a5a6b;
background: rgba(255,209,224,0.5); border: 1px solid rgba(255,143,181,0.4);
padding: 9px 12px; border-radius: 10px;
}
.sr-vote b { color: #d24f7f; font-size: 15px; }
JavaScript
// Sakura:メンバー能力を多角形レーダーで描画(本人 vs グループ平均、出現アニメ)
(() => {
const svg = document.getElementById('radar');
const gridG = document.getElementById('radarGrid');
const axesG = document.getElementById('radarAxes');
const polyA = document.getElementById('radarPolyA');
const polyB = document.getElementById('radarPolyB');
const dotsG = document.getElementById('radarDots');
const labelsG = document.getElementById('radarLabels');
if (!svg || !polyA || !polyB) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
const CX = 150, CY = 150, R = 105; // 中心と最大半径
const MAX = 100;
// 能力項目と2系列の値(a=本人, b=グループ平均, 0..100)
const axes = [
{ label: '歌唱', a: 92, b: 74 },
{ label: 'ダンス', a: 86, b: 78 },
{ label: 'トーク', a: 70, b: 66 },
{ label: '表現力', a: 95, b: 72 },
{ label: 'ビジュ', a: 88, b: 80 },
{ label: '人気', a: 90, b: 68 },
];
const N = axes.length;
// i番目の頂点座標(12時から時計回り)
function point(i, ratio) {
const ang = (-Math.PI / 2) + (i / N) * Math.PI * 2;
const r = R * ratio;
return { x: CX + Math.cos(ang) * r, y: CY + Math.sin(ang) * r };
}
// 同心の目盛りポリゴン(4段階)
const rings = 4;
for (let g = 1; g <= rings; g++) {
const ratio = g / rings;
const pts = [];
for (let i = 0; i < N; i++) {
const p = point(i, ratio);
pts.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
}
const poly = document.createElementNS(NS, 'polygon');
poly.setAttribute('points', pts.join(' '));
gridG.appendChild(poly);
}
// 軸線とラベル
for (let i = 0; i < N; i++) {
const edge = point(i, 1);
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', CX); line.setAttribute('y1', CY);
line.setAttribute('x2', edge.x); line.setAttribute('y2', edge.y);
axesG.appendChild(line);
const lp = point(i, 1.16);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', lp.x);
t.setAttribute('y', lp.y);
t.setAttribute('text-anchor', lp.x < CX - 5 ? 'end' : lp.x > CX + 5 ? 'start' : 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.textContent = axes[i].label;
labelsG.appendChild(t);
}
// 系列の頂点列を作成
function buildPoints(key, scale) {
return axes.map((ax, i) => {
const p = point(i, (ax[key] / MAX) * scale);
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
}).join(' ');
}
// 平均(B)は即表示、本人(A)はスケールで出現アニメ
polyB.setAttribute('points', buildPoints('b', 1));
axes.forEach((ax, i) => {
const p = point(i, ax.a / MAX);
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x); c.setAttribute('cy', p.y); c.setAttribute('r', 3.5);
const title = document.createElementNS(NS, 'title');
title.textContent = `${ax.label}: ${ax.a}`;
c.appendChild(title);
dotsG.appendChild(c);
});
const dotEls = Array.from(dotsG.children);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
polyA.setAttribute('points', buildPoints('a', 1));
return;
}
// 中心から広がるアニメーション
polyA.setAttribute('points', buildPoints('a', 0.001));
dotEls.forEach((d) => { d.style.opacity = '0'; d.style.transition = 'opacity .3s ease'; });
const start = performance.now();
const dur = 900;
function tick(now) {
const t = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
polyA.setAttribute('points', buildPoints('a', Math.max(0.001, eased)));
if (t < 1) {
requestAnimationFrame(tick);
} else {
dotEls.forEach((d, i) => setTimeout(() => { d.style.opacity = '1'; }, i * 60));
}
}
requestAnimationFrame(tick);
})();
コード
HTML
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">スキル評価レーダー</h2>
<p class="dv-sub">SVGポリゴンで描く多角形レーダーチャート(2系列比較)</p>
</figcaption>
<svg id="radar" class="dv-radar" viewBox="0 0 300 300"
role="img" aria-label="複数項目のスキル評価レーダーチャート">
<defs>
<linearGradient id="radarFillA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.55" />
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.3" />
</linearGradient>
</defs>
<g id="radarGrid" class="dv-radar__grid"></g>
<g id="radarAxes" class="dv-radar__axes"></g>
<polygon id="radarPolyB" class="dv-radar__poly-b"></polygon>
<polygon id="radarPolyA" class="dv-radar__poly-a" fill="url(#radarFillA)"></polygon>
<g id="radarDots"></g>
<g id="radarLabels" class="dv-radar__labels"></g>
</svg>
<ul class="dv-radar__legend">
<li><span class="dot a"></span>今期</li>
<li><span class="dot b"></span>前期</li>
</ul>
</figure>
</div>
CSS
:root {
--dv-radius: 18px;
--dv-ink: #e0e7ff;
--dv-sub: #a5b4fc;
--col-a: #38bdf8;
--col-b: #f472b6;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(800px 480px at 20% -10%, #312e81 0%, transparent 55%),
linear-gradient(160deg, #1e1b4b, #020617);
}
.dv-wrap { width: min(92vw, 560px); padding: 20px; }
.dv-card {
margin: 0;
padding: 18px 24px 16px;
border-radius: var(--dv-radius);
background: rgba(30, 27, 75, 0.45);
border: 1px solid rgba(129, 140, 248, 0.22);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
text-align: center;
}
.dv-head { margin-bottom: 8px; text-align: left; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-radar {
display: block;
width: min(100%, 232px);
height: auto;
margin: 2px auto 0;
overflow: visible;
}
.dv-radar__grid polygon {
fill: none;
stroke: rgba(165, 180, 252, 0.18);
stroke-width: 1;
}
.dv-radar__axes line { stroke: rgba(165, 180, 252, 0.22); stroke-width: 1; }
.dv-radar__labels text { fill: var(--dv-sub); font-size: 11px; }
.dv-radar__poly-a {
stroke: var(--col-a);
stroke-width: 2.5;
stroke-linejoin: round;
filter: drop-shadow(0 4px 10px rgba(56, 189, 248, 0.4));
}
.dv-radar__poly-b {
fill: rgba(244, 114, 182, 0.14);
stroke: var(--col-b);
stroke-width: 2;
stroke-dasharray: 5 4;
stroke-linejoin: round;
}
#radarDots circle { fill: #1e1b4b; stroke: var(--col-a); stroke-width: 2.5; }
.dv-radar__legend {
list-style: none;
display: flex;
gap: 18px;
justify-content: center;
margin: 8px 0 0;
padding: 0;
font-size: 13px;
color: var(--dv-sub);
}
.dv-radar__legend li { display: flex; align-items: center; gap: 6px; }
.dv-radar__legend .dot { width: 11px; height: 11px; border-radius: 3px; }
.dv-radar__legend .dot.a { background: var(--col-a); }
.dv-radar__legend .dot.b { background: var(--col-b); }
JavaScript
// SVGで多角形レーダーチャートを生成。グリッド・軸・2系列・出現アニメ付き
(() => {
const svg = document.getElementById('radar');
const gridG = document.getElementById('radarGrid');
const axesG = document.getElementById('radarAxes');
const polyA = document.getElementById('radarPolyA');
const polyB = document.getElementById('radarPolyB');
const dotsG = document.getElementById('radarDots');
const labelsG = document.getElementById('radarLabels');
if (!svg || !polyA || !polyB) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
const CX = 150, CY = 150, R = 110; // 中心と最大半径
const MAX = 100;
// 項目と2系列の値(0..100)
const axes = [
{ label: '技術力', a: 90, b: 70 },
{ label: '速度', a: 75, b: 60 },
{ label: '品質', a: 85, b: 78 },
{ label: '協調性', a: 70, b: 65 },
{ label: '創造性', a: 88, b: 55 },
{ label: '継続性', a: 78, b: 72 },
];
const N = axes.length;
// i番目の頂点座標(12時から時計回り、valは割合)
function point(i, ratio) {
const ang = (-Math.PI / 2) + (i / N) * Math.PI * 2;
const r = R * ratio;
return { x: CX + Math.cos(ang) * r, y: CY + Math.sin(ang) * r };
}
// 同心の目盛りポリゴン(4段階)
const rings = 4;
for (let g = 1; g <= rings; g++) {
const ratio = g / rings;
const pts = [];
for (let i = 0; i < N; i++) {
const p = point(i, ratio);
pts.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
}
const poly = document.createElementNS(NS, 'polygon');
poly.setAttribute('points', pts.join(' '));
gridG.appendChild(poly);
}
// 軸線とラベル
for (let i = 0; i < N; i++) {
const edge = point(i, 1);
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', CX); line.setAttribute('y1', CY);
line.setAttribute('x2', edge.x); line.setAttribute('y2', edge.y);
axesG.appendChild(line);
const lp = point(i, 1.18);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', lp.x);
t.setAttribute('y', lp.y);
t.setAttribute('text-anchor', lp.x < CX - 5 ? 'end' : lp.x > CX + 5 ? 'start' : 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.textContent = axes[i].label;
labelsG.appendChild(t);
}
// 系列の頂点列を作成
function buildPoints(key, scale) {
return axes.map((ax, i) => {
const p = point(i, (ax[key] / MAX) * scale);
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
}).join(' ');
}
// 前期(B)は即表示、今期(A)はスケールで出現アニメ
polyB.setAttribute('points', buildPoints('b', 1));
axes.forEach((ax, i) => {
const p = point(i, ax.a / MAX);
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x); c.setAttribute('cy', p.y); c.setAttribute('r', 3.5);
const title = document.createElementNS(NS, 'title');
title.textContent = `${ax.label}: ${ax.a}`;
c.appendChild(title);
dotsG.appendChild(c);
});
const dotEls = Array.from(dotsG.children);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
polyA.setAttribute('points', buildPoints('a', 1));
return;
}
// 中心から広がるアニメーション
polyA.setAttribute('points', buildPoints('a', 0.001));
dotEls.forEach((d) => { d.style.opacity = '0'; d.style.transition = 'opacity .3s ease'; });
const start = performance.now();
const dur = 900;
function tick(now) {
const t = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
polyA.setAttribute('points', buildPoints('a', Math.max(0.001, eased)));
if (t < 1) {
requestAnimationFrame(tick);
} else {
dotEls.forEach((d, i) => setTimeout(() => { d.style.opacity = '1'; }, i * 60));
}
}
requestAnimationFrame(tick);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「レーダーチャート」の効果を追加してください。
# 追加してほしい効果
レーダーチャート(データ可視化)
SVGポリゴンで描く多角形レーダーチャート。2系列を重ねて中心から広がるアニメで表示し、スキルや評価の多軸比較に向きます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="dv-wrap">
<figure class="dv-card">
<figcaption class="dv-head">
<h2 class="dv-title">スキル評価レーダー</h2>
<p class="dv-sub">SVGポリゴンで描く多角形レーダーチャート(2系列比較)</p>
</figcaption>
<svg id="radar" class="dv-radar" viewBox="0 0 300 300"
role="img" aria-label="複数項目のスキル評価レーダーチャート">
<defs>
<linearGradient id="radarFillA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.55" />
<stop offset="100%" stop-color="#818cf8" stop-opacity="0.3" />
</linearGradient>
</defs>
<g id="radarGrid" class="dv-radar__grid"></g>
<g id="radarAxes" class="dv-radar__axes"></g>
<polygon id="radarPolyB" class="dv-radar__poly-b"></polygon>
<polygon id="radarPolyA" class="dv-radar__poly-a" fill="url(#radarFillA)"></polygon>
<g id="radarDots"></g>
<g id="radarLabels" class="dv-radar__labels"></g>
</svg>
<ul class="dv-radar__legend">
<li><span class="dot a"></span>今期</li>
<li><span class="dot b"></span>前期</li>
</ul>
</figure>
</div>
【CSS】
:root {
--dv-radius: 18px;
--dv-ink: #e0e7ff;
--dv-sub: #a5b4fc;
--col-a: #38bdf8;
--col-b: #f472b6;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
color: var(--dv-ink);
background:
radial-gradient(800px 480px at 20% -10%, #312e81 0%, transparent 55%),
linear-gradient(160deg, #1e1b4b, #020617);
}
.dv-wrap { width: min(92vw, 560px); padding: 20px; }
.dv-card {
margin: 0;
padding: 18px 24px 16px;
border-radius: var(--dv-radius);
background: rgba(30, 27, 75, 0.45);
border: 1px solid rgba(129, 140, 248, 0.22);
box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.75);
backdrop-filter: blur(6px);
text-align: center;
}
.dv-head { margin-bottom: 8px; text-align: left; }
.dv-title { margin: 0; font-size: clamp(18px, 3.4vw, 22px); }
.dv-sub { margin: 4px 0 0; font-size: 13px; color: var(--dv-sub); }
.dv-radar {
display: block;
width: min(100%, 232px);
height: auto;
margin: 2px auto 0;
overflow: visible;
}
.dv-radar__grid polygon {
fill: none;
stroke: rgba(165, 180, 252, 0.18);
stroke-width: 1;
}
.dv-radar__axes line { stroke: rgba(165, 180, 252, 0.22); stroke-width: 1; }
.dv-radar__labels text { fill: var(--dv-sub); font-size: 11px; }
.dv-radar__poly-a {
stroke: var(--col-a);
stroke-width: 2.5;
stroke-linejoin: round;
filter: drop-shadow(0 4px 10px rgba(56, 189, 248, 0.4));
}
.dv-radar__poly-b {
fill: rgba(244, 114, 182, 0.14);
stroke: var(--col-b);
stroke-width: 2;
stroke-dasharray: 5 4;
stroke-linejoin: round;
}
#radarDots circle { fill: #1e1b4b; stroke: var(--col-a); stroke-width: 2.5; }
.dv-radar__legend {
list-style: none;
display: flex;
gap: 18px;
justify-content: center;
margin: 8px 0 0;
padding: 0;
font-size: 13px;
color: var(--dv-sub);
}
.dv-radar__legend li { display: flex; align-items: center; gap: 6px; }
.dv-radar__legend .dot { width: 11px; height: 11px; border-radius: 3px; }
.dv-radar__legend .dot.a { background: var(--col-a); }
.dv-radar__legend .dot.b { background: var(--col-b); }
【JavaScript】
// SVGで多角形レーダーチャートを生成。グリッド・軸・2系列・出現アニメ付き
(() => {
const svg = document.getElementById('radar');
const gridG = document.getElementById('radarGrid');
const axesG = document.getElementById('radarAxes');
const polyA = document.getElementById('radarPolyA');
const polyB = document.getElementById('radarPolyB');
const dotsG = document.getElementById('radarDots');
const labelsG = document.getElementById('radarLabels');
if (!svg || !polyA || !polyB) return; // null安全
const NS = 'http://www.w3.org/2000/svg';
const CX = 150, CY = 150, R = 110; // 中心と最大半径
const MAX = 100;
// 項目と2系列の値(0..100)
const axes = [
{ label: '技術力', a: 90, b: 70 },
{ label: '速度', a: 75, b: 60 },
{ label: '品質', a: 85, b: 78 },
{ label: '協調性', a: 70, b: 65 },
{ label: '創造性', a: 88, b: 55 },
{ label: '継続性', a: 78, b: 72 },
];
const N = axes.length;
// i番目の頂点座標(12時から時計回り、valは割合)
function point(i, ratio) {
const ang = (-Math.PI / 2) + (i / N) * Math.PI * 2;
const r = R * ratio;
return { x: CX + Math.cos(ang) * r, y: CY + Math.sin(ang) * r };
}
// 同心の目盛りポリゴン(4段階)
const rings = 4;
for (let g = 1; g <= rings; g++) {
const ratio = g / rings;
const pts = [];
for (let i = 0; i < N; i++) {
const p = point(i, ratio);
pts.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
}
const poly = document.createElementNS(NS, 'polygon');
poly.setAttribute('points', pts.join(' '));
gridG.appendChild(poly);
}
// 軸線とラベル
for (let i = 0; i < N; i++) {
const edge = point(i, 1);
const line = document.createElementNS(NS, 'line');
line.setAttribute('x1', CX); line.setAttribute('y1', CY);
line.setAttribute('x2', edge.x); line.setAttribute('y2', edge.y);
axesG.appendChild(line);
const lp = point(i, 1.18);
const t = document.createElementNS(NS, 'text');
t.setAttribute('x', lp.x);
t.setAttribute('y', lp.y);
t.setAttribute('text-anchor', lp.x < CX - 5 ? 'end' : lp.x > CX + 5 ? 'start' : 'middle');
t.setAttribute('dominant-baseline', 'middle');
t.textContent = axes[i].label;
labelsG.appendChild(t);
}
// 系列の頂点列を作成
function buildPoints(key, scale) {
return axes.map((ax, i) => {
const p = point(i, (ax[key] / MAX) * scale);
return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
}).join(' ');
}
// 前期(B)は即表示、今期(A)はスケールで出現アニメ
polyB.setAttribute('points', buildPoints('b', 1));
axes.forEach((ax, i) => {
const p = point(i, ax.a / MAX);
const c = document.createElementNS(NS, 'circle');
c.setAttribute('cx', p.x); c.setAttribute('cy', p.y); c.setAttribute('r', 3.5);
const title = document.createElementNS(NS, 'title');
title.textContent = `${ax.label}: ${ax.a}`;
c.appendChild(title);
dotsG.appendChild(c);
});
const dotEls = Array.from(dotsG.children);
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
polyA.setAttribute('points', buildPoints('a', 1));
return;
}
// 中心から広がるアニメーション
polyA.setAttribute('points', buildPoints('a', 0.001));
dotEls.forEach((d) => { d.style.opacity = '0'; d.style.transition = 'opacity .3s ease'; });
const start = performance.now();
const dur = 900;
function tick(now) {
const t = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
polyA.setAttribute('points', buildPoints('a', Math.max(0.001, eased)));
if (t < 1) {
requestAnimationFrame(tick);
} else {
dotEls.forEach((d, i) => setTimeout(() => { d.style.opacity = '1'; }, i * 60));
}
}
requestAnimationFrame(tick);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。