diff --git a/plots/slope-basic/implementations/javascript/highcharts.js b/plots/slope-basic/implementations/javascript/highcharts.js new file mode 100644 index 0000000000..346c89fb9f --- /dev/null +++ b/plots/slope-basic/implementations/javascript/highcharts.js @@ -0,0 +1,164 @@ +// anyplot.ai +// slope-basic: Basic Slope Chart (Slopegraph) +// Library: highcharts 12.6.0 | JavaScript 22.23.1 +// Quality: 96/100 | Created: 2026-07-25 + +const t = window.ANYPLOT_TOKENS; + +// --- Data (in-memory, deterministic) ---------------------------------------- +// Customer satisfaction score (0-100) for 10 products, two survey waves. +const products = [ + { name: "CloudSync", start: 62, end: 78 }, + { name: "DataVault", start: 88, end: 91 }, + { name: "PixelPro", start: 74, end: 65 }, + { name: "NetGuard", start: 55, end: 72 }, + { name: "FlowStream", start: 80, end: 76 }, + { name: "CoreLogic", start: 69, end: 84 }, + { name: "ByteWave", start: 91, end: 88 }, + { name: "GridPoint", start: 58, end: 61 }, + { name: "LinkHub", start: 77, end: 59 }, + { name: "TaskFlow", start: 66, end: 82 }, +]; + +const INCREASE = t.palette[0]; // brand green — profit/up/gain +const DECREASE = t.palette[4]; // matte red — loss/down +const FLAT = t.muted; + +const lineOf = (p) => (p.end > p.start ? INCREASE : p.end < p.start ? DECREASE : FLAT); + +// Tighten the y-domain to the actual data band (55-91) instead of a fixed +// 40-100 range, so the canvas isn't dominated by dead space. +const scores = products.flatMap((p) => [p.start, p.end]); +const yPad = 5; +const yMin = Math.floor(Math.min(...scores) - yPad); +const yMax = Math.ceil(Math.max(...scores) + yPad); + +// The mover with the largest absolute change gets a slightly heavier line +// and marker so the standout swing reads at a glance. +const standout = products.reduce((a, p) => (Math.abs(p.end - p.start) > Math.abs(a.end - a.start) ? p : a)); + +// Endpoint labels within NUDGE_GAP score points of a neighbor at the same +// wave get nudged apart vertically (label only, marker stays put) so +// stacked text doesn't crowd together. +const NUDGE_GAP = 3; +const NUDGE_PX = 6; +const labelNudges = (values) => { + const byValue = [...values].sort((a, b) => a.value - b.value); + const nudge = new Map(values.map((v) => [v.name, 0])); + for (let i = 1; i < byValue.length; i++) { + const lo = byValue[i - 1]; + const hi = byValue[i]; + if (hi.value - lo.value <= NUDGE_GAP) { + nudge.set(lo.name, nudge.get(lo.name) + NUDGE_PX); + nudge.set(hi.name, nudge.get(hi.name) - NUDGE_PX); + } + } + return nudge; +}; +const startNudge = labelNudges(products.map((p) => ({ name: p.name, value: p.start }))); +const endNudge = labelNudges(products.map((p) => ({ name: p.name, value: p.end }))); + +// --- Chart ------------------------------------------------------------------- +Highcharts.chart("container", { + chart: { + type: "line", + backgroundColor: "transparent", + animation: false, + style: { fontFamily: "inherit" }, + marginLeft: 170, + marginRight: 170, + }, + credits: { enabled: false }, + title: { + text: "slope-basic · javascript · highcharts · anyplot.ai", + style: { color: t.ink, fontSize: "22px", fontWeight: "600" }, + }, + xAxis: { + categories: ["Survey Wave 1", "Survey Wave 2"], + lineColor: t.inkSoft, + tickColor: t.inkSoft, + gridLineWidth: 0, + minPadding: 0.08, + maxPadding: 0.08, + labels: { style: { color: t.inkSoft, fontSize: "14px" } }, + }, + yAxis: { + title: { + text: "Customer Satisfaction Score", + style: { color: t.inkSoft, fontSize: "16px" }, + }, + min: yMin, + max: yMax, + startOnTick: false, + endOnTick: false, + gridLineWidth: 0, + lineColor: t.inkSoft, + tickColor: t.inkSoft, + labels: { style: { color: t.inkSoft, fontSize: "14px" } }, + }, + legend: { + enabled: true, + align: "center", + verticalAlign: "bottom", + itemStyle: { color: t.inkSoft, fontSize: "14px", fontWeight: "normal" }, + itemHoverStyle: { color: t.ink }, + }, + tooltip: { + headerFormat: "", + pointFormat: "{series.name}: {point.y}", + backgroundColor: t.elevatedBg, + borderColor: t.inkSoft, + style: { color: t.ink, fontSize: "13px" }, + }, + plotOptions: { + series: { + animation: false, + marker: { enabled: true, radius: 5, symbol: "circle" }, + lineWidth: 2, + dataLabels: { + enabled: true, + crop: false, + overflow: "allow", + style: { fontSize: "14px", fontWeight: "normal", textOutline: "none" }, + }, + }, + }, + series: [ + // Legend-only entries — real product lines below are excluded from the legend + // since 10 individual names would overwhelm it; color already carries the story. + { name: "Improved", type: "line", color: INCREASE, data: [], marker: { enabled: true }, enableMouseTracking: false }, + { name: "Declined", type: "line", color: DECREASE, data: [], marker: { enabled: true }, enableMouseTracking: false }, + ...products.map((p) => { + const isStandout = p.name === standout.name; + return { + name: p.name, + showInLegend: false, + color: lineOf(p), + lineWidth: isStandout ? 3.5 : 2, + marker: { radius: isStandout ? 6.5 : 5 }, + data: [ + { + y: p.start, + dataLabels: { + align: "right", + x: -12, + y: startNudge.get(p.name), + color: t.inkSoft, + format: `${p.name} · ${p.start}`, + }, + }, + { + y: p.end, + dataLabels: { + align: "left", + x: 12, + y: endNudge.get(p.name), + color: t.inkSoft, + format: `${p.end} · ${p.name}`, + }, + }, + ], + }; + }), + ], +}); diff --git a/plots/slope-basic/metadata/javascript/highcharts.yaml b/plots/slope-basic/metadata/javascript/highcharts.yaml new file mode 100644 index 0000000000..eec45c35e5 --- /dev/null +++ b/plots/slope-basic/metadata/javascript/highcharts.yaml @@ -0,0 +1,261 @@ +library: highcharts +language: javascript +specification_id: slope-basic +created: '2026-07-25T23:38:12Z' +updated: '2026-07-25T23:58:38Z' +generated_by: claude-sonnet +workflow_run: 30179585442 +issue: 981 +language_version: 22.23.1 +library_version: 12.6.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/slope-basic/javascript/highcharts/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/slope-basic/javascript/highcharts/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/slope-basic/javascript/highcharts/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/slope-basic/javascript/highcharts/plot-dark.html +quality_score: 96 +review: + strengths: + - Y-axis domain is now tightened to the actual data band (~50-95 vs. the prior fixed + 40-100), eliminating the dead space flagged in the attempt-1 review and giving + the plot excellent canvas utilization + - 'Endpoint-label collision nudging (labelNudges helper) resolves the tight label + clusters from attempt 1 — verified pixel-level via cropped zooms on both the FlowStream/LinkHub/PixelPro + cluster and the CoreLogic/TaskFlow/CloudSync/FlowStream cluster: no overlap in + either theme' + - Real product series no longer set enableMouseTracking:false — native Highcharts + tooltips now work on the actual data, while the two phantom legend-only series + (empty data) still correctly use enableMouseTracking:false to avoid polluting + the legend without disabling interactivity where it matters + - The largest-magnitude mover (LinkHub, -18) is visually emphasized with a heavier + line (3.5 vs 2 lineWidth) and larger marker (6.5 vs 5 radius), giving the chart + a clear focal point without resorting to fake/simulated interactivity + - 'Correct Imprint semantic color mapping retained: brand green (palette[0]) for + improved, matte red (palette[4]) for declined — matches the style guide''s finance/sentiment + semantic-exception rule, and direction is redundantly encoded by slope, keeping + it colorblind-safe' + - All font sizes explicitly set (title 22px, axis titles 16px, ticks/legend/dataLabels + 14px), readable in both light and dark renders with no theme-adaptation failures + weaknesses: + - The standout-mover emphasis (thicker line/larger marker) is a good visual-hierarchy + device but is still not an explicit callout — a small text annotation or delta + badge (e.g. '-18') on the standout line would push the storytelling further toward + publication-grade + - 'Distinctive-feature usage is still moderate rather than showcase-level: the phantom-series + legend and dataLabels crop/overflow handling are genuinely Highcharts-specific, + but the chart otherwise reads as a fairly standard direct-labeled line chart that + other JS libraries could approximate with similar effort' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white, consistent with #FAF8F1 — not pure white. + Chrome: Title "slope-basic · javascript · highcharts · anyplot.ai" in bold dark text, top center. Y-axis title "Customer Satisfaction Score" (rotated) and tick labels (50-95, tightened to the actual data band) in soft dark gray on the left. X-axis category labels "Survey Wave 1" / "Survey Wave 2" below the two vertical lanes. Legend ("Improved" / "Declined") centered at the bottom. All chrome text is dark and clearly legible against the light background. + Data: 10 product lines connect Wave 1 to Wave 2 values, each colored brand green (#009E73, "Improved") or matte red ("Declined") depending on direction. The largest-magnitude mover (LinkHub, 77→59) renders with a visibly heavier line and larger marker than the rest, creating a clear focal point. Direct data labels showing "{name} · {value}" flank both endpoints of every line, right-aligned on the left column and left-aligned on the right column. Zoomed crops of the two closest label clusters (FlowStream/LinkHub/PixelPro on Wave 1; CoreLogic/TaskFlow/CloudSync/FlowStream on Wave 2) confirm no overlap — the collision-nudge logic keeps every label pair legibly separated. + Legibility verdict: PASS — every title, axis label, tick label, and data label is clearly readable; no light-on-light text found; no clipping at any canvas edge (verified via edge crops). + + Dark render (plot-dark.png): + Background: Warm near-black, consistent with #1A1A17 — not pure black. + Chrome: Same title, axis titles, tick labels, category labels, and legend, now rendered in light gray/off-white tones against the dark background. Layout is pixel-identical to the light render. + Data: The same 10 lines with identical brand-green and matte-red hues as the light render — data colors did not shift between themes, only chrome (text, background) flipped as expected. The LinkHub standout emphasis is equally visible in this theme. + Legibility verdict: PASS — no dark-on-dark failures; all text (title, axis labels, tick labels, data labels, legend) is clearly visible against the near-black surface, confirmed via the same cluster crops used for the light render. + criteria_checklist: + visual_quality: + score: 30 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: All font sizes explicitly set (title 22px, axis 16px, ticks/legend/dataLabels + 14px); no overflow; readable in both themes + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: Collision-nudge logic fixed the tight endpoint-label clusters flagged + in attempt 1 — verified via zoomed crops, no overlap remains + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Marker radius 5 (6.5 standout) / line width 2 (3.5 standout) well + matched for 10 sparse lines + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Green/red direction encoding is redundant with each line's up/down + slope, colorblind-safe + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Y-domain now tightened to the actual 55-91 data band (padded ~50-95), + fixing the dead-space issue from attempt 1; balanced margins confirmed via + edge crops + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: '"Customer Satisfaction Score" plus labeled survey waves are fully + descriptive' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: Correct Imprint semantic assignment (green=improved pos.1, matte + red=declined pos.5); theme-correct chrome in both renders + design_excellence: + score: 18 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Thoughtful semantic color use, phantom-series legend, direct labeling, + standout-mover emphasis — clearly above a generic default line chart, though + short of FiveThirtyEight-level polish + - id: DE-02 + name: Visual Refinement + score: 6 + max: 6 + passed: true + comment: No gridlines, minimal chrome, generous whitespace, and the label-crowding + weakness from attempt 1 is now resolved + - id: DE-03 + name: Data Storytelling + score: 6 + max: 6 + passed: true + comment: Color + slope direction plus the standout-mover line-weight/marker + emphasis gives the chart a clear, immediate focal point + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct slope-chart construction via line series across two categorical + positions + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Endpoint labels, direction color-coding, and labeled time-point axes + all present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: X = survey wave, Y = satisfaction score; all 10 entities visible + on both axes + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format exact; legend labels ("Improved"/"Declined") match the + data story + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Shows both increases and decreases with varying magnitude across + 10 entities + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Plausible, neutral business scenario (product satisfaction across + two survey waves) + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 0-100 satisfaction scores with realistic 55-91 spread + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: No functions/classes beyond small inline helpers, linear data-then-chart + structure + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Deterministic hardcoded data, no RNG needed + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: No stray imports; uses only the Highcharts global and window tokens + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean, appropriate complexity for the collision-nudge logic, no fake + UI + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Correct mount-node contract, animation disabled, credits disabled + library_mastery: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Idiomatic dataLabels + phantom-legend-series pattern; enableMouseTracking:false + now correctly scoped only to the empty legend-proxy series + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Tooltips re-enabled on real series, plus dataLabels crop:false/overflow:allow + and the phantom-legend pattern are genuinely Highcharts-specific techniques, + though the overall chart remains a fairly standard direct-labeled line chart + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - custom-legend + patterns: + - iteration-over-groups + dataprep: [] + styling: + - minimal-chrome