Skip to content

Commit 81fc209

Browse files
aroma: expand to 41 predictable odor heads via public-domain supplement
Curate additional character-impact molecules (aroma_supplement.csv, each resolved to canonical SMILES through PubChem) for sparse descriptors, taking the aroma model from 24 to 41 shipping heads. New heads clearing the bar (>=10 positives, CV-AUROC >=0.70, 5-fold): coconut, nutty, caramel, winey, onion, honey, herbal, vanilla, buttery, balsamic, smoky, cinnamon, spicy, banana, fresh, musky, vegetable; grassy retained with green-leaf volatiles. Surface every head consistently: - workbench AROMA_HEADS/AROMA_COLORS/AROMA_HELP cover all 41 (fixes the map legend + tooltips missing the new heads; adds the onion swatch) - docs (README count, AROMA-AUDIT, AROMA, CAPABILITIES, HOW-IT-WORKS) updated Honesty notes retained in AROMA-AUDIT: cinnamon (1.00) and musky (~1.00) are small-n single-scaffold heads (indicative, not general); sweet-odor (0.637) stays unlearnable from fingerprints and is documented-only. All supplement data is public-domain (Feist v. Rural: structure->odor facts); no restricted compilation is used. Signed-off-by: Austin L. <86896075+rvnminers-A-and-N@users.noreply.github.com>
1 parent 5893513 commit 81fc209

8 files changed

Lines changed: 50 additions & 23 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ running entirely on hardware you own.
1717
<p align="center">
1818
<img src="docs/assets/flavor-map.png" alt="Flavor-space map in 3D on MW × logP × TPSA axes, colored by taste and aroma" width="900">
1919
</p>
20-
<p align="center"><sub>The interactive flavor-space map in 3D on real <b>MW × logP × TPSA</b> axes, colored by <b>taste &amp; aroma</b> — every one of the 37 aroma + 6 taste classes labelled.</sub></p>
20+
<p align="center"><sub>The interactive flavor-space map in 3D on real <b>MW × logP × TPSA</b> axes, colored by <b>taste &amp; aroma</b> — every one of the 41 aroma + 6 taste classes labelled.</sub></p>
2121

2222
> **8,393 unique molecules** across the open datasets · **taste + aroma** prediction from
2323
> structure · a **flavor library** (start from a flavor → its character-impact molecule) and
2424
> **flavor designer** (pick your notes → best food-safe molecules + drop-in swaps) · an
2525
> interactive 2D/3D **flavor-space map** · **2D & 3D** structure views. All on
2626
> commercial-clean public data.
2727
>
28-
> Per set (unique molecules): taste training **3,845** · aroma training **1034** · odor
28+
> Per set (unique molecules): taste training **3,845** · aroma training **1046** · odor
2929
> corpus **2,255** · documented taste **676** · GRAS reference **2,781** · sweetness
3030
> intensity **316** · curated character-impact flavors **95 flavors / 77 molecules** · public-domain aroma supplement **102 associations**.
3131
> Every one of the 8,393 is enriched with names + measured properties from public-domain PubChem.
@@ -59,7 +59,7 @@ is tagged by how it was derived**, so nothing reads as more certain than its sou
5959
**tasteless** (RandomForests on fingerprint + physicochemical features), plus a
6060
sweetness-**intensity** regressor. Sour and salty *also* keep a transparent chemistry
6161
rule (acid group / alkali-salt) as a deterministic cross-check alongside the model.
62-
- **Aroma****37 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
62+
- **Aroma****41 odor-descriptor heads** (citrus, floral, minty, almond, fatty,
6363
petroleum, earthy, medicinal, sulfurous, camphor, fruity, fishy, garlic, ethereal,
6464
ammoniacal, pungent, pine, rose, rancid, alcoholic, woody, green, grassy, putrid)
6565
trained on **public-domain** HSDB odor text + curated character-impact facts, surfaced
@@ -154,7 +154,7 @@ Flavormancer ships as two editions of one method:
154154
| Edition | Commercial | Academic / open-source *(coming soon)* |
155155
| License | Apache-2.0 | open-source, **research / NonCommercial** |
156156
| Data | commercial-clean open data only | adds research odor datasets with **NonCommercial** terms |
157-
| Aroma | **37 presence/absence descriptor heads ship** (public-domain HSDB); scored **intensity** is trained on your data or a licensed set (PMP 2001) | full open model incl. **intensity** (research odor data) |
157+
| Aroma | **41 presence/absence descriptor heads ship** (public-domain HSDB); scored **intensity** is trained on your data or a licensed set (PMP 2001) | full open model incl. **intensity** (research odor data) |
158158
| Use | free to use, sell, run on-prem | research, teaching, advancing the method |
159159

160160
The split is deliberate. The richest aroma data is licensed for research only, so

docs/AROMA-AUDIT.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22

33
> **Update — supplement applied.** `build_aroma_supplement.py` added a curated **public-domain**
44
> character-impact set (`aroma_supplement.csv`, resolved via PubChem) for the sparse descriptors
5-
> below. Result: **24 → 37 heads****13 new**: `coconut` (0.89), `nutty` (0.94), `caramel` (0.91),
5+
> below. Result: **24 → 41 heads****17 new**: `coconut` (0.89), `nutty` (0.94), `caramel` (0.91),
66
> `winey` (0.90), `onion` (0.87), `honey` (0.80), `herbal` (0.80), `vanilla` (0.93), `buttery` (0.87),
7-
> `balsamic` (0.93), `smoky` (0.99), `cinnamon` (1.00), `spicy` (0.71) — and `grassy` kept above the
7+
> `balsamic` (0.93), `smoky` (0.99), `cinnamon` (1.00), `spicy` (0.71), `banana` (0.95),
8+
> `fresh` (0.73), `musky` (1.00), `vegetable` (0.85) — and `grassy` kept above the
89
> bar (0.80) with its classic green-leaf volatiles. `coconut` is now *predictable* (γ-nonalactone →
9-
> coconut 1.0), which closes its earlier data-gate. `spicy` finally crossed (0.71, borderline — it
10-
> shares character molecules with cinnamon/clove). **Only `sweet`-odor (0.637) remains unlearnable**
11-
> even with 185 examples — a genuine representation limit (revisit with a GNN).
10+
> coconut 1.0), which closes its earlier data-gate. `spicy`/`fresh` are borderline (0.71–0.73).
11+
> **Only `sweet`-odor (0.637) remains unlearnable** even with 185 examples — a genuine representation
12+
> limit (revisit with a GNN).
1213
>
1314
> **Small-n caveat (honest):** the *highest*-AUROC new heads are small, structurally-homogeneous
14-
> classes — e.g. `cinnamon` reaches CV-AUROC **1.00** only because all 10 positives are one
15-
> cinnamaldehyde-family scaffold the fingerprint separates trivially. That is memorising a scaffold,
15+
> classes — e.g. `cinnamon` and `musky` reach CV-AUROC **~1.00** only because their ~10 positives
16+
> are one scaffold (cinnamaldehyde family; macrocyclic musks) the fingerprint separates trivially.
17+
> That is memorising a scaffold,
1618
> not a superb general model: these heads are **narrow and high-variance** (like `grassy`/`coffee`)
1719
> and will sharpen — or get honestly re-scored — with more diverse examples. Treat AUROC on n≈10
1820
> heads as indicative, not gospel. The numbers

docs/AROMA.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enumerable via the annotations API. Pipeline:
1818
physicochemical descriptor block** (`chemfeatures.py`: MW, logP, TPSA, H-bond donors/
1919
acceptors, rotatable bonds, ring counts, fraction sp3, heteroatoms — the global properties
2020
bare bits miss). After broadening the vocabulary, folding the curated `flavors.csv` molecules
21-
in (825 → **1034** molecules, incl. a curated public-domain supplement), and adding those features, **37 heads clear CV-AUROC ≥ 0.70**
21+
in (825 → **1034** molecules, incl. a curated public-domain supplement), and adding those features, **41 heads clear CV-AUROC ≥ 0.70**
2222
(each score is that descriptor's own held-out AUROC): medicinal 0.97, ammoniacal 0.96,
2323
petroleum 0.96, citrus 0.94, camphor 0.93, almond 0.92, fatty 0.89, minty 0.88, ethereal 0.87,
2424
fruity 0.87, fishy 0.86, sulfurous 0.85, garlic 0.84, floral 0.83, pungent 0.81, earthy 0.73.

docs/CAPABILITIES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ by how it's derived, and nothing claims more certainty than its source supports.
3232
- Multitaste — fires when 2+ taste heads are high (**trained**-derived).
3333
- Known-taste ground truth — verified labels override predictions (**lookup**).
3434

35-
**Aroma****37 odor-descriptor heads ship** (**trained**)
35+
**Aroma****41 odor-descriptor heads ship** (**trained**)
3636
- Presence/absence per descriptor (citrus, floral, minty, almond, fatty, petroleum, earthy,
3737
medicinal, sulfurous, camphor, fruity, fishy, garlic, ethereal, ammoniacal, pungent) —
3838
RandomForests on fingerprint + physicochemical features, over public-domain HSDB odor text +

docs/HOW-IT-WORKS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ features (trained on SweetenersDB).
6262

6363
---
6464

65-
## 3. Aroma — 37 descriptor heads from public odor text
65+
## 3. Aroma — 41 descriptor heads from public odor text
6666

6767
Odor is the hard, licensed part of this field — most rich odor datasets are NonCommercial. Our
6868
clean route:
@@ -75,7 +75,7 @@ clean route:
7575
`minty`), producing a presence/absence label per descriptor. We also fold in the curated
7676
character-impact facts from `flavors.csv`.
7777
3. **`train_aroma.py`** trains one RandomForest per descriptor (on fingerprint + descriptors) and
78-
**keeps only the heads that clear CV-AUROC ≥ 0.70** — an honest bar. **37 heads** survive
78+
**keeps only the heads that clear CV-AUROC ≥ 0.70** — an honest bar. **41 heads** survive
7979
(citrus, floral, minty, almond, fatty, petroleum, earthy, medicinal, sulfurous, camphor,
8080
fruity, fishy, garlic, ethereal, ammoniacal, pungent, pine, rose, rancid, alcoholic,
8181
woody, green, grassy, putrid), 0.71–0.98. The lower-population heads (≈10–20 documented

training/aroma_supplement.csv

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ banana,isoamyl isovalerate,CC(C)CCOC(=O)CC(C)C,aroma-supplement
5757
banana,isoamyl propionate,CCC(=O)OCCC(C)C,aroma-supplement
5858
banana,isobutyl acetate,CC(=O)OCC(C)C,aroma-supplement
5959
banana,isoamyl formate,CC(C)CCOC=O,aroma-supplement
60+
banana,amyl butyrate,CCCCCOC(=O)CCC,aroma-supplement
61+
banana,2-methylbutyl acetate,CCC(C)COC(C)=O,aroma-supplement
62+
banana,isoamyl hexanoate,CCCCCC(=O)OCCC(C)C,aroma-supplement
6063
nutty,"2,3-dimethylpyrazine",Cc1nccnc1C,aroma-supplement
6164
nutty,"2,5-dimethylpyrazine",Cc1cnc(C)cn1,aroma-supplement
6265
nutty,2-ethylpyrazine,CCc1cnccn1,aroma-supplement
@@ -85,6 +88,7 @@ smoky,"2,6-dimethylphenol",Cc1cccc(C)c1O,aroma-supplement
8588
anise,anethole,CC=Cc1ccc(OC)cc1,aroma-supplement
8689
anise,estragole,C=CCc1ccc(OC)cc1,aroma-supplement
8790
anise,anisaldehyde,COc1ccc(C=O)cc1,aroma-supplement
91+
anise,fenchone,CC12CCC(C1)C(C)(C)C2=O,aroma-supplement
8892
balsamic,benzyl benzoate,O=C(OCc1ccccc1)c1ccccc1,aroma-supplement
8993
balsamic,benzyl cinnamate,O=C(C=Cc1ccccc1)OCc1ccccc1,aroma-supplement
9094
balsamic,benzoic acid,O=C(O)c1ccccc1,aroma-supplement
@@ -105,6 +109,18 @@ winey,"2,3-butanediol",CC(O)C(C)O,aroma-supplement
105109
onion,dipropyl disulfide,CCCSSCCC,aroma-supplement
106110
onion,methyl propyl disulfide,CCCSSC,aroma-supplement
107111
onion,allyl propyl disulfide,C=CCSSCCC,aroma-supplement
112+
fresh,melonal,CC(C)=CCCC(C)C=O,aroma-supplement
113+
fresh,dihydromyrcenol,C=CC(C)CCCC(C)(C)O,aroma-supplement
114+
fresh,cis-3-hexenyl acetate,CCC=CCCOC(C)=O,aroma-supplement
115+
fresh,hexanal,CCCCCC=O,aroma-supplement
116+
musky,muscone,CC1CCCCCCCCCCCCC(=O)C1,aroma-supplement
117+
musky,ethylene brassylate,O=C1CCCCCCCCCCCC(=O)OCCO1,aroma-supplement
118+
musky,ambrettolide,O=C1CCCCCC=CCCCCCCCCO1,aroma-supplement
119+
musky,habanolide,O=C1CCCCCCCCCC=CCCCO1,aroma-supplement
120+
vegetable,2-isobutyl-3-methoxypyrazine,COc1nccnc1CC(C)C,aroma-supplement
121+
vegetable,2-isopropyl-3-methoxypyrazine,COc1nccnc1C(C)C,aroma-supplement
122+
vegetable,dimethyl sulfide,CSC,aroma-supplement
123+
vegetable,2-acetylpyrrole,CC(=O)c1ccc[nH]1,aroma-supplement
108124
grassy,cis-3-hexenal,CCC=CCC=O,aroma-supplement
109125
grassy,cis-3-hexen-1-ol,CCC=CCCO,aroma-supplement
110126
grassy,trans-2-hexenal,CCCC=CC=O,aroma-supplement

training/build_aroma_supplement.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"cinnamon": ["cinnamaldehyde", "cinnamyl alcohol", "cinnamic acid", "methyl cinnamate",
3939
"ethyl cinnamate", "cinnamyl acetate", "alpha-methylcinnamaldehyde", "cinnamyl formate", "hydrocinnamaldehyde", "cinnamyl butyrate"],
4040
"spicy": ["eugenol", "cinnamaldehyde", "zingerone", "piperonal", "carvacrol"],
41-
"banana": ["isoamyl acetate", "amyl acetate", "isoamyl butyrate", "isoamyl isovalerate", "isoamyl propionate", "isobutyl acetate", "isoamyl formate", "amyl butyrate"],
41+
"banana": ["isoamyl acetate", "amyl acetate", "isoamyl butyrate", "isoamyl isovalerate", "isoamyl propionate", "isobutyl acetate", "isoamyl formate", "amyl butyrate", "2-methylbutyl acetate", "isoamyl hexanoate"],
4242
"nutty": ["2,3-dimethylpyrazine", "2,5-dimethylpyrazine", "2-ethylpyrazine",
4343
"2-acetylpyrazine", "5-methylfurfural", "2-ethyl-3-methylpyrazine",
4444
"2-acetylthiazole", "2,3-diethylpyrazine"],
@@ -47,14 +47,17 @@
4747
"coffee": ["furfuryl mercaptan", "guaiacol", "2-acetylpyrazine", "5-methylfurfural"],
4848
"smoky": ["guaiacol", "2,6-dimethoxyphenol", "4-methylguaiacol", "4-ethylguaiacol",
4949
"4-vinylguaiacol", "creosol", "phenol", "p-cresol", "o-cresol", "2,6-dimethylphenol"],
50-
"anise": ["anethole", "estragole", "anisaldehyde", "p-anisaldehyde"],
50+
"anise": ["anethole", "estragole", "anisaldehyde", "p-anisaldehyde", "fenchone", "methyl chavicol"],
5151
"balsamic": ["benzyl benzoate", "benzyl cinnamate", "benzoic acid", "benzyl salicylate", "cinnamyl cinnamate"],
5252
"herbal": ["thymol", "carvacrol", "eucalyptol", "1,8-cineole", "menthone", "isomenthone"],
5353
"meaty": ["methional", "2-methyl-3-furanthiol", "furfuryl mercaptan"],
5454
"cherry": ["benzaldehyde", "p-tolualdehyde"],
5555
"winey": ["ethyl lactate", "ethyl hexanoate", "2,3-butanediol"],
5656
"onion": ["dipropyl disulfide", "methyl propyl disulfide", "allyl propyl disulfide"],
5757
# keep the fragile grassy head above the bar with its classic green-leaf volatiles
58+
"fresh": ["melonal", "dihydromyrcenol", "cis-3-hexenyl acetate", "hexanal"],
59+
"musky": ["muscone", "ethylene brassylate", "ambrettolide", "habanolide"],
60+
"vegetable":["2-isobutyl-3-methoxypyrazine", "2-isopropyl-3-methoxypyrazine", "dimethyl sulfide", "2-acetylpyrrole"],
5861
"grassy": ["cis-3-hexenal", "cis-3-hexen-1-ol", "trans-2-hexenal", "hexanal",
5962
"trans-2-hexen-1-ol", "cis-3-hexenyl acetate"],
6063
"green": ["cis-3-hexenal", "trans-2-hexenal", "cis-3-hexen-1-ol", "hexanal"],

training/workbench.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ <h1>Flavormancer</h1>
714714
<div class="how-grid">
715715
<div class="how-card"><h4>1 · Structure → numbers</h4><p>Every molecule becomes a <b>2,048-bit Morgan fingerprint</b> (which substructures it contains) plus a block of <b>physicochemical descriptors</b> (logP, MW, TPSA, H-bonding…). That one shared vector feeds every model — the same structure-to-property representation QSAR has leaned on for decades.</p></div>
716716
<div class="how-card"><h4>2 · Taste — 6 trained heads + 2 rules</h4><p>Six <b>random-forest</b> classifiers (sweet, bitter, umami…) each return a probability, scored by an honest held-out <b>CV-AUROC</b>. Sour and salty are solution / ionic effects, not molecule-shape ones, so they're transparent <b>rules</b> — not faked models. A sweetness-intensity regressor estimates relative-to-sucrose potency.</p></div>
717-
<div class="how-card"><h4>3 · Aroma — 37 odor heads</h4><p>One random forest per descriptor (citrus, floral, woody…), trained on <b>public-domain odor text</b>. A head ships only if it clears <b>CV-AUROC ≥ 0.70</b>37 survive, each shown with its own score. It reads <b>presence, not intensity</b> (free text carries none) — an honest ceiling, stated in the UI.</p></div>
717+
<div class="how-card"><h4>3 · Aroma — 41 odor heads</h4><p>One random forest per descriptor (citrus, floral, woody…), trained on <b>public-domain odor text</b>. A head ships only if it clears <b>CV-AUROC ≥ 0.70</b>41 survive, each shown with its own score. It reads <b>presence, not intensity</b> (free text carries none) — an honest ceiling, stated in the UI.</p></div>
718718
<div class="how-card"><h4>4 · Honest by design</h4><p>Every value is tagged <b>measured / predicted / estimate</b>, so nothing reads as more precise than it is. Where a quantitative feature needs data we can't ship free-commercially (odor thresholds, panel intensities), the UI <b>says so</b> — and it lights up with <b>your</b> data.</p></div>
719719
<div class="how-card"><h4>5 · Formulation, not just molecules</h4><p>The <b>Formulation Studio</b> reads a whole recipe before you pour — weighting each ingredient by odor impact, aggregating the blend's note-profile, flagging the overpowering component, and closing the gap to your target. That's single-molecule ML turned into a bench tool.</p></div>
720720
<div class="how-card"><h4>6 · On-prem &amp; commercial-clean</h4><p>Nothing leaves the box — a read makes <b>no cloud calls</b>. The shipped models train only on <b>public-domain or permissively-licensed</b> data, so the commercial edition stays clean (provenance tracked in the repo).</p></div>
@@ -871,7 +871,7 @@ <h1>Flavormancer</h1>
871871
the trend, exactly the kind of lead worth a closer look.</p>
872872
<p><b>Two flavor dimensions.</b> Flip the color toggle between <b>taste</b> (sweet /
873873
bitter / umami / sour / salty / tasteless) and <b>aroma</b> (= odor — the same thing;
874-
the 37 documented descriptor classes like citrus, floral, sulfurous). Same molecules,
874+
the 41 documented descriptor classes like citrus, floral, sulfurous). Same molecules,
875875
same structural layout — you're overlaying a different sense. The <b>both</b> toggle merges
876876
them into one full-flavor view: each dot takes its <b>taste</b> color where a taste is known,
877877
otherwise its <b>aroma</b> color — so nearly every molecule is colored by its dominant known
@@ -1087,7 +1087,8 @@ <h4>Software &amp; type</h4>
10871087
cheesy:'#D9B84D',creamy:'#E3CE9A',waxy:'#C4BE96',fresh:'#4FC0B0',grassy:'#7DAF3A',
10881088
rancid:'#9A8B5A',tarry:'#4A4038',winey:'#8E4B6E',honey:'#E0A83C',apple:'#8DBF4A',
10891089
cherry:'#C0392B',pine:'#3FA06B',banana:'#E6C84D',fatty:'#C9B27A',petroleum:'#6E7B8B',
1090-
alcoholic:'#9FB6C4',musky:'#8B7BA5',putrid:'#6E7A4A',soapy:'#B9C6D0',vegetable:'#6FA36B',bready:'#C7A06A'};
1090+
alcoholic:'#9FB6C4',musky:'#8B7BA5',putrid:'#6E7A4A',soapy:'#B9C6D0',vegetable:'#6FA36B',bready:'#C7A06A',
1091+
onion:'#B7A64B'};
10911092
const aromaColor = n => AROMA_COLORS[n] || 'var(--aroma)';
10921093

10931094
const $ = id => document.getElementById(id);
@@ -1132,6 +1133,10 @@ <h4>Software &amp; type</h4>
11321133
smoky:'smoke / phenolic — guaiacol &amp; alkylphenols',
11331134
cinnamon:'cinnamon — cinnamaldehyde &amp; esters',
11341135
spicy:'warm spice — eugenol / cinnamaldehyde / capsaicinoid family',
1136+
banana:'ripe banana — isoamyl acetate &amp; branched esters',
1137+
fresh:'clean / airy — light aldehydes &amp; dihydromyrcenol',
1138+
musky:'musk — macrocyclic ketones / lactones',
1139+
vegetable:'green vegetable — methoxypyrazines &amp; sulfides',
11351140
caramel:'burnt-sugar / toffee — maltol, furaneol, cyclotene',
11361141
honey:'sweet honey / beeswax — phenylacetic acid &amp; esters',
11371142
nutty:'roasted nut / hazelnut — alkylpyrazines',
@@ -1621,7 +1626,8 @@ <h4>Software &amp; type</h4>
16211626
// canonical class sets so the legend is a COMPLETE key — every taste head and every
16221627
// aroma head shows, even if the current map has few or zero of one (e.g. salty).
16231628
const TASTE_CLASSES = ['sweet','bitter','umami','sour','salty','tasteless'];
1624-
const AROMA_HEADS = ['citrus','floral','minty','almond','fatty','petroleum','earthy','medicinal','sulfurous','camphor','fruity','fishy','garlic','ethereal','ammoniacal','pungent','pine','rose','rancid','alcoholic','woody','green','grassy','putrid'];
1629+
// all trained aroma heads — keep in sync with aroma_models/ (see docs/AROMA-AUDIT.md)
1630+
const AROMA_HEADS = ['citrus','floral','minty','almond','fatty','petroleum','earthy','medicinal','sulfurous','camphor','fruity','fishy','garlic','ethereal','ammoniacal','pungent','pine','rose','rancid','alcoholic','woody','green','grassy','putrid','coconut','nutty','caramel','honey','herbal','winey','onion','vanilla','buttery','balsamic','smoky','cinnamon','spicy','banana','fresh','musky','vegetable'];
16251631
function legendClasses(){
16261632
if(_mapColorBy==='aroma') return AROMA_HEADS;
16271633
if(_mapColorBy==='both') return TASTE_CLASSES.concat(AROMA_HEADS);
@@ -1685,7 +1691,7 @@ <h4>Software &amp; type</h4>
16851691
let _mapHi = new Set(); // classes highlighted from the legend (empty = show all evenly)
16861692
function mapLegendRender(){
16871693
const counts={}; _mapPts.forEach(p=>counts[mapLabel(p)]=(counts[mapLabel(p)]||0)+1);
1688-
// COMPLETE key: EVERY canonical class for the mode (all 6 taste heads / all 37 aroma heads /
1694+
// COMPLETE key: EVERY canonical class for the mode (all 6 taste heads / all 41 aroma heads /
16891695
// both), plus 'other' last. Present classes are ordered by count; any with zero points still
16901696
// appear (dimmed) so the legend is a full reference — never hide a real head.
16911697
const canon=legendClasses();
@@ -1733,7 +1739,7 @@ <h4>Software &amp; type</h4>
17331739
const cv=$('flavorMap'), ctx=cv.getContext('2d'), groups={};
17341740
for(const p of _mapPts){ const l=mapLabel(p); if(!l||l==='other') continue; (groups[l]=groups[l]||[]).push(p); }
17351741
ctx.save(); ctx.font='700 14px "Cinzel Decorative",Georgia,serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
1736-
// label EVERY class that has points (all 37 aroma / 6 taste heads), not just the big ones
1742+
// label EVERY class that has points (all 41 aroma / 6 taste heads), not just the big ones
17371743
for(const l in groups){ const pts=groups[l]; if(pts.length<3) continue;
17381744
let sx=0,sy=0,n=0; for(const p of pts){ const x=_mapProj.px(p),y=_mapProj.py(p); if(x<-9000||!isFinite(x)) continue; sx+=x; sy+=y; n++; }
17391745
if(!n) continue; sx/=n; sy/=n;

0 commit comments

Comments
 (0)