Skip to content

Commit 20b032b

Browse files
feat(highcharts): implement scatter-color-mapped (#11629)
## Implementation: `scatter-color-mapped` - javascript/highcharts Implements the **javascript/highcharts** version of `scatter-color-mapped`. **File:** `plots/scatter-color-mapped/implementations/javascript/highcharts.js` **Parent Issue:** #2004 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/33970153006)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 57a80ae commit 20b032b

2 files changed

Lines changed: 438 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// anyplot.ai
2+
// scatter-color-mapped: Color-Mapped Scatter Plot
3+
// Library: highcharts 12.6.0 | JavaScript 22.23.2
4+
// Quality: 87/100 | Created: 2026-09-05
5+
6+
const t = window.ANYPLOT_TOKENS;
7+
8+
// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------
9+
let seed = 42;
10+
function rand() {
11+
seed = (1664525 * seed + 1013904223) % 4294967296;
12+
return seed / 4294967296;
13+
}
14+
15+
const pointCount = 150;
16+
const readings = [];
17+
for (let i = 0; i < pointCount; i++) {
18+
const temperature = 150 + rand() * 100; // reaction temperature, deg C
19+
const duration = 10 + rand() * 80; // reaction duration, minutes
20+
const noise = (rand() - 0.5) * 15;
21+
let yieldPct =
22+
95 -
23+
0.012 * (temperature - 200) ** 2 -
24+
0.02 * (duration - 50) ** 2 +
25+
noise;
26+
yieldPct = Math.max(5, Math.min(98, yieldPct));
27+
readings.push({ x: temperature, y: duration, yieldPct });
28+
}
29+
30+
const yieldValues = readings.map((r) => r.yieldPct);
31+
const yieldMin = Math.min(...yieldValues);
32+
const yieldMax = Math.max(...yieldValues);
33+
34+
// --- Color mapping ------------------------------------------------------------
35+
// The `coloraxis` module (colorAxis + automatic legend gradient) lives in
36+
// modules/coloraxis.js, which isn't loaded — only the core bundle is. Interpolate
37+
// each point's fill from the Imprint imprint_seq gradient by hand instead, and
38+
// draw a matching colorbar with the core SVG renderer.
39+
function hexToRgb(hex) {
40+
const v = parseInt(hex.slice(1), 16);
41+
return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
42+
}
43+
const seqLow = hexToRgb(t.seq[0]);
44+
const seqHigh = hexToRgb(t.seq[1]);
45+
function yieldColor(value) {
46+
const ratio = (value - yieldMin) / (yieldMax - yieldMin);
47+
const [r, g, b] = seqLow.map((c, i) => Math.round(c + (seqHigh[i] - c) * ratio));
48+
return `rgb(${r}, ${g}, ${b})`;
49+
}
50+
51+
const points = readings.map((r) => ({
52+
x: r.x,
53+
y: r.y,
54+
color: yieldColor(r.yieldPct),
55+
custom: { yieldPct: r.yieldPct },
56+
}));
57+
58+
const peak = readings.reduce((best, r) => (r.yieldPct > best.yieldPct ? r : best));
59+
60+
// --- Chart -------------------------------------------------------------------
61+
Highcharts.chart("container", {
62+
chart: {
63+
type: "scatter",
64+
backgroundColor: "transparent",
65+
animation: false,
66+
marginRight: 190,
67+
style: { fontFamily: "inherit" },
68+
events: {
69+
load: function () {
70+
// Manual colorbar (see "Color mapping" note above) mirroring the
71+
// imprint_seq gradient used for the point fills.
72+
const chart = this;
73+
const barWidth = 26;
74+
const barX = chart.plotLeft + chart.plotWidth + 46;
75+
const barY = chart.plotTop;
76+
const barHeight = chart.plotHeight;
77+
78+
// Fragment-url paint servers (linearGradient defs) don't resolve on the
79+
// harness's about:blank document, so the bar is built from many thin
80+
// interpolated bands instead of a single gradient fill.
81+
const bandCount = 60;
82+
const bandHeight = barHeight / bandCount;
83+
for (let i = 0; i < bandCount; i++) {
84+
const frac = (i + 0.5) / bandCount;
85+
const value = yieldMin + frac * (yieldMax - yieldMin);
86+
chart.renderer
87+
.rect(barX, barY + barHeight - (i + 1) * bandHeight, barWidth, bandHeight + 0.5)
88+
.attr({ fill: yieldColor(value) })
89+
.add();
90+
}
91+
chart.renderer
92+
.rect(barX, barY, barWidth, barHeight)
93+
.attr({ fill: "none", stroke: t.inkSoft, "stroke-width": 1 })
94+
.add();
95+
96+
const tickCount = 5;
97+
for (let i = 0; i < tickCount; i++) {
98+
const frac = i / (tickCount - 1);
99+
const value = yieldMin + frac * (yieldMax - yieldMin);
100+
const tickY = barY + barHeight * (1 - frac);
101+
chart.renderer
102+
.path(["M", barX + barWidth, tickY, "L", barX + barWidth + 6, tickY])
103+
.attr({ stroke: t.inkSoft, "stroke-width": 1 })
104+
.add();
105+
chart.renderer
106+
.text(value.toFixed(0), barX + barWidth + 12, tickY + 5)
107+
.css({ color: t.inkSoft, fontSize: "14px" })
108+
.add();
109+
}
110+
111+
chart.renderer
112+
.text("Reaction Yield (%)", barX + barWidth + 60, barY + barHeight / 2)
113+
.attr({ rotation: 90, align: "center" })
114+
.css({ color: t.inkSoft, fontSize: "16px" })
115+
.add();
116+
117+
// Storytelling touch: ring the highest-yield point and label it, so the
118+
// chart guides the viewer to the reaction's sweet spot instead of leaving
119+
// an undifferentiated point cloud.
120+
const peakX = chart.xAxis[0].toPixels(peak.x, false);
121+
const peakY = chart.yAxis[0].toPixels(peak.y, false);
122+
chart.renderer
123+
.circle(peakX, peakY, 13)
124+
.attr({ fill: "none", stroke: t.ink, "stroke-width": 1.5, dashstyle: "Dash" })
125+
.add();
126+
chart.renderer
127+
.label(`Peak yield ${peak.yieldPct.toFixed(0)}%`, peakX + 16, peakY - 26)
128+
.css({ color: t.ink, fontSize: "13px", fontWeight: "600" })
129+
.attr({ padding: 4, zIndex: 6 })
130+
.add();
131+
},
132+
},
133+
},
134+
credits: { enabled: false },
135+
accessibility: { enabled: false },
136+
title: {
137+
text: "scatter-color-mapped · javascript · highcharts · anyplot.ai",
138+
style: { color: t.ink, fontSize: "22px", fontWeight: "600" },
139+
},
140+
legend: { enabled: false },
141+
tooltip: {
142+
pointFormat:
143+
"Temperature: {point.x:.0f}°C<br/>Duration: {point.y:.0f} min<br/>Yield: {point.custom.yieldPct:.1f}%",
144+
},
145+
xAxis: {
146+
title: {
147+
text: "Reaction Temperature (°C)",
148+
style: { color: t.inkSoft, fontSize: "16px" },
149+
},
150+
lineColor: t.inkSoft,
151+
tickColor: t.inkSoft,
152+
gridLineColor: t.grid,
153+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
154+
},
155+
yAxis: {
156+
title: {
157+
text: "Reaction Duration (min)",
158+
style: { color: t.inkSoft, fontSize: "16px" },
159+
},
160+
lineColor: t.inkSoft,
161+
tickColor: t.inkSoft,
162+
gridLineColor: t.grid,
163+
labels: { style: { color: t.inkSoft, fontSize: "14px" } },
164+
},
165+
plotOptions: {
166+
series: { animation: false },
167+
scatter: {
168+
marker: {
169+
radius: 7,
170+
lineWidth: 1,
171+
lineColor: t.pageBg,
172+
fillOpacity: 0.88,
173+
},
174+
},
175+
},
176+
series: [
177+
{
178+
name: "Yield",
179+
showInLegend: false,
180+
data: points,
181+
},
182+
],
183+
});

0 commit comments

Comments
 (0)