Support Arduino-style labeled multi-value plotter data#487
Merged
makermelissa merged 2 commits intocircuitpython:betafrom May 6, 2026
Conversation
Match the Arduino IDE Serial Plotter input format so sketches that print 'label:value' pairs separated by tabs (or commas/spaces) are plotted with the labels shown in the legend instead of confusing the parser. Each labeled series is tracked by name across frames so the order can vary, and a per-frame padding pass keeps the x-axis aligned when not every series reports on every line. Existing tuple, list, and plain CSV formats continue to work unchanged. The default color palette is also expanded from 3 colors to 16 so plots with many series remain legible. Fixes circuitpython#457
- Drop pale tints, gray, and brown from palette since they wash out on the dark theme's #777 plotter background. - Pull legend label color from --terminal-text-color so it contrasts with the active theme instead of being hardcoded black.
makermelissa
approved these changes
May 6, 2026
Collaborator
makermelissa
left a comment
There was a problem hiding this comment.
Successfully tested with the following:
# plotter_test.py
#
# Test sketch for circuitpython/web-editor PR #487
# (Arduino-style labeled multi-value plotter data).
#
# Copy this to CIRCUITPY/code.py, then open the web-editor plotter on the
# PR #487 build. Each phase runs for ~10 seconds and prints a banner to
# the serial console so you can tell which behavior is being exercised.
#
# What to look for in the plotter as it runs:
#
# 1. Legacy CSV -> 3 lines, labels "0","1","2", legend visible.
# 2. Tab-separated labeled (Temp/Hum) -> 2 named lines, legend updates.
# 3. Comma labeled (AS7343-style) -> 8 named lines, all distinct
# colors (no black collapse).
# 4. Reorder test -> Temp/Hum stay on the same color/line even though
# the sketch flips their order each frame.
# 5. Missing-label test -> "B" line goes flat / dotted on frames where
# it's omitted (null-padded), "A" stays smooth.
# 6. Unlabeled -> labeled -> the two anonymous lines get renamed to
# X / Y in the legend without spawning new series.
# 7. Tuple + list legacy formats -> still plot, still positional.
# 8. 16-series stress -> all 16 lines visible, colors round-robin
# (S0 and S12 share, S1 and S13 share, etc.).
#
# No external libraries beyond `time` and `math`; should work on any
# CircuitPython board that has a USB serial console.
import time
import math
PHASE_SECONDS = 10
FRAME_DELAY = 0.1 # 10 Hz, matches a typical Arduino plotter sketch
def banner(msg):
# Blank line + comment line. The plotter ignores lines that don't
# parse to numbers, so this is safe to print mid-stream.
print()
print("# ---", msg, "---")
def run_for(seconds, fn):
end = time.monotonic() + seconds
i = 0
while time.monotonic() < end:
fn(i)
i += 1
time.sleep(FRAME_DELAY)
# Phase 1: legacy CSV (must keep working unchanged)
def phase_csv(i):
a = 50 + 20 * math.sin(i * 0.1)
b = 50 + 20 * math.sin(i * 0.1 + 2)
c = 50 + 20 * math.sin(i * 0.1 + 4)
print("{:.2f},{:.2f},{:.2f}".format(a, b, c))
# Phase 2: tab-separated labeled, the canonical Arduino format
def phase_tab_labeled(i):
temp = 23 + 2 * math.sin(i * 0.05)
hum = 55 + 5 * math.cos(i * 0.05)
print("Temp:{:.2f}\tHum:{:.2f}".format(temp, hum))
# Phase 3: comma-separated labeled, AS7343-style names from issue #457
AS7343_BANDS = ["405nm_F1", "425nm_F2", "475nm_F3", "515nm_F4",
"550nm_F5", "600nm_F6", "640nm_F7", "Clear"]
def phase_comma_labeled(i):
parts = []
for idx, name in enumerate(AS7343_BANDS):
v = 200 + 80 * math.sin(i * 0.07 + idx * 0.6)
parts.append("{}:{:.1f}".format(name, v))
print(",".join(parts))
# Phase 4: reorder labels each frame; same line/color must stick.
def phase_reorder(i):
temp = 23 + 2 * math.sin(i * 0.05)
hum = 55 + 5 * math.cos(i * 0.05)
if i % 2 == 0:
print("Temp:{:.2f}\tHum:{:.2f}".format(temp, hum))
else:
print("Hum:{:.2f}\tTemp:{:.2f}".format(hum, temp))
# Phase 5: drop label B on every other frame so it gets null-padded.
def phase_missing(i):
a = 30 + 10 * math.sin(i * 0.1)
b = 70 + 10 * math.cos(i * 0.1)
if i % 2 == 0:
print("A:{:.2f}\tB:{:.2f}".format(a, b))
else:
print("A:{:.2f}".format(a))
# Phase 6: start unlabeled, then add labels. Existing series should
# get renamed in place rather than duplicated.
def phase_upgrade(i):
x = 40 + 15 * math.sin(i * 0.1)
y = 60 + 15 * math.cos(i * 0.1)
if i < 30:
print("{:.2f},{:.2f}".format(x, y))
else:
print("X:{:.2f},Y:{:.2f}".format(x, y))
# Phase 7: tuple + list legacy paths still parse.
def phase_tuple_list(i):
a = 10 + 5 * math.sin(i * 0.1)
b = 20 + 5 * math.cos(i * 0.1)
c = 30 + 5 * math.sin(i * 0.1 + 1.5)
if i % 2 == 0:
print("({:.2f},{:.2f},{:.2f})".format(a, b, c))
else:
print("[{:.2f},{:.2f},{:.2f}]".format(a, b, c))
# Phase 8: 16 labeled series to push color palette past its 12-entry
# table and confirm the round-robin (no '#000000' fallback line).
def phase_palette(i):
parts = []
for s in range(16):
v = 100 + 40 * math.sin(i * 0.08 + s * 0.4)
parts.append("S{:02d}:{:.1f}".format(s, v))
print("\t".join(parts))
PHASES = [
("Phase 1: legacy CSV (positional)", phase_csv),
("Phase 2: tab-separated labeled (Temp/Hum)", phase_tab_labeled),
("Phase 3: comma labeled, AS7343 spectral", phase_comma_labeled),
("Phase 4: labeled, reordered every frame", phase_reorder),
("Phase 5: missing label every other frame", phase_missing),
("Phase 6: unlabeled -> labeled in-place", phase_upgrade),
("Phase 7: legacy tuple + list", phase_tuple_list),
("Phase 8: 16 series, color palette wraps", phase_palette),
]
banner("PR #487 plotter test sketch starting")
print("# Each phase runs for {} seconds.".format(PHASE_SECONDS))
while True:
for label, fn in PHASES:
banner(label)
run_for(PHASE_SECONDS, fn)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #457.
The Arduino IDE Serial Plotter accepts a
label:valueformat withtab/comma/space separators (and supports many series at once). The
web-editor plotter previously only understood plain CSV / tuple /
list lines, so a sketch that prints
Temp:23.4\tHum:55.1wouldeither drop the line or only plot the digits it could pull from the
front of the token.
This PR teaches
plotValuesthe labeled format:parseLabeledValueshelper splits on,,\t, or runs ofwhitespace, then peels off an optional
label:prefix per token.stay on the same line across frames even if the sketch reorders
them (or omits some on a given frame). Unlabeled lines keep the
existing positional behavior, so old CSV / tuple / list sketches
are unaffected.
dataset is padded with
nullso the x-axis stays aligned.reused round-robin, so multi-channel sketches (e.g. the AS7343
spectral example referenced in the issue) don't all collapse to
black after the fourth series.
Manual test cases
I unit-tested the parser against the following inputs (all parse to
the expected
{label, value}pairs):1,2,31\t2\t31 2 3Temp:23.4,Hum:55.1405nm_F1:123\t425nm_F2:456\tClear:789A:1\tB:-2.5\tC:3e21,Temp:42,3(mixed labeled + positional)A:1\tB:2\t(trailing tab)Existing tuple
(1,2,3)and list[1,2,3]paths are unchanged.npm run buildpasses locally.