Add colored SCA-style flavor wheel to cupping; colorize the radar chart
Test and deploy / test-and-deploy (push) Successful in 55s

- New flavor-wheel.js: two-ring sunburst (families inner, descriptors
  outer) in SCA hue families; tapping a wedge toggles the same flavor
  tag as the checkbox list; selections render bold with a dark outline;
  respects the tag limit; labels drawn above all wedges so long names
  never clip (screenshot-verified in headless Chromium)
- Cupping radar: one hue per attribute on labels + vertex dots

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-09 10:11:31 -04:00
co-authored by Claude Fable 5
parent 2e1b8d24f9
commit ec078116b4
4 changed files with 187 additions and 2 deletions
+34 -2
View File
@@ -2,6 +2,7 @@ import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
import { showToast } from "./toast.js?v=__ASSET_VERSION__";
import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__";
import { renderFlavorWheel } from "./flavor-wheel.js?v=__ASSET_VERSION__";
import {
SCORE_ATTRS,
SCORE_LABELS,
@@ -392,11 +393,24 @@ function toggleFlavorTag(tag, checked) {
} else {
data.flavor_tags = data.flavor_tags.filter((t) => t !== tag);
}
renderWheel();
renderFlavorFamilies();
renderFlavorChips();
scheduleSave();
}
function renderWheel() {
const svg = document.getElementById("flavor-wheel");
if (!svg) return;
renderFlavorWheel(
svg,
FLAVOR_TAXONOMY,
data.flavor_tags,
(tag, on) => toggleFlavorTag(tag, on),
data.flavor_tags.length >= MAX_FLAVOR_TAGS,
);
}
function renderFlavorFamilies() {
const wrap = document.getElementById("flavor-families");
const atLimit = data.flavor_tags.length >= MAX_FLAVOR_TAGS;
@@ -438,6 +452,8 @@ function renderFlavorFamilies() {
// ── Radar chart (pure SVG, no dependency) ──
const RADAR_CENTER = 120;
const RADAR_RADIUS = 90;
// One hue per attribute so the wheel reads as ten distinct spokes, not a grey web.
const AXIS_COLOR = (i) => `hsl(${(i * 36 + 8) % 360} 46% 40%)`;
function radarPoints() {
const n = SCORE_ATTRS.length;
@@ -498,7 +514,8 @@ function initRadarStatic() {
"text-anchor": "middle",
"dominant-baseline": "middle",
"font-size": "9",
fill: "var(--ink-2)",
"font-weight": "600",
fill: AXIS_COLOR(i),
});
label.textContent = SCORE_LABELS[attr].split("/")[0];
svg.append(label);
@@ -516,7 +533,21 @@ function initRadarStatic() {
function renderRadar() {
const shape = document.getElementById("radar-shape");
if (!shape) return;
shape.setAttribute("points", radarPoints().map((p) => `${p.x},${p.y}`).join(" "));
const points = radarPoints();
shape.setAttribute("points", points.map((p) => `${p.x},${p.y}`).join(" "));
// Colored vertex dots matching each axis label.
const svg = document.getElementById("cup-radar");
for (const old of svg.querySelectorAll("[data-vertex]")) old.remove();
points.forEach((p, i) => {
const dot = svgEl("circle", {
cx: p.x,
cy: p.y,
r: 3.2,
fill: AXIS_COLOR(i),
"data-vertex": "1",
});
svg.append(dot);
});
}
function wireNotes() {
@@ -553,6 +584,7 @@ async function initSessionView(user) {
renderScoreRows();
renderTickRows();
renderDefectRows();
renderWheel();
renderFlavorFamilies();
renderFlavorChips();
wireNotes();