レーダーチャート

SVGポリゴンで描く多角形レーダーチャート。2系列を重ねて中心から広がるアニメで表示し、スキルや評価の多軸比較に向きます。

#svg#radar#chart#animation

ライブデモ

使用例(お題: アイドルグループ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。