A Helix project is one plain-text JSON file (UTF-8). Any program can read or write it: your data are never locked into Helix.
- Old files always open. Fields are only ever added in new versions, never renamed or removed.
- Newer files open too. A version of Helix keeps the fields it doesn't know and writes them back when saving, so opening a newer file in an older Helix loses nothing.
- Forgiving reading. Field names and choices are read whatever their case
(
tableType,TableType,"XY"). A value of the wrong type, or a choice Helix doesn't offer (a dash"zigzag"), falls back to its default; a number beyond what Helix offers is brought back within (a point size of 500 reads as 20). A broken item is left out on its own, and Helix says so. A table is put straight: values beyond its last column are left out (and said), exclusions and column widths that point nowhere are dropped, and a grouped table'sgroupsandreplicatesare those of its columns. A file Helix had to change as it read it (repaired, or written by an older Helix) opens as edited, and saving it writes it up to date. - Nothing about you. The file holds the project only: no file paths, user names or app state.
{
"app": "helix",
"schemaVersion": 1,
"savedAt": "2026-09-24T12:00:00.000Z",
"project": {
"nodes": { "<id>": { … }, … },
"rootOrder": ["<table id>", …],
"childOrder": { "<table id>": ["<analysis or graph id>", …] },
"activeNodeId": "<id>"
}
}appis always"helix".schemaVersionandsavedAtare informative.nodesholds every item, keyed by itsid.rootOrderlists the data tables in sidebar order;childOrderthe analyses and graphs of each table. Items missing from these lists are shown after the others.activeNodeIdis the item shown when the file opens.
Every item has id, type ("table", "analysis" or "graph"), name, and
parentId: null for a table, the table's id for an analysis or a graph.
{
"id": "table_x1y2", "type": "table", "name": "Table 1", "parentId": null,
"tableType": "grouped",
"data": {
"columns": ["Title", "A:Y1", "A:Y2", "B:Y1", "B:Y2"],
"rows": [
["Day 1", "10", "11", "14", "15"],
["Day 2", "12", null, "18", "19"]
],
"groups": 2,
"replicates": 2
}
}tableType:"column","xy","grouped","multiple"(Multiple Variables) or"contingency".columns: the column names. The first is always the row-title column (Title); in an XY table the second is X.rows: one list per row, one cell per column, as typed: text, ornullfor an empty cell. Numbers are written as text ("2.5"), exactly as entered. A row may be shorter thancolumns: the missing cells are empty.- A grouped table names its sub-columns
<group>:<sub-column>("A:Y1") and givesgroupsandreplicates(sub-columns per group).
Optional fields:
| Field | Meaning |
|---|---|
excluded |
Cells excluded from analyses and graphs, as "row,column" (0-based, column 0 is Title): ["2,1"] |
widths |
Column widths set by the user, in pixels, by column name |
freezeTitle |
true keeps the Title column visible while scrolling |
Files from before Helix 1.0.3 may also have seriesColors, a color per column (or
group), and pointColors, a color per cell as "column name#row", each "blue",
"red", "green", "purple", "orange" or "black": Helix reads them as the
colors of the table's graphs (see Graph).
{
"id": "analysis_k3m4", "type": "analysis", "name": "Analysis 1", "parentId": "table_x1y2",
"analysisType": "two-way-anova",
"analysisParams": { "design": "ordinary", "posthoc": "tukey", "posthocTarget": "groups", "sets": ["A", "C"] }
}An analysis stores what to compute, never its results: Helix computes them again
from the table when the file opens, so results always match the data.
analysisParams holds the choices made in the analysis's wizard (see
src/stats/types.ts). sets (Helix 1.0.5 and later) names the data sets it runs on, in
order: columns, or a grouped table's groups. Without it, an analysis runs on all of
them (a two-group test: the first two holding data).
{
"id": "graph_p5q6", "type": "graph", "name": "Graph 1", "parentId": "table_x1y2",
"graphType": "grouped-bars",
"graphOptions": { "center": "mean", "error": "sem", "width": 640, "height": 460, … }
}graphOptions holds every display setting (see GraphOptions in
src/store/types.ts). A missing setting takes its default.
A graph's colors and style are its own:
"palette": { "name": "okabe-ito", "kind": "categorical", "colors": ["#000000", "#E69F00", …] },
"series": { "Control": { "color": "#0072B2", "shape": "square", "dash": "dash", "pattern": "diagonal" } },
"points": { "Control#3": { "color": "#D55E00", "size": 11, "shape": "diamond", "outline": 2, "outlineColor": "#000000" } },
"fill": 1, "pointFill": 1, "pointSize": 7, "pointShape": "circle", "pointOutline": 1.5,
"outline": 1.5, "outlineColor": "#000000", "pointOutlineColor": "#FFFFFF", "barGap": 0.1,
"lineWidth": 2, "lineDash": "solid", "errorWidth": 1.5, "errorCaps": 6, "errorColor": "#000000", "errorDirection": "above",
"legend": { "x": 0.8, "y": 0.1 }, "note": { "x": 30, "y": 40 }, "xTitleGap": 12, "yTitleGap": 2palette: which palette (src/lib/palettes.ts), and its colors, written out so the figure keeps them whatever Helix's list of palettes becomes.kindis"categorical"(colors taken in turn) or"sequential"/"diverging"(a gradient: series take evenly spaced colors along it, a heatmap all of it).reversed: true: its colors are written from the last (Reverse Colors).series: the series styled by hand, by column or group name (a volcano's are"down"and"up"):color,shapeof its points,dashof its line ("solid","dash","dot","dashdot") andpatternover its bars, area or slice ("diagonal","back-diagonal","crosshatch","horizontal","vertical","grid","dots"); the others take the palette's color for their place and the graph'spointShapeandlineDash.points: the points styled one by one, as"column name#row"(row 0 is the first), each with any ofcolor,size,shape,fill(how opaque, 0 to 1),outline,outlineColor(a color, or"fill": the point's own color),label(true: it shows its row's title, or its row number, beside it) andlabelOffset(where that name was moved, in pixels from the point,ydownward); the rest comes from its series and the graph. A volcano's genes are their rows alone,"#row"; a gene's own style wins over its up / down / not significant look. They follow the table: a renamed column, inserted or deleted rows.fill: how opaque bars, boxes, violins and areas are, from 0 to 1;pointFill, points.pointSize(2 to 20) andpointOutline(0 to 6), in pixels, andpointShape("circle","square","diamond","triangle-up","triangle-down"): every point of the graph.outline(in pixels, 0 to 6) andoutlineColor: the outline of bars, boxes, violins, areas and pie slices;pointOutlineColor, of points. Without a color, an outline takes the color of what it outlines.barGap: on interleaved grouped bars and XY bars, the gap between the bars of a row (or of an X), as a fraction of it, 0 (side by side) to 0.5.lineWidth,lineDash: every series line's thickness (in pixels, 0.5 to 6) and dashes.errorWidth(0.5 to 6) anderrorCaps(0 to 20), in pixels,errorColor(none: each series' color) anderrorDirection("both"or"above": up, or right on horizontal bars): error bars, and the mean or median line drawn with them.legendandnote: texts moved by hand. The legend (or a heatmap's color legend), as a fraction of the plot area from its bottom-left corner; the note (a fit's equation), in pixels from the plot's top-left corner,ydownward. Without them, each sits in its usual place.xTitleGap,yTitleGap: the space in pixels between each axis title and its tick labels, 0 to 40.
A graph without palette comes from Helix 1.0.2 or before: it takes the Helix
palette, its table's seriesColors, its old heatmapColorScale or
volcanoColorUp / volcanoColorDown / volcanoPointSize, fills at 0.5 (a pie's at 1,
a volcano's points at 0.75), its type's point size and outline, and bars side by side
(barGap 0), so it looks as it did.