読み込み中の一言
コチョン「こういう演出の時に一息入れるのも大事なんだよ、覚えておこうね」
カエデ「気持ちを整理する時間を意識的に作ろう」

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

スポンサーリンク

当サイトのゲームレビューで使用しているこういうやつです。

軽量なうえ、記事内のカスタムHTMLブロックに張り付けるだけなので使い勝手が良く、筆者も汎用で使っています。

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

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

※Ctrl+Aで全選択してCtrl+Cでコピー、Ctrl+Vでペーストすると楽です。

コード
<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: -78, y: 10, width: 453, height: 340,
class: "xr-radar-outer-frame"
}));

svg.appendChild(svgEl("rect", {
x: -70, y: 18, 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>

◆使い方の流れ。

〇ワードプレスの記事編集画面でカスタムHTMLブロックを呼び出す→HTMLを編集→HTMLを入力――のところにコードを張り付ける。

とりあえず4項目のみの初期状態にしてありますが、数字や項目は6項目まで自由に変更可能です。

説明書的なもの(簡単なカスタマイズのやり方です。メモ帳などにコピペしてご利用ください)

とりあえずコード最初の以下部分を参照してください。

<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="4.7,3.7,3.6,2.8"の数字を変えると表示スコアが変わります。
例 "4.7,3.7,3.6,2.8"→"1,2,3,4.5"でも〇。
サンプルでは小数で入れておりますが、上記のように整数でもオーケーです。

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

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

〇スコアの最大値変更
・ data-max="5"の数字部分を任意の数字に変更してください。
例 10点満点にしたい→ data-max="10"

〇4から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の数は必ず一致させてください。合っていないとチャートが表示されません。

〇〇ランク基準の調整
・ランク表示はチャート全体の平均値をもとに決まります。
・基準を変えたい場合は、以下のif文を探して数字を調整してください。

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";
↑これは平均値が4.4以上ならSと表示するという意味です。

※基本的にはifの数字を調整してください。
※const normalized = (avg / maxVal) * 5;の「5」は基準化用なので、慣れていない場合はそのままにしておくのが無難です。

――足りないところがまだあるかもしれませんので、また追加・削除・変更するかもしれません。時点の最新版としてお使いください。

バージョン1.0

筆者のWordPress のブロックエディタ内にあるカスタムHTMLブロック では、問題なく表示されることを確認しています。

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

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

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

項目位置やサイズ調整、色替え程度でしたらお答えできる場合もありますが、必ずしも返信や対応をお約束するものではありません。その点もご了承ください。

※コードは必要に応じて自己責任で調整してお使いください。

了。

コメント

もりそばのはれときどきゲーム
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.

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