読み込み中のゆるライフハック
コチョン「メモは覚えようと思って手書きすると記憶に残りやすいっていわれているよ」

サイトでたまに見るような評価レーダーチャート(WordPressのカスタムHTMLブロックに貼り付けるだけで表示可能)

スポンサーリンク

当サイトのゲームレビューで使っているこういうやつです。(実物ではありませんが、画像拡大したのでぼやけています💦)

記事内のカスタムHTMLブロックに貼り付けるだけで使い勝手が良く、しかも軽量で設定も楽なため、ものぐさな筆者はメインで使っています。

自分用に調整した簡易的なチャートですが、よろしければお使いください。

以下にコードを格納しています。コピーしてお使いください。

※Ctrl+Aで全選択してCtrl+Cでコピー、Ctrl+VでカスタムHTMLブロック編集内にペッ!とペーストすると楽です。

ここを開くとコードが表示されます
<div class="xr-radar-wrap">
<div class="xr-radar" data-values="4.7,3.7,3.6,2.8" data-labels="ストーリー,システム,グラフィック,サウンド" data-max="5" data-title="STATUS" data-type="GAME DATA" data-rank="on"></div>
</div>

<style>
.xr-radar-wrap {
width: 100%;
max-width: 480px;
margin: 0 auto;
font-family:
"Press Start 2P",
"VT323",
"Courier New",
monospace;
}

.xr-radar-svg {
width: 100%;
height: auto;
display: block;
image-rendering: pixelated;
}

.xr-radar-status-diamond {
fill: #1b2350;
stroke: #133;
stroke-width: 6;
shape-rendering: crispEdges;
}

.xr-radar-status-text {
fill: #f8f8f8;
font-size: 12px;
font-weight: bold;
letter-spacing: 0.08em;
dominant-baseline: middle;
}

.xr-radar-outer-frame {
fill: none;
stroke: #111;
stroke-width: 3;
shape-rendering: crispEdges;
}

.xr-radar-inner-frame {
fill: transparent;
stroke: #323;
stroke-width: 3;
shape-rendering: crispEdges;
}

.xr-radar-grid polygon,
.xr-radar-grid line {
fill: none;
stroke: #566;
stroke-width: 0.7;
shape-rendering: crispEdges;
}

.xr-radar-grid polygon:first-child {
stroke: #222;
stroke-width: 2;
}

.xr-radar-shape-shadow {
fill: rgba(0, 0, 10, 0.1);
}

.xr-radar-shape {
fill: rgba(140, 210, 255, 0.28);
stroke: none;
shape-rendering: crispEdges;
}

.xr-radar-point {
fill: #111;
stroke: #111;
stroke-width: 1;
shape-rendering: crispEdges;
}

.xr-radar-label {
font-size: 12px;
fill: #111;
font-weight: bold;
dominant-baseline: middle;
letter-spacing: 0.04em;
}

.xr-radar-value {
font-size: 14px;
fill: #111;
font-weight: bold;
dominant-baseline: middle;
}

.xr-radar-info text {
font-size: 12px;
fill: #111;
font-weight: bold;
}

.xr-radar-rank-value {
fill: #111;
}

@media (max-width: 480px) {
.xr-radar-status-text,
.xr-radar-label {
font-size: 10px;
}

.xr-radar-value,
.xr-radar-info text {
font-size: 10px;
}
}
</style>

<script>
(function () {
const SVG_NS = "http://www.w3.org/2000/svg";

function svgEl(tag, attrs = {}, text = "") {
const el = document.createElementNS(SVG_NS, tag);
for (const key in attrs) el.setAttribute(key, attrs[key]);
if (text) el.textContent = text;
return el;
}

function clamp(value, min, max) {
return Math.max(min, Math.min(value, max));
}

function formatValue(value) {
const n = Math.round(value * 10) / 10;
return Number.isInteger(n) ? String(Math.round(n)) : String(n);
}

function textAnchor(x, centerX, deadZone) {
if (x > centerX + deadZone) return "start";
if (x < centerX - deadZone) return "end";
return "middle";
}

function getRank(avg, maxVal) {
const normalized = (avg / maxVal) * 5;
if (normalized >= 4.4) return "S";
if (normalized >= 3.7) return "A";
if (normalized >= 3.3) return "B+";
if (normalized >= 3.0) return "B";
if (normalized >= 2.0) return "C";
return "D";
}

function ringPoints(count, cx, cy, radius, rate) {
const startAngle = -Math.PI / 2;
return Array.from({ length: count }, (_, i) => {
const angle = startAngle + (Math.PI * 2 * i) / count;
return [
cx + Math.cos(angle) * radius * rate,
cy + Math.sin(angle) * radius * rate
];
});
}

function polygonPoints(values, cx, cy, radius, maxVal) {
const count = values.length;
const startAngle = -Math.PI / 2;

return values.map((value, i) => {
const angle = startAngle + (Math.PI * 2 * i) / count;
const rate = clamp(value, 0, maxVal) / maxVal;
return [
cx + Math.cos(angle) * radius * rate,
cy + Math.sin(angle) * radius * rate
];
});
}

function pointsStr(points) {
return points.map((p) => `${p[0]},${p[1]}`).join(" ");
}

function buildChart(chart) {
if (chart.dataset.xrBuilt === "1") return;
chart.dataset.xrBuilt = "1";

const values = (chart.dataset.values || "")
.split(",")
.map(v => Number(v.trim()))
.filter(v => !Number.isNaN(v));

const labels = (chart.dataset.labels || "")
.split(",")
.map(v => v.trim())
.filter(Boolean);

const maxVal = Number(chart.dataset.max || 5);
const titleText = chart.dataset.title || "STATUS";
const typeText = chart.dataset.type || "GAME DATA";
const rankOn = (chart.dataset.rank || "on") !== "off";

if (values.length < 4 || values.length > 6) {
chart.innerHTML = "<p style='font-size:12px;'>レーダーチャートは4~6項目で指定してください。</p>";
return;
}

if (values.length !== labels.length) {
chart.innerHTML = "<p style='font-size:12px;'>data-values と data-labels の数を一致させてください。</p>";
return;
}

const cx = 150;
const cy = 180;
const radius = 115;
const valueRadius = 135;
const labelRadius = 150;
const gridLevels = 5;
const deadZone = 8;

const labelAdjust = [
{ dx: 0, dy: 8, fontSize: 13 },
{ dx: -25, dy: -14, fontSize: 13 },
{ dx: 2, dy: -19, fontSize: 13 },
{ dx: 25, dy: -15, fontSize: 13 },
{ dx: 0, dy: 0, fontSize: 12 },
{ dx: 0, dy: 0, fontSize: 12 }
];

const valueAdjust = [
{ dx: -1, dy: 9, fontSize: 14 },
{ dx: 0, dy: 2, fontSize: 14 },
{ dx: 0, dy: 12, fontSize: 14 },
{ dx: 0, dy: 0, fontSize: 14 },
{ dx: 0, dy: 0, fontSize: 13 },
{ dx: 0, dy: 0, fontSize: 13 }
];

const finalPoints = polygonPoints(values, cx, cy, radius, maxVal);

const svg = svgEl("svg", {
viewBox: "-100 -40 500 440",
class: "xr-radar-svg",
"aria-label": `${values.length}項目レーダーチャート`
});

svg.appendChild(svgEl("polygon", {
points: "150,-25 195,-10 150,1 105,-10",
class: "xr-radar-status-diamond"
}));

svg.appendChild(svgEl("text", {
x: 150,
y: -10,
"text-anchor": "middle",
class: "xr-radar-status-text"
}, titleText));

svg.appendChild(svgEl("rect", {
x: -77.8, y: 10, width: 453, height: 340,
class: "xr-radar-outer-frame"
}));

svg.appendChild(svgEl("rect", {
x: -70, y: 19.6, width: 437, height: 320,
class: "xr-radar-inner-frame"
}));

const gridGroup = svgEl("g", { class: "xr-radar-grid" });

for (let level = 1; level <= gridLevels; level++) {
const rate = (gridLevels - level + 1) / gridLevels;
gridGroup.appendChild(svgEl("polygon", {
points: pointsStr(ringPoints(values.length, cx, cy, radius, rate))
}));
}

ringPoints(values.length, cx, cy, radius, 1).forEach((p) => {
gridGroup.appendChild(svgEl("line", {
x1: cx,
y1: cy,
x2: p[0],
y2: p[1]
}));
});

svg.appendChild(gridGroup);

svg.appendChild(svgEl("polygon", {
class: "xr-radar-shape-shadow",
points: pointsStr(finalPoints)
}));

svg.appendChild(svgEl("polygon", {
class: "xr-radar-shape",
points: pointsStr(finalPoints)
}));

const pointGroup = svgEl("g");
const pointSize = 6;
const half = pointSize / 2;

finalPoints.forEach((p) => {
pointGroup.appendChild(svgEl("rect", {
x: p[0] - half,
y: p[1] - half,
width: pointSize,
height: pointSize,
class: "xr-radar-point"
}));
});

svg.appendChild(pointGroup);

const valueGroup = svgEl("g");
const valueBasePoints = ringPoints(values.length, cx, cy, valueRadius, 1);

labels.forEach((_, i) => {
const base = valueBasePoints[i];
const adj = valueAdjust[i] || { dx: 0, dy: 0, fontSize: 14 };
const x = base[0] + adj.dx;
const y = base[1] + adj.dy;

const text = svgEl("text", {
x,
y,
"text-anchor": textAnchor(x, cx, deadZone),
class: "xr-radar-value"
}, formatValue(values[i]));

text.style.fontSize = adj.fontSize + "px";
valueGroup.appendChild(text);
});

svg.appendChild(valueGroup);

const labelGroup = svgEl("g");
const labelBasePoints = ringPoints(values.length, cx, cy, labelRadius, 1);

labels.forEach((label, i) => {
const base = labelBasePoints[i];
const adj = labelAdjust[i] || { dx: 0, dy: 0, fontSize: 12 };
const x = base[0] + adj.dx;
const y = base[1] + adj.dy;

const text = svgEl("text", {
x,
y,
"text-anchor": textAnchor(x, cx, deadZone),
class: "xr-radar-label"
}, label);

text.style.fontSize = adj.fontSize + "px";
labelGroup.appendChild(text);
});

svg.appendChild(labelGroup);

const infoGroup = svgEl("g", { class: "xr-radar-info" });
const average = values.reduce((sum, v) => sum + v, 0) / values.length;
const rank = getRank(average, maxVal);

if (rankOn) {
const rankText = svgEl("text", {
x: 150,
y: 376,
"text-anchor": "middle"
});

rankText.style.fontSize = "17px";
rankText.appendChild(document.createTextNode("RANK "));
rankText.appendChild(svgEl("tspan", { class: "xr-radar-rank-value" }, rank));
infoGroup.appendChild(rankText);
}

const typeNode = svgEl("text", {
x: 150,
y: rankOn ? 398 : 388,
"text-anchor": "middle"
}, typeText);

typeNode.style.fontSize = "13px";
infoGroup.appendChild(typeNode);

svg.appendChild(infoGroup);

chart.innerHTML = "";
chart.appendChild(svg);
}

function initRadarCharts() {
document.querySelectorAll(".xr-radar").forEach(buildChart);
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initRadarCharts);
} else {
initRadarCharts();
}
})();
</script>

◆使い方の流れ。

〇WordPressの記事編集画面で「カスタムHTML」ブロックを追加し、入力欄にコードを貼り付けてください。

貼り付けた後はレビューや内容に合わせて、任意の数字や項目へと変更してください。

※項目と数値は4~6まで増やせます(初期状態では項目は4つ、数字も5をMAXに見立てた適当な値にしています)

説明書的なもの(以下はカスタマイズ方法を簡単にまとめたものです。メモ帳などにコピペしてご利用ください)

◆「こんなのいちいち読んでられるか!」という人は◆

・とりあえず、コード最初のこの部分だけ見てください。

<div class="xr-radar-wrap">
<div class="xr-radar" data-values="4.7,3.7,3.6,2.8" data-labels="ストーリー,システム,グラフィック,サウンド" data-max="5" data-title="STATUS" data-type="GAME DATA" data-rank="on"></div>
</div>

この中の……

・data-values → 点数(スコア)
・data-labels → 項目の名前

この2つを変えるだけでもチャートは使えます。

※まずはdata-valuesとdata-labelsだけ変更して動かしてみるのがおすすめです

〇それぞれのスコアの変え方
・上記のdata-values="4.7,3.7,3.6,2.8"の数字を変えると表示スコアが変わります。
例 "4.7,3.7,3.6,2.8"→"1,2,3,4.5"のような形でもOKです。
サンプルでは小数で入れておりますが、上記のように整数でも問題ありません。

〇項目名の変え方
・data-labels="ストーリー,システム,グラフィック,サウンド"の各名を変更することでチャートの項目名を変更できます。
例 data-labels="ストーリー,システム,グラフィック,サウンド"→data-labels="味,サービス,待ち時間,値段"。

・data-title="STATUS" data-type="GAME DATA"の各名変更も同じ感じでいじれます。
例 data-title="STATUS" data-type="GAME DATA"→ data-title="総評" data-type="お店の評価"

〇5、6項目に変更したい
・HTML内の .xr-radar にある
「data-values」と「data-labels」を編集してください。

◇4項目例
data-values="4.7,3.7,3.6,2.8"
data-labels="ストーリー,システム,グラフィック,サウンド"

◇5項目例
data-values="4.7,3.7,3.6,2.8,4.2"
data-labels="ストーリー,システム,グラフィック,サウンド,快適性"

◇6項目例
data-values="4.7,3.7,3.6,2.8,4.2,3.9"
data-labels="ストーリー,システム,グラフィック,サウンド,快適性,ボリューム"

※data-valuesとdata-labelsの数は必ず一致させてください。合っていないとチャートが表示されません。(数値が5つなのに、項目の数は4など)

〇〇項目と数値の移動調整

・位置をカスタマイズしたい時は以下の箇所で値を変更してください。
特に項目数を増やすと表示位置がかぶったりずれたりして、思った通りの位置にならないことがあります。

〇基本の操作

・dx→左右の位置調整(+の値で右に移動、-値で左に移動)
 dy→上下の位置調整(+値で下に移動、-値で上に移動)
 fontSize→文字サイズ

・ストーリーやシステムなどの項目位置(各項目は上から時計回り順)

const labelAdjust = [
{ dx: 0, dy: 8, fontSize: 13 }, ←上
{ dx: -25, dy: -14, fontSize: 13 }, ←右
{ dx: 2, dy: -19, fontSize: 13 }, ←下
{ dx: 25, dy: -15, fontSize: 13 }, ←左
{ dx: 0, dy: 0, fontSize: 12 }, ←5項目の時に調整
{ dx: 0, dy: 0, fontSize: 12 } ←6項目の時に調整
];

・スコアの位置(各数値は上から時計回り順)

const valueAdjust = [
{ dx: -1, dy: 9, fontSize: 14 }, ←上
{ dx: 0, dy: 2, fontSize: 14 }, ←右
{ dx: 0, dy: 12, fontSize: 14 }, ←下
{ dx: 0, dy: 0, fontSize: 14 }, ←左
{ dx: 0, dy: 0, fontSize: 13 }, ←5項目の時に調整
{ dx: 0, dy: 0, fontSize: 13 } ←6項目の時に調整
];

・ありがたいことにカスタムHTMLの編集画面ではプレビューがでてくるので、画面上で実際に確認しながら作業できます。「ここらへんかな」という風に値を変えながら調節してみてください。

・以下の箇所でチャートや項目の一括の位置調整もできますが、各項目と数値の微妙な位置調整は上のやつで行った方が無難です。(一応)

  const cx = 150; チャート・数値・項目丸ごと左右移動
const cy = 180; 〃上下移動
const radius = 115; チャートサイズ変更
const valueRadius = 135; 値を変えると数値が一括移動
const labelRadius = 150; 値を変えると項目が一括移動
const gridLevels = 5; 値を変えると中の線が増減

〇〇〇ランク基準の調整

・ランク表示はチャート全体の平均値をもとにして決まります。
・基準を変えたい場合は、まず以下のif文を探してください。(Ctrl+Fでfunction getRankを検索窓に貼り付けると一発で見つかるので楽です)

function getRank(avg, maxVal) {
const normalized = (avg / maxVal) * 5;
if (normalized >= 4.4) return "S";
if (normalized >= 3.7) return "A";
if (normalized >= 3.3) return "B+";
if (normalized >= 3.0) return "B";
if (normalized >= 2.0) return "C";
return "D";
}

・if (normalized >= 4.4) return "S";
↑これは最終的な評価値(normalized)が4.4以上なら「S」と表示するという意味です。

・もちろん10点方式・100点方式に変更したいこともあると思うので、その場合は以下のように変更してください。

◆10点方式

①最初の方の「data-max="5"」を「data-max="10"」に変更

②「const normalized = (avg / maxVal) * 5;」を「const normalized = avg;」に変更。

③最後にランク基準も10点に合わせて変更します。

例 if (normalized >= 8.8) return "S";
  if (normalized >= 7.0) return "A";
  if (normalized >= 6.6) return "B+";
  if (normalized >= 6.0) return "B";
  if (normalized >= 4.0) return "C";
  return "D";

◆100点方式

①最初の方の「data-max="5"」を「data-max="100"」に変更

②「const normalized = (avg / maxVal) * 5;」を「const normalized = avg;」に変更。

③最後にランク基準も100点に合わせて変更します。

例 if (normalized >= 89) return "S";
  if (normalized >= 74) return "A";
  if (normalized >= 66) return "B+";
  if (normalized >= 60) return "B";
  if (normalized >= 40) return "C";
  return "D";

・ポイント

100点方式だと、デフォルト状態より項目を細かくした方がいいかもしれません。たとえば……

if (normalized >= 100) return "Perfect";
if (normalized >= 95) return "S";
if (normalized >= 88) return "A+";
if (normalized >= 80) return "A";
if (normalized >= 74) return "B+";

……のように項目を増加した方がいいかもです(数値やアルファベットは適当です)

※全角文字や余分なスペースが混ざると正しく表示されない場合があります。意外と見落としやすいポイントなので、コードはコピーして使用することをおすすめします。

終わり。

――このチャート本体や説明書の内容は追加・削除・変更するかもしれません。今書かれているものを最新版としてお使いください。

バージョン1.3(1.2から文言を追加変更)

なお、ご利用のテーマやプラグイン、記述環境によってはうまく表示されない可能性があります。

コードはご自身のサイト用に自由にカスタマイズなどを行っていただいてかまいませんが、動作保証はしておらず、利用により生じた損害について責任は負いかねます。

また「表示されないんだけど……」など、個別環境での不具合については、筆者は専門対応ができませんので、あらかじめご了承ください。

項目の位置やサイズの調整に関する程度でしたらお答えできる場合もありますが、必ずしも返信や対応をお約束するものではありません。

了。

コメント

タイトルとURLをコピーしました