CV Chart Series
One score per detected face? CV Chart Series is the chart the numbers always needed
- values
- spec
What this is for
Half the arrays in a vision workflow aren't pictures at all - they're a measurement per thing. One similarity score per detected face, a histogram, a metric per method, a score against a parameter sweep. Preview CV Array renders those as a few anonymous pixels and min-max normalization deletes the absolute scale, which is exactly the part you cared about: is this score above the 0.363 threshold your face-recognition model uses, or not?
CV Chart Series draws them as labelled bars (or lines, areas, steps, scatter) with the values readable, in the node. Hover gives exact numbers, box-zoom works, and the toolbar saves a PNG - so the picture you paste into a write-up carries the numbers, not just the shape.
How it works
The full Apache ECharts option object is built in Python - palettes, legend, reference lines, value formatting - and passed to web/cv_charts.js, which does nothing but instantiate it. All the chart logic is server-side and comes back out as the spec STRING, so you can read and diff the plot as JSON. Values are capped at 200,000 points; past that the node tells you you're charting an image by mistake rather than freezing the tab.
The inputs that matter
values is required. A 1-D array is one series of N samples; a 2-D array is samples × series, one column per series. Both (N, 1) and (1, N) count as a single series of N samples, so you don't have to reshape before feeding it.
chart is the second required input and the one that decides honesty of read: bar for independent measurements (one per object, one per method), line/area/step when consecutive samples form a signal (a histogram, score versus a swept parameter), scatter when samples have no order at all.
From the optional set, the three worth wiring:
x_labels- name per sample,;/newline/comma or JSON. Missing names fall back to#0,#1, … Feed the same string that labels the corresponding image tiles and bar i provably refers to tile i. That is a real difference from eyeballing a legend.reference_lines-valueorvalue=label, separated like the label lists.0.363=SFace thresholdpaints your decision threshold into the picture instead of leaving it in a widget, andcolor_by→above/below first reference linethen splits every mark into pass and fail colours with a legend saying which is which. That's the whole point: a chart where you can see the threshold crossing without hovering anything.orientation- horizontal when the sample names are file names, person names or anything else longer than three characters. Vertical bars with rotated labels are where charts go to die.
series_names names the columns for the legend. color_by also takes series color (default) or value (gradient) with gradient_palette. value_range defaults to start at zero, which is correct for bars - auto fits the data and exaggerates small differences, manual (with min_value/max_value) is what makes two runs comparable. show_values prints each value next to its mark so the chart survives being saved as a PNG. Plus x_axis_name/y_axis_name (y wants units), palette, theme, decimals, chart_height.
Output is spec. The node is an output node, so it draws on Queue; Preview as Text reads the charted values.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
# restart ComfyUI
Or search ComfyUI CV in ComfyUI Manager (registry publisher bmad4ever). Python ≥ 3.12 and a V3-node-API ComfyUI; opencv-contrib-python-headless~=5.0.0.93 comes from the pack's requirements. ECharts is vendored - no model or library downloads for this node.
Common issues
- Labels one short. Not an error. Missing names get numbered placeholders by design; a chart with a short label list is still a readable chart, and the author decided a label typo shouldn't kill a run.
- Bars all look identical, or wildly different across runs. Check
value_range.autorescales to whatever is in front of it, so small differences shout. Pin it. - Nothing appears in the node. Reload the page - the chart is rendered by a frontend module, and a tab that was already open when you installed won't have it.
- Contrib-backed nodes vanished pack-wide. A non-contrib OpenCV wheel clobbered the shared
site-packages/cv2.python tools/repair_opencv_contrib.py --checkthen--apply.
Inputs (19)
| Name | Type | Default | Description |
|---|---|---|---|
| values | NPARRAY | 1-D array (one series), or 2-D (samples x series) with one COLUMN per series. (N, 1) and (1, N) both count as one series of N samples. | |
| chart | COMBO | bar | Mark type. 'bar' for independent measurements (one per detected object/method); 'line'/'area'/'step' when consecutive samples form a signal or a curve (histogram, score vs parameter); 'scatter' to show samples without implying continuity. |
| title | STRING | Heading drawn above the chart. Empty draws none. | |
| x_labelsopt | STRING | Name per sample, as a JSON list or ';' / newline / ',' separated text (e.g. 'face 0;face 1;face 2'). Missing names fall back to '#0', '#1', ... - a short list never breaks the chart. Feed the same string that labels the corresponding images. | |
| series_namesopt | STRING | Name per series (per COLUMN of values), same formats as x_labels. Shown in the legend and the tooltip; defaults to 'series 0', 'series 1', ... | |
| reference_linesopt | STRING | Horizontal marker lines: 'value' or 'value=label', separated like the label lists - e.g. '0.363=SFace threshold'. This is how a decision threshold becomes visible in the picture instead of living only in a widget. | |
| color_byopt | COMBO | series color | 'series color' gives each series one color. 'value (gradient)' colors every mark by its own value and adds a labelled scale. 'above/below first reference line' splits the marks into two colors at the threshold - the pass/fail read, with the legend saying which side is which. |
| orientationopt | COMBO | vertical (bars go up) | Horizontal keeps long sample names readable - the right choice when the labels are file or person names rather than indices. |
| show_valuesopt | BOOLEAN | false | Print each value next to its mark, so the chart reads without hovering (and survives being saved as a PNG). |
| value_rangeopt | COMBO | start at zero | 'auto' fits the values, which EXAGGERATES small differences; 'start at zero' keeps bar lengths proportional to the values; 'manual' pins the axis so separate runs are comparable. |
| min_valueopt | FLOAT | 0.00-1000000000–1000000000 | Value-axis minimum ('manual' range only). |
| max_valueopt | FLOAT | 1.00-1000000000–1000000000 | Value-axis maximum ('manual' range only). |
| x_axis_nameopt | STRING | Caption under the sample axis (what the samples ARE: 'detected face', 'bin', 'method'). | |
| y_axis_nameopt | STRING | Caption beside the value axis, with units ('cosine similarity', 'pixels', 'dB'). | |
| paletteopt | COMBO | colorblind-safe (Okabe-Ito) | Series colors. The default stays readable for a colour-blind reader and in greyscale; 'bright' is punchier but puts a red and a green next to each other; the sampled ramps suit ORDERED series (sweep steps). |
| gradient_paletteopt | COMBO | viridis | Ramp used by the 'value (gradient)' coloring; 'viridis' matches 'Preview CV Array' heatmaps. |
| themeopt | COMBO | dark | Chart colors. 'light' is for a PNG that will be pasted into a document. |
| decimalsopt | INT | 30–8 | Decimals shown in the tooltips and the printed values. Trailing zeros are dropped. |
| chart_heightopt | INT | 320160–2048 | Height of the chart area inside the node, in pixels. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| spec | STRING | The chart definition as JSON - the exact option the frontend renders. Wire into 'Preview as Text' to read the values that were charted. |