pyplotc is a Python tool for visualizing space-separated telemetry/log data generated by C/C++ code. It supports both legacy fprintf structure files and newer std::ostringstream data-writing snippets, and can also read column names from a # header line in the data file itself.
Plotting is available through Gnuplot or Matplotlib.
- Automatic parsing of C/C++ structure files:
fprintfformat viafprintf_structure.pystd::ostringstreamdata-writing format viaoss_structure.py
- Optional
#header line in data files for explicit column names (for examplearm0_j0_set_velocity) - Binary CTLG telemetry (
.bin) with column schema from file header viatelemetry_helper.py - Flexible plotting: select columns by index or variable name, split series into multiple graphs with
: - Supports Gnuplot and Matplotlib
Install Python dependencies:
pip install -r requirements.txtOr install packages individually:
pip install py-gnuplot==1.3 matplotlib pandas pycparserFor Gnuplot plotting (--plot_tool=gnuplot), also install the system gnuplot executable:
sudo apt-get install gnuplotWhen resolving column names, plot_data.py uses this order:
--structure_fileif provided- Else
#header line in the data file - Else error
If a data file contains a # header but you also pass --structure_file, the structure file takes precedence. The header row is still skipped when reading data.
If the header lists more names than the data rows actually contain (max-size header vs runtime-sized rows), pyplotc warns and only plots columns whose index exists in the data.
- Add a structure file in
data_structure/(copy the C++fprintf/oss <<data-writing snippet). - Update
replacement.jsonwith numeric loop bounds that match your robot (e.g.NUM_ARM,ctx.robot_state.arm_size). - Run
plot_data.pywith--structure_file,--data_file, and--target.
python plot_data.py \
--structure_file=data_structure/example.txt \
--data_file=data/example.txt \
--target=3,17:position_errorIn the first graph, 3,17 does not mean “only column 3 and column 17”. Each number looks up that column’s variable name and plots every column with the same name. With example.txt, column 3 is set_velocity and column 17 is read_velocity, so the first graph plots all set_velocity and all read_velocity columns. The second graph (position_error) plots every column with that name.
Structure file data_structure/example_oss.txt describes how each data row is written with stream insertion.
python plot_data.py \
--structure_file=data_structure/example_oss.txt \
--data_file=data/example.txt \
--target=set_velocity,position_errorRepeated field names such as set_velocity expand to every matching column (both arms and all joints).
data/example_header.txt starts with a comment header:
# arm0_j0_set_velocity arm0_j1_set_velocity ... status t1 t2 receiver_communication_time receiver_running_time
1.1000 1.2000 ...
No structure file is required when the header is present:
python plot_data.py \
--data_file=data/example_header.txt \
--target=arm0_j0_set_velocity,position_error_0Use the exact names from the header row when targeting specific joints or array elements.
Binary telemetry files carry their own column schema in the CTLG header. No structure file is required:
python plot_data.py \
--data_file=data/telemetry.bin \
--target=motor_target,joint_velocity \
--plot_tool=matplotlibSemantic aliases are derived from column names (for example arm0_j0_motor_target → motor_target). Use --column_mode to plot by direct column index.
Convert .bin or .txt telemetry to CSV with:
python telemetry_helper.py input.bin -o output.csv| Argument | Description |
|---|---|
--structure_file |
Optional path to a C/C++ structure snippet (fprintf or ostringstream); not used for .bin telemetry |
--data_file |
Path to the data file (space-separated .txt or CTLG .bin) |
--data_directory |
Optional directory prepended to --data_file (resolved as data_directory/data_file) |
--replacement_file |
JSON macro replacements for loops in structure files (default: replacement.json) |
--target |
Comma-separated columns or variable names; use : for separate graphs |
--index |
Optional comma-separated flat indices (e.g. 0,1) applied per same-name variable to limit expanded columns |
--column_mode |
When set, numeric targets refer to direct column indices instead of variable-name lookup |
--start_frame |
Start row index (default: 0) |
--end_frame |
End row index (default: all rows) |
--plot_tool |
gnuplot (default) or matplotlib |
- Structure expressions like
curobo_command_ptr->JointPosCmd[i]map to target nameJointPosCmd --target=set_velocityplots every column with that name- With a
#header file, use explicit names such asarm0_j0_set_velocity
All column numbers in --target, printed figure_indexs, and legend labels use 0-based indexing (first data column is 0).
Gnuplot reads files with 1-based columns internally, but pyplotc converts automatically. Legend titles still show the 0-based index (for example 3: set_velocity).
A number refers to a column index lookup, then expands to all columns sharing that base name:
| Target | Column lookup | Plotted columns (example.txt) |
|---|---|---|
3 |
column 3 → set_velocity |
all set_velocity → 0, 1, 2, 3, 4, 5 |
17 |
column 17 → read_velocity |
all read_velocity → 12, 13, 14, 15, 16, 17 |
3,17 |
both lookups in one graph | all set_velocity + all read_velocity |
If the variable at that column index is unique (appears only once), the result is just that one column.
--index is same-name variable based: indices count within each target's expanded list of columns sharing that variable name, not global file column numbers.
Use --index=0,1 to plot only flat positions 0 and 1 of each expanded same-name target. Explicit bracket syntax is not filtered.
For each target, indices refer to the flattened 1D order in which the structure file expands nested loops (outer loop slowest, inner loop fastest). They are not interpreted as “last loop dimension only”.
For set_velocity[i][j] with NUM_ARM=2, JOINTS_PER_ARM=3, the flattened order is:
arm0_j0, arm0_j1, arm0_j2, arm1_j0, arm1_j1, arm1_j2 → cols 0–5
So --index=0,1 picks flat positions 0 and 1 (arm0_j0, arm0_j1), not “joint 0 and 1 on every arm”. To pick one flattened element, use set_velocity[2] (0-based index 2 → col 2 here).
Example with example.txt:
python plot_data.py \
--structure_file=data_structure/example.txt \
--data_file=data/example.txt \
--target=3,17:position_error,set_velocity[2] \
--index=0,1| Graph | Target | Plotted columns |
|---|---|---|
| 1 | 3,17 |
set_velocity flat[0,1] → 0, 1; read_velocity flat[0,1] → 12, 13 |
| 2 | position_error |
position_error flat[0,1] → 18, 19 |
| 3 | set_velocity[2] |
col 2 only (explicit index unchanged) |
Without --index, 3,17 and position_error plot every matching column.
Examples below use example.txt with NUM_ARM=2, JOINTS_PER_ARM=3 (see replacement.json).
| Target | Meaning | Result |
|---|---|---|
set_velocity |
all columns with this name | 6 curves (cols 0–5) |
set_velocity[2] |
one element, flat 0-based index 2 | 1 curve (col 2 = arm0_j2) |
Bracket indexing is always flat after nested loops are expanded in the structure file (outer loop slow, inner loop fast). There is no set_velocity[arm][joint] syntax — use the flattened index instead.
set_velocity→ all arms and jointsset_velocity[2]→ one flattened element (index 2), not allset_velocity- With a
#header file, use explicit names such asarm1_j2_set_velocity
Structure files in data_structure/ describe how one output line is written in C/C++.
| File | Format | Purpose |
|---|---|---|
example.txt |
fprintf |
Simple tutorial example |
example_oss.txt |
ostringstream |
Same layout as example.txt, stream style |
Each output value must be separated by spaces so columns align with the data file.
replacement.json substitutes macros and loop bounds in structure files:
{
"NUM_JOINTS": 6,
"robot.arm_size[0]": 2,
"robot.joint_size[i]": 3
}Data files contain space-separated numeric values. Each row is one sample/frame.
| File | Description |
|---|---|
data/example.txt |
Sample data for example.txt / example_oss.txt |
data/example_header.txt |
Same data with a # name header row |
Example header + data:
# arm0_j0_set_velocity arm0_j1_set_velocity ... receiver_running_time
0.1000 0.2000 0.3000 ... 1000
0.1100 0.2100 0.3100 ... 1001
pyplotc/
├── plot_data.py # CLI entry point
├── telemetry_helper.py # CTLG binary telemetry parser / CSV converter
├── fprintf_structure.py # fprintf parser
├── oss_structure.py # ostringstream data parser
├── requirements.txt # Python dependencies
├── replacement.json
├── data_structure/
│ ├── example.txt
│ ├── example_oss.txt
└── data/
├── example.txt
└── example_header.txt