PivoGraph

Format specification

PivoGraph project file format (.pivograph), schema version 3

Current schema Introduced in PivoGraph 1.7 Format overview Markdown version Previous version (v2)

This specification describes the project file written by File → Save Project… and read by File → Open Project… or drag-and-drop. It also defines how external tools and AI agents can create projects that open for further editing in PivoGraph. Schema version 3 supersedes version 2; version 1 and 2 files keep opening and are migrated automatically.

The JSON Schema is generated from the same validator the application uses, so it is authoritative for field names, enumerations and limits. This page explains the semantics that a schema cannot express.

What changed since version 2

Version 3 collects the changes to the document structure made for PivoGraph 1.7. A version 2 document is still a valid starting point: everything below is either a new optional value or is converted on open.

  • Spike width in points. The barsSpikesDroplines style (the spikes of xy-spikes / xy-spikes-error) replaces widthRatio (a fraction of the X spacing) with widthPt, the spike width in points (default 4). The old fraction depended on the data's X spacing and cannot be converted exactly, so it is dropped on open and migrated spikes use the default width.
  • Spike baseline moved to the graph settings. barsSpikesDroplines.beginAtY is removed. Where the spikes start is now graphSettingsOverride.baseline / baselineY, the same setting that bar graphs use (automatic: the axis minimum / zero, manual: the given Y with a baseline drawn across the plot, hidden: the given Y without the line). A non-zero global beginAtY in a version 2 file becomes baseline: "manual" with that baselineY.
  • Area fill to the X axis. areaFill.position accepts toXAxis in addition to belowCurve and aboveCurve: the area between the curve and Y = 0 (clamped to the plot's Y range). belowCurve now fills down to the lower end of the Y axis and aboveCurve up to the upper end; version 2 always filled down to Y = 0, so with negative data a migrated belowCurve fill now reaches the bottom of the plot.
  • Optional symbols and lines on spike graphs. xy-spikes and xy-spikes-error offer the symbol and line elements as optional elements (off by default) in elementActivation.
  • Survival error bar defaults. On survival graphs, error bars now default to both directions (errorBar.direction: "both") with narrow caps (capStyle: "narrowT"); other graph families keep "above" / "wideT". Version 2 survival graphs that can draw error bars keep their previous look through the migration.
  • Censor ticks on survival-staircase-ticks. The short vertical marks at censored observations are a new optional element censorTick (on by default) with its own style: color (default "theme", the data set's border colour — the colour the theme uses for bar borders, darker than the line in several built-in themes), thickness (points, default 1), length (points, default 6) and direction (above — from the step upwards, the default; below; both — centred on the step). A censored observation at the same time as an event is marked at the level after the drop. On the same variant, the plot option errorBars now defaults to "none" (SE and 95% CI remain available); survival-point-to-point-eb keeps "se".
  • Survival axes, minor ticks and end gaps. Continuous axes gain minorTicks (axesOverrides.xContinuous.minorTicks is new; leftY / rightY.minorTicks was an unused integer and is now drawn): the number of segments each major interval is divided into (5 draws four minor ticks per interval; 0 or 1 draws none), or "auto" (default). Automatic minor ticks appear only on survival graphs; other families keep none. axesOverrides.xContinuous.autoRangePadding (boolean, default false) extends the automatic X range by 0.475 major intervals at each end and allows at most five major intervals. Survival graphs now default to an X axis that always starts at time 0, a Y axis from 0 to 110 (fractions: 0 to 1.1) with major ticks every 50 (0.5) and ten minor segments, and minor ticks on X when the axis has at most three major intervals of 5, 50 and the like. Switching a survival graph between percents and fractions rescales explicit leftY range values (×100 or ÷100). Version 2 survival graphs keep their previous axes through the migration.
  • Optional mean bars and points on column graphs. column-before-after offers bar as an optional element (off by default, can be switched per data set): a bar at each column's mean, drawn behind the paired points and their connecting lines. column-floating-bars offers symbol as an optional element (off by default): the individual values drawn on top of the floating bars. On column-mean-error, the graph-scope line switch (off by default) now draws the line connecting the means, as column-mean-error-connected does.
  • t-tests on XY tables. The ttest method is only available for column tables. ttest instances on XY tables are removed on open (they never produced a bracket on the graph).
  • New XY analyses. Two numbers-only methods for XY tables: areaUnderCurve (trapezoid rule per data set, params.baselineY) and xyCorrelation (Pearson or Spearman correlation between X and each data set's Y, params.method). See Analyses.
  • New column analyses. oneWayAnova (three or more data sets: ordinary, Welch, repeated-measures, Kruskal-Wallis or Friedman, with multiple comparisons whose significant results can be placed as brackets) and descriptiveStatistics (numbers only). See Analyses.
  • Nonparametric t-test option. ttest gains params.distribution ("normal" | "nonparametric", default "normal"): the nonparametric choice runs the Mann-Whitney or the Wilcoxon matched-pairs signed rank test. rowTTests is unchanged.
  • Survival event codes. In survival tables only 1 (event) and 0 (censored) are read. Rows with any other value, with a missing time, or with a time of 0 or less are left out of the curves and of survivalComparison, and the analysis results report how many rows were left out. Version 2 counted every non-zero value as an event and kept times of 0 or less.
  • Survival comparison results. survivalComparison also reports the log-rank test for trend (three or more curves, data sets scored 1, 2, 3, … in table order), the 95% confidence interval of each median survival time and, with exactly two curves, the ratio of the median survival times with its 95% confidence interval. The parameters are unchanged: with showPValue the text placed on the graph includes the P value of the test for trend, and with showMedians each median carries its confidence interval.
  • Parts-of-whole graphic size. plot.options.graphicSizePt of partsOfWhole graphs (40 to 300 points, default 144) is the size that is drawn: the diameter of a pie or donut, the long side of the slices bar, or ten dot spacings of the 10 × 10 dot plot. When the graphic does not fit, the plot area grows to fit it (plotGeometry stays as written). Version 2 drew the graphic inside a plot area of fixed size, so a size that did not fit was drawn smaller — a default pie with the total shown was 141 points across, the dot plot about 128.
  • Parts-of-whole legend keys. The legend keys of pie, donut, slices and dot plot graphs are drawn with the outer border of partsOfWholeBorders (no border when the partsOuterBorder element is off), matching the graph. Version 2 drew them with the theme's per-data-set bar border colours.

Migration of version 2 files. The migration runs in memory when a version 2 file is opened: widthRatio is dropped from every barsSpikesDroplines patch; a non-zero global beginAtY is baked into graphSettingsOverride.baseline = "manual" / baselineY unless a baseline is already set, and beginAtY is removed everywhere else; ttest analyses on xy tables are removed; on column-before-after graphs, brackets attached to data sets at both ends drop yOffsetAboveBars and keep their absolute y (the bracket anchors of that graph type now sit at the highest value of each data set instead of the X axis, and the offset is re-derived from y when the graph is drawn, so the bracket stays where it was); on survival-point-to-point-eb and survival-staircase-ticks graphs, the global errorBar override gets direction: "above" and capStyle: "wideT" where those fields are not already set; on survival-staircase-ticks graphs, a missing plot.options.errorBars becomes "se" and the global censorTick override gets direction: "both" and length: 8, with an explicit line.color / line.thickness (at least 1 point) in the global or per-data-set layer copied to censorTick in the same layer, which reproduces the old ticks (dashed connecting lines no longer make the ticks dashed; ticks that follow the theme now take the darker border colour in themes whose bar borders differ from the lines); on every survival graph (survival-* variants), axesOverrides.xContinuous gets minorTicks: 0, axesOverrides.leftY gets minorTicks: 0, and on a linear Y axis whose maximum is still automatic leftY.rangeMax becomes 120 (1.2 when plot.options.showAs is "fractions"), which reproduces the previous 0 to 120 axis with ticks every 20 — fields already set are left alone; on parts-pie, parts-donut, parts-slices and parts-dot-plot graphs whose graphicSizePt did not fit the plot area, graphicSizePt becomes the size version 2 actually drew (rounded down to 0.01 point), so the graphic and the plot area keep their size. Not migrated: the automatic survival X range now always includes time 0 (version 2 used the earliest observation, which cut off the start of curves whose data begins late); staircase curves start at time 0 rather than at the left end of the X axis; survival rows coded other than 1 / 0 or with a time of 0 or less are left out (the data are not rewritten); parts-of-whole legend keys take the graph's outer border. Existing areaFill.position values are kept. The original file is never rewritten; saving writes a version 3 document.

What changed since version 1 (version 2, PivoGraph 1.6)

Version 2 collected every change to the document structure and to rendering defaults made for PivoGraph 1.6. A version 1 document is still a valid starting point: everything below either adds optional fields or is converted on open.

  • Rich text. Every text field that is drawn on the graph — graph / X / Y titles, free text, the label of a significance bracket, text bound to a drawing shape, and custom legend labels — is now a rich text value: either a plain string or { "runs": [...] } with per-run bold / italic / superscript / subscript. Plain strings are taken literally; the ^{…} / _{…} markup of version 1 is no longer interpreted. See Rich text values.
  • Title rotation (axesOverrides.titlesAndFonts.<title>.rotation) is a number in degrees (clockwise positive) instead of the names horizontal / verticalUp / verticalDown.
  • Line spacing. Multi-line free text, titles and shape-bound text carry an optional lineSpacing (shapes: textLineSpacing) in points.
  • Category label weight. axesOverrides.xCategorical.labelBold switches the bold category labels of column-type graphs off.
  • Text distance of annotation lines. The default textGap of shape-bound text is 3 points (version 1 drew it at 1.5).
  • Exploded pie / donut slices. New optional element partsExplode (off by default) in elementActivation, keyed per slice by the slice's row index; the offset lives in the bar style as explodeOffsetPt.
  • Fitted-curve draw order. The curve style carries drawOrder: underneath (default; the curve is drawn behind the data points) or onTop.
  • Stacked-bar error bars. grouped-stacked-bars accepts the optional errorBar element (on by default; each segment's error bar is centred on the segment's outer edge, extends to both sides and is drawn on top of all segments).
  • Significance bracket formatting. bracket annotations accept the same line and text formatting as text-bearing shapes: dashStyle, lineColor, fontFamily, base bold / italic, textRotation, textLineSpacing and textGap (default 3, the same as annotation lines) join the existing lineWidth, fontSize and textColor. Brackets did not store a text distance in version 1 and drew it at 1.5 points; old brackets are not migrated to keep that value, so their label sits 1.5 points higher than before.
  • Summary symbols on scatter plots. New optional element summarySymbol (off by default) on the scatter variants of Column, Grouped and Nested graphs (column-scatter-only, grouped-interleaved-scatter, grouped-separated-scatter, grouped-superimposed-scatter, nested-scatter): a second symbol drawn at each group's mean / median on top of the individual points and error bars, styled by the summarySymbol entry of styleOverrides (same fields as symbol; default a white-filled bordered circle of size 6 whose border follows the data set colour). On the same variants the summary line (summaryLine) became an optional element that is on by default, so it can be switched off.
  • More heat map colour scales. plot.options.heatMap.colormap also accepts viridis, magma, cividis (perceptually uniform sequential scales) and purpleOrange (diverging purple – white – orange). Unlike sequential and diverging, these scales do not follow the theme.
  • Heat map colour bar length. The default colorScaleBar.lengthRatio is 1 (the full plot height); version 1 drew it at 0.6, and heat maps from version 1 files keep that value.
  • Edge spacing of bar layouts. graphSettingsOverride.firstGapRatio / lastGapRatio are "auto" (default) or a number ≥ 0: the clear distance from the plot edge to the outer edge of the first / last bar, as a fraction of the bar slot (the row slot on grouped graphs), so 0 puts the bar flush with the edge while the spacing between bars is kept. "auto" leaves half the space between bars, as before. In version 1 the numbers were extra padding added on top of that half space; version 1 values are converted on open (0 becomes "auto", larger values keep their visible distance).
  • Before-after in grouped rows. grouped-rm-across-row defaults to plot.options.seriesAlign: "staggered", so the data sets of a row sit side by side and each subject's values (same sub-column) are joined across the row. Its connecting lines follow the graph-level line style (colour, thickness, dash); the connecting lines of grouped-rm-in-subcol now also follow the line dash style. In version 1 the across-row default was "aligned" and its lines were always solid dark grey. Version 1 across-row graphs get seriesAlign: "aligned" when it is not set, and a global line colour of #3f3f46 unless a concrete line colour is already set globally or for the first plotted data set. Line colours and dash styles that were set explicitly in version 1, and that these connecting lines ignored, apply from version 2.
  • Brackets on grouped graphs. A bracket anchor may carry rowIndex to snap to one data set in one row of a grouped graph (see Annotations and titles).
  • Per-row t-tests. New analysis method rowTTests for grouped tables (see Analyses).

Migration of version 1 files. The migration runs in memory when a version 1 file is opened and makes the explicit choices that keep the old appearance: markup in free text, titles, shape text and legend labels becomes runs (an unclosed ^{ or _{ stays literal); bracket labels and data set names are never parsed; rotation names become degrees; texts that already contain a line break get lineSpacing: 0; shape-bound text without an explicit textGap gets textGap: 1.5; tables that carry a linearRegression / nonlinearRegression analysis get styleOverrides.global.curve.drawOrder: "onTop"; heat maps get styleOverrides.global.colorScaleBar.lengthRatio: 0.6; numeric firstGapRatio / lastGapRatio are converted to the new clear-distance meaning; grouped-rm-across-row graphs get seriesAlign: "aligned" and the former dark-grey line colour. The only intended visible difference is the label of significance brackets without an explicit textGap, which moves up by 1.5 points. The original file is never rewritten; saving writes a current-version document.

1. File basics

  • Plain UTF-8 JSON, no byte-order mark (a leading BOM is tolerated when reading). Extension .pivograph; MIME type application/json. Where a tool needs the JSON language recognised from the file name, .pivograph.json is also accepted when opening.
  • The whole document is one JSON object. Unknown keys are rejected at every level with the JSON path of the offending key. Tool-specific data belongs in usermeta (document level or per table).
  • Everything in the file is data. There is no executable content, no external references are fetched when a file is opened, and the application never downloads the $schema URL.
  • Write complete, read tolerant. PivoGraph writes resolved document state explicitly so that saved settings do not silently inherit future defaults. Hand-written or generated documents may omit any optional field — and may give any nested object partially; the application fills its current defaults. Optional fields are expressed by omitting the key; null is only valid where the field's type includes it (missing cells, unmapped variable roles, automatic geometry).
  • Opening reports two kinds of problems: errors (the file is not opened; the current project is untouched) and warnings (the file opens, but the listed parts were ignored or replaced by defaults, for example a style override keyed to a data set that does not exist). Both carry JSON paths.

2. Envelope

{
  "$schema": "https://pivograph.com/schemas/project/v3.json",
  "format": "pivograph-project",
  "schemaVersion": 3,
  "generator": { "name": "PivoGraph", "version": "1.7.0", "url": "https://pivograph.com" },
  "createdAt": "2026-09-06T10:00:00.000Z",
  "modifiedAt": "2026-09-06T12:00:00.000Z",
  "meta": { "title": "Dose response" },
  "usermeta": {},
  "view": { "activeTableId": "dose", "workspaceView": "graph" },
  "tables": []
}
  • format (required): always "pivograph-project". Together with schemaVersion this is how a reader recognises the file; $schema is informational.
  • schemaVersion (required): integer. See Versioning.
  • tables (required): array of tables in sidebar order (maximum 200). A minimal valid document is exactly format + schemaVersion + tables.
  • generator (optional): the program that wrote the file (name, version, optional url). Purely informational; PivoGraph always writes it.
  • createdAt / modifiedAt (optional): ISO 8601 timestamps with timezone. PivoGraph keeps createdAt from the first save and rewrites modifiedAt.
  • meta (optional): title (required inside meta, may be empty), optional description, authors[], license, source, keywords[]. license and source are meant for example / template projects.
  • usermeta (optional): a JSON object for tool-specific data. Its JSON value is preserved across save and reopen, never interpreted, and limited to 64 KB when serialised; whitespace and object formatting are not preserved.
  • view (optional): which table is active (activeTableId, must be an explicit table id) and whether the editor opens in "table" or "graph" view. This is the only presentation state in the file.

3. Tables

Each table is one data table of one of the eight families: xy, column, grouped, nested, contingency, survival, partsOfWhole, multipleVariables.

{
  "id": "dose",
  "name": "Dose response",
  "format": "xy",
  "replicates": 3,
  "x": [-9, -8, -7],
  "rowLabels": ["low", "mid", "high"],
  "dataSets": [
    { "name": "Control", "values": [[1.1, 1.2, null], [2.0, 2.1, 1.9], [5.0, 4.8, 5.1]] },
    { "name": "Treated", "values": [[0.9, 1.0, 1.1], [1.5, 1.6, 1.4], [2.9, 3.1, 3.0]] }
  ],
  "graph": { "variantId": "xy-points-error" },
  "analyses": [],
  "annotations": [],
  "titles": {},
  "usermeta": {}
}
  • format (required): the family.
  • dataSets (required unless derivedFrom is present; at least one): one entry per data set, corresponding to the table's data columns. values[row][replicate] holds finite numbers, null for missing, or text. Rows may be shorter than replicates (missing cells are filled with null). Data set names must be unique within a table.
    • Text is accepted only in multipleVariables tables (text categories and labels). In every other family a text cell is an error, except that numeric-looking strings such as "1.5" are read as numbers.
    • name is the stable key that styles, analyses and variableTypes refer to. title (optional) is the label shown on the graph and in panels; it may repeat across data sets. When omitted the label equals name; null means the data set has no label (the legend shows a placeholder and category axes stay blank). Data set names and titles are plain strings and are always drawn literally.
    • subColumnTitles (optional, nested tables only): one label per sub-column of this data set (null = none), aligned with replicates; these appear on the category axis.
  • replicateTitles (optional, tables whose sub-columns are shared, e.g. xy / grouped): one label per sub-column (null = none), aligned with replicates. Spreadsheet headers only; not drawn on the graph.
  • id (optional): any non-empty string (≤ 128 characters), unique among tables; generated when omitted. Tables referenced by derivedFrom or view.activeTableId need an explicit id. Human-readable slugs are fine.
  • name (optional): defaults to Table N.
  • replicates (optional): number of sub-columns per data set. Fixed at 1 for column, contingency, survival, partsOfWhole and multipleVariables; ≥ 2 for nested; free for xy and grouped. When omitted it is inferred from the widest row (never below the family minimum).
  • x: only for xy and survival — the shared X column, one entry per row, aligned with values. In survival tables X is time and each cell is the event code of one subject of that data set (1 = event, 0 = censored). Cells with any other value, and cells in rows whose time is missing or 0 or less, are ignored.
  • rowLabels: optional row titles (categories for grouped / contingency / partsOfWhole, sample names for heat maps).
  • variableTypes: multipleVariables only — explicit overrides of the inferred variable type (continuous, categorical, label) keyed by variable (data set) name.
  • columnWidths: optional spreadsheet column widths; presentation only.
  • usermeta: optional tool-specific data for this table; same rules as the document-level slot.
  • derivedFrom: marks a results table produced by an analysis on another table: { "sourceTableId", "analysisId" }. Such tables carry no dataSets, x, rowLabels or variableTypes; their content is recomputed when the file opens. The referenced analysis must exist on the source table (with an explicit id) and must be one that produces a results table (frequencyDistribution, correlationMatrix).
  • Row count is the longest of x, rowLabels and any values; trailing empty rows are not stored.

4. Graph

graph is the graph document of the table (one graph per table). Only variantId is required; every other field, and every nested object inside them, may be given partially.

  • variantId: a graph variant from the PivoGraph registry, for example bar, column-box-whiskers, xy-points-line-error, grouped-heat-map, survival-staircase-ticks, parts-donut, mv-volcano. The full list is the GraphVariantId definition in the JSON Schema. The variant must belong to the table's family and be available for the table's replicate count (for example xy-points-error needs replicates > 1).
  • plot: statistical / topology options of the family ({ "family": "...", "options": {...} }); family must equal the table format. Options are merged over the defaults of the variant; a mode that the variant does not offer is replaced by the variant default with a warning. For multipleVariables tables plot is required because it carries the variable mapping: plot.options.kind must match the variant (bubble → mv-bubble, scatter → mv-scatter, volcano → mv-volcano, forest → mv-forest) and plot.options.mapping gives data set indexes per role (x required; y, size, color, shape, label, lower, upper as the plot kind requires; omitted roles are unmapped).
  • activePresetId: built-in theme preset (modern, classic, floral, starry). applyPresetNonColor: whether the preset's non-colour constants are applied.
  • styleOverrides: user overrides on top of the theme, in three layers — global, perDataSet (keyed by data set name, or a semantic key such as volcano:up) and perPoint (data set name → point index). Each layer is a partial patch per element kind (bar, symbol, errorBar, line, …). Colours are either "theme" (follow the preset) or { "hex": "#rrggbb", "alpha": 0.8 }; hex colours have 3 or 6 digits and transparency always goes in alpha. Two fields worth knowing: bar.explodeOffsetPt (points, default 2) is the distance an exploded pie / donut slice moves away from the centre (pie slices reuse the bar style; perPoint is keyed by the slice's row index), and curve.drawOrder (underneath default / onTop) places a fitted curve and its confidence band behind or in front of the data points. summarySymbol carries the same fields as symbol and styles the optional mean / median symbol of scatter variants independently of the individual points. barsSpikesDroplines styles the spikes of xy-spikes / xy-spikes-error: color, widthPt (points, default 4), borderColor, borderThickness (points), fillPattern, patternColor. areaFill styles the filled area of xy-area-fill: fillColor, position (belowCurve — from the curve down to the lower end of the Y axis, aboveCurve — up to the upper end, toXAxis — to Y = 0), fillPattern, patternColor. censorTick styles the censor ticks of survival-staircase-ticks: color, thickness and length (points), direction (above / below / both).
  • graphSettingsOverride: partial graph settings (bar width, gaps, baseline, line of identity, …). baseline / baselineY apply to bar graphs and to XY spike graphs: automatic starts bars / spikes at the axis minimum (or zero when the data straddle it), manual starts them at baselineY and draws a baseline across the plot, hidden starts them at baselineY without the line.
  • axesOverrides: six partial buckets (frameAndOrigin, xCategorical, xContinuous, leftY, rightY, titlesAndFonts); "auto" means automatic where a number is otherwise expected. Nested objects (grid lines, tick appearance, number format, title font) may be partial. Title text is not part of the axes — see titles. Additional ticks / reference lines (additionalTicks) may carry lineColor and lineThickness; when omitted the line follows the axis colour and the major grid width. minorTicks (xContinuous, leftY, rightY) is the number of segments per major interval (0 = none) or "auto"; the part of a minor tick outside the axis line is half that of a major tick, and minor ticks have no labels. xContinuous.autoRangePadding (boolean, default false) pads the automatic X range by 0.475 major intervals at each end (at most five major intervals) and only affects ends that are still automatic.
    • titlesAndFonts.graphTitle / xAxisTitle / yAxisTitle: font (family, size, bold, italic — the base style of the title, see Rich text values) and rotation in degrees, clockwise positive, normally a multiple of 45 (defaults: 0 for the graph and X titles, −90 for the Y title).
    • xCategorical.labelBold (boolean, default true): whether the category labels of column-type graphs are bold.
  • plotGeometry: plot-area size in points (null or omitted = automatic).
  • dataSetVisibility: data set index (as a string key) → false to hide; missing = visible.
  • elementActivation: optional-element switches in three scopes (global, perDataSet by index, graph). Which elements a variant offers is fixed by the registry (for example errorBar on grouped-stacked-bars — on by default — summarySymbol and summaryLine on the scatter variants — off / on by default — symbol and line on xy-spikes / xy-spikes-error — off by default — bar on column-before-after and symbol on column-floating-bars — off by default — censorTick on survival-staircase-ticks — on by default — and partsExplode on parts-pie / parts-donut — off by default). For pie and donut graphs perDataSet is keyed by the slice's row index, so a single slice can be exploded. Switches for elements the variant does not offer are accepted and have no effect.
  • legend: legend configuration including itemOverrides keyed by data set index or semantic id. bold / italic are the base style of the whole legend; an item's label is a rich text value (see below) that replaces the data set label.
  • partsOfWholeBorders: outer / inner border of pie, donut and slice graphs (the outer border also draws the dots of the dot plot). Legend keys use the outer border. The size of the graphic is plot.options.graphicSizePt (see What changed since version 2); plot.options.dataSetIndex chooses the one data set (column) that is drawn.

Keys that refer to data sets by name and keys that refer to them by index coexist in version 3 (they mirror the application's model). Keys that match no data set are ignored with a warning. A future schema version may unify them; documents will be migrated automatically.

A graph that uses several of the version 2 fields — a Column scatter plot with a white mean symbol drawn over semi-transparent points, the summary line switched off, the first group flush with the left edge of the plot, regular-weight category labels and a Y title rotated to read upwards:

{
  "variantId": "column-scatter-only",
  "plot": { "family": "column", "options": { "mode": "meanWithSd" } },
  "styleOverrides": {
    "global": {
      "symbol": { "color": { "hex": "#1f77b4", "alpha": 0.35 } },
      "summarySymbol": { "size": 7, "borderColor": { "hex": "#000000" } },
      "errorBar": { "color": { "hex": "#000000" } }
    }
  },
  "elementActivation": { "global": { "summarySymbol": true, "summaryLine": false } },
  "graphSettingsOverride": { "firstGapRatio": 0, "lastGapRatio": "auto" },
  "axesOverrides": {
    "xCategorical": { "labelBold": false },
    "titlesAndFonts": { "yAxisTitle": { "rotation": -90 } }
  }
}

5. Analyses

analyses is a list of analysis instances attached to the table: { "id", "method", "params" }. id is optional (generated when omitted; required when a results table refers to it) and params may be partial (method defaults fill the rest). Results are not stored; they are recomputed from the data and the parameters.

  • ttest (column): { "groupA": "name" | null, "groupB": "name" | null, "design": "unpaired" | "paired", "variance": "welch" | "equal", "distribution": "normal" | "nonparametric" }. Group names refer to data set names; null uses the first two populated data sets. "distribution": "nonparametric" (default "normal") runs the Mann-Whitney test (unpaired) or the Wilcoxon matched-pairs signed rank test (paired) instead of a t-test; variance is then ignored.
  • oneWayAnova (column, three or more data sets): { "groups": ["name", ...] | null, "design": "independent" | "matched", "distribution": "normal" | "nonparametric", "equalVariance": boolean, "comparisons": "none" | "allPairs" | "vsControl" | "selectedPairs", "controlGroup": "name" | null, "selectedPairs": [["name", "name"], ...], "correction": "tukey" | "dunnett" | "bonferroni" | "sidak" | "holmSidak" | "gamesHowell" | "dunnettT3" | "dunn" | "fdrTwoStage" | "fdrBh" | "none" | null, "confidenceLevel": 90 | 95 | 99, "bracketsIncludeNs": boolean }. Defaults: all populated data sets (groups: null), independent, normal, equal SDs, all pairs, recommended correction, 95%, brackets for significant comparisons only. The design, distribution and equalVariance select the test: ordinary ANOVA, Welch and Brown-Forsythe ANOVA (equalVariance: false), repeated-measures ANOVA with the Geisser-Greenhouse correction (matched; each row is one subject, first sub-column of each data set, incomplete rows are left out), Kruskal-Wallis or Friedman (nonparametric). correction: null uses the recommended method for that test and comparison (Tukey for all pairs, Dunnett against a control, Šídák for selected pairs; Dunnett T3 after Welch; Dunn after a nonparametric test); a correction that does not fit the current test also falls back to the recommended one. controlGroup: null uses the first data set. Significance brackets can be added to the graph; they are ordinary bracket annotations.
  • descriptiveStatistics (column, numbers only): { "confidenceLevel": 90 | 95 | 99, "percentile": number | null }. N, mean, SD, SEM, confidence interval of the mean, quartiles ((n + 1)p rule), minimum, maximum and further statistics per data set; percentile (between 0 and 100, exclusive) adds one more percentile. Nothing is drawn on the graph.
  • rowTTests (grouped, needs at least two sub-columns): the same parameters as ttest; groupA / groupB name the two data sets that are compared in every row. Each row is tested on its own, and P values are not adjusted for multiple comparisons. With "design": "paired" the values in the same sub-column of a row belong to one subject; sub-columns without a value on both sides are skipped. Significance brackets for the rows can be added to the graph (see the rowIndex anchor in Annotations and titles). Example: { "method": "rowTTests", "params": { "groupA": "Before", "groupB": "After", "design": "paired" } }.
  • linearRegression (xy): { "confidenceLevel": 90 | 95 | 99 }.
  • nonlinearRegression (xy, four-parameter logistic): { "model": "logX" | "linearX" }.
  • areaUnderCurve (xy, numbers only): { "baselineY": number } (default 0). Each data set is integrated separately with the trapezoid rule after sorting by X and averaging the replicates at each X; the results list the area above and below baselineY, the net area (above − below) and, when every X has at least two replicates, the standard error of the net area. Nothing is drawn on the graph.
  • xyCorrelation (xy, numbers only): { "method": "pearson" | "spearman" }. Correlation between X and the Y of each data set (mean of the replicates in each row; rows with a blank X or no Y are skipped), with R², a two-tailed P value (t approximation, also for Spearman) and a Fisher-z 95% confidence interval for Pearson. Nothing is drawn on the graph.
  • frequencyDistribution (column): { "binWidth": number | null, "binStart": number | null, "output": "count" | "fraction" | "percent", "cumulative": boolean }. Produces a results table (grouped).
  • correlationMatrix (multipleVariables): { "method": "pearson" | "spearman" }. Produces a results table (grouped) rendered as a heat map.
  • survivalComparison (survival): { "showPValue": boolean, "showHazardRatio": boolean, "showMedians": boolean }. Log-rank (Mantel-Cox) and Gehan-Breslow-Wilcoxon tests across all curves, the log-rank test for trend with three or more curves (data sets scored 1, 2, 3, … in table order), and median survival per data set with its 95% confidence interval (log-log limits); with two curves also the hazard ratio (Mantel-Haenszel and log-rank) and the ratio of median survival times with their 95% confidence intervals. The switches only choose the lines of the text that can be placed on the graph.

A method must be available for the table's family. Explicit analysis ids must be unique within their table.

6. Rich text values

Text that is drawn on the graph is a rich text value — one of two forms:

"Cell number"
{
  "runs": [
    { "text": "Cell number (×10" },
    { "text": "6", "baseline": "super" },
    { "text": ")" },
    { "text": "\nper well", "italic": true }
  ]
}
  • A plain string is drawn literally — there is no markup. \n starts a new line.
  • The runs form concatenates its runs in order. Each run has text (non-empty; may contain \n) and optional bold, italic (booleans) and baseline ("super" or "sub"). Omitting bold / italic means inherit the base style of the object (the title font, the free text's or shape's bold / italic, or the legend's bold / italic); giving false explicitly overrides an inherited bold / italic. Omitting baseline means normal position; super- and subscripts are drawn at reduced size and shifted. Formatting cannot nest beyond these three properties.
  • Limits: at most 500 runs per value; the concatenated text obeys the general string limit. Adjacent runs with identical formatting and empty runs are accepted but PivoGraph normalises them when saving, and writes the plain-string form whenever no run carries formatting.

Rich text values appear in: titles.graph|x|y.text, annotations[].text (all three annotation types), and graph.legend.itemOverrides.<key>.label. Data set names and titles, row labels and axis numbers are plain strings and are never parsed.

7. Annotations and titles

annotations holds drawing objects placed on the graph, discriminated by type (id optional, generated when omitted):

  • bracket: significance bracket. left / right anchors are { "kind": "group", "groupIndex": n } (snapped to the n-th plotted data set), { "kind": "group", "groupIndex": n, "rowIndex": r } on grouped graphs (the n-th plotted data set in row r; available when the data sets of a row sit side by side — interleaved or separated layouts, or staggered series), or { "kind": "free", "x": pt }; y is the bar position, leftFootLen / rightFootLen the foot lengths, text the label ("*", "ns", … — a rich text value). Line formatting: lineWidth, dashStyle, lineColor. Text formatting: fontSize, fontFamily, textColor, base bold / italic, textRotation in degrees (multiples of 45, about the label centre), textLineSpacing in points and textGap — distance between label and bar in points, default 3. Labels that consist only of asterisks are drawn enlarged and aligned to the text baseline.
  • text: free text at x, y (points): text (rich text value), fontSize, fontFamily, color, base bold / italic, rotation in degrees (multiples of 45, about the centre of the text block), and lineSpacing — extra space in points between lines when the text has more than one line (default 2).
  • shape: lines, arrows, rectangles, ellipses, brackets, … (shape enum), with two or three control points and optional bound text (text rich text value, textRotation in degrees, textLineSpacing in points, textGap — distance between text and line in points, default 3 — and base bold / italic). A two-point line with text is always drawn horizontal or vertical (the end point is projected onto the nearer axis); a two-point line without text keeps the exact angle its end points describe. Closed shapes (circle, ellipse, square, rectangle, rounded rectangle) may carry fillColor; omitted means no fill.

A significance bracket between the two data sets of the first row of a grouped graph, with a dashed line and an italic two-line label:

{
  "type": "bracket",
  "left": { "kind": "group", "groupIndex": 0, "rowIndex": 0 },
  "right": { "kind": "group", "groupIndex": 1, "rowIndex": 0 },
  "y": 24,
  "text": { "runs": [{ "text": "P = 0.012\n", "italic": true }, { "text": "n = 8" }] },
  "dashStyle": "dashed",
  "textGap": 3,
  "textLineSpacing": 1
}

titles overrides the graph, X and Y titles (graph, x, y): text (rich text value), drag offset (dx, dy), fontSize, fontFamily, color and lineSpacing. This is the only place where title text lives; the title's base bold / italic and its rotation live in axesOverrides.titlesAndFonts.

8. Versioning and compatibility

  • schemaVersion is an integer. Every change to the document structure or to rendering-relevant defaults increases it by one and ships with a migration step. Migrations run once, at open time, before validation.
  • A reader opens every version from 1 up to its own. Older files are migrated in memory; the original file is never rewritten.
  • A file with a newer schemaVersion than the reader supports is refused with an explicit message (in the web application: reload the page to get the latest version). Nothing is guessed or silently dropped.
  • Registry identifiers (variants, presets, methods, symbols, colormaps) are part of the format contract. Deprecated identifiers are handled by migrations, not by aliases.
  • Every version has exactly one JSON Schema (/schemas/project/v<N>.json, immutable), one specification page (/developers/project-format/v<N>/, plus the Markdown copy v<N>.md) and one set of examples, listed in /schemas/project/v<N>/examples/index.json. /schemas/project/index.json lists all versions and which one is current. There is deliberately no "latest" schema URL: a file's $schema must stay stable.
  • Fixtures of every published version are kept in the repository and tested on every build.

9. Limits

Files larger than 50 MB, more than 200 tables, more than 2,000,000 cells in total, more than 200,000 rows or 500 data sets per table, more than 2,000 annotations or 200 analyses per table, more than 500 runs in one rich text value, non-finite numbers, over-long strings, or record keys named __proto__, constructor or prototype are rejected. Exact values are in the JSON Schema.

10. Validation

Opening a file goes through: size limit → strict UTF-8 → JSON parse → envelope (format, schemaVersion) → version gate → forbidden-key scan → migration → strict structural validation (JSON Schema equivalent) → semantic validation → import. Semantic errors include: duplicate ids, dangling derivedFrom or view.activeTableId references, replicate counts invalid for the family, variant / plot family not matching the table, variant unavailable for the replicate count, incomplete variable mapping, analysis methods unavailable for the family, text cells outside multipleVariables, rows wider than replicates, total cell count. Semantic warnings include: style / visibility / legend keys that match no data set, and plot modes the variant does not offer.

All problems are reported together with JSON paths (for example tables[0].graph.variantId). If any error occurs, the current project in the editor is left untouched.

11. Minimal example

{
  "format": "pivograph-project",
  "schemaVersion": 3,
  "tables": [
    {
      "name": "Cell viability",
      "format": "column",
      "dataSets": [
        { "name": "Vehicle", "values": [[98], [102], [95]] },
        { "name": "Compound", "values": [[71], [68], [74]] }
      ],
      "graph": { "variantId": "scatter-bar", "plot": { "family": "column", "options": { "mode": "meanWithSd" } } }
    }
  ]
}

More examples, one per table family, are listed in https://pivograph.com/schemas/project/v3/examples/index.json (each entry gives the file URL and the table families, graph variants and analyses it uses; the Developers page links them individually); representative.pivograph is a complete document written by PivoGraph itself.

12. For AI assistants

PivoGraph's Help menu offers Copy Prompt for AI Assistants…: a prompt generated from the running version that lists every family, variant id, analysis method, the main format rules, and a minimal example. Give that prompt to an AI assistant, describe the data and intended figure, save the returned JSON as .pivograph, and open it in PivoGraph. Treat generated projects as drafts: verify the source values, analysis choices, and graph before using the result. Agents can also use this page, its Markdown copy, llms.txt, the JSON Schema, and the examples.

Changelog

  • v3 (PivoGraph 1.7) — barsSpikesDroplines.widthPt replaces widthRatio; beginAtY removed in favour of graphSettingsOverride.baseline / baselineY; areaFill.position gains toXAxis; optional symbol / line on spike variants; optional mean bar on column-before-after and symbol on column-floating-bars, and the line switch on column-mean-error is now drawn; survival error bars default to both directions with narrow caps; censorTick element on survival-staircase-ticks, whose errorBars now default to "none"; minorTicks on continuous X and drawn on Y, xContinuous.autoRangePadding, survival axes from time 0 with Y 0 to 110 and automatic minor ticks; ttest limited to column tables; new XY analyses areaUnderCurve and xyCorrelation; new column analyses oneWayAnova and descriptiveStatistics; ttest.distribution for the Mann-Whitney / Wilcoxon option; survival tables read only the codes 1 / 0; survivalComparison adds the test for trend, median confidence intervals and the ratio of medians; parts-of-whole graphicSizePt is the drawn size and the plot area grows to fit; parts-of-whole legend keys use the outer border. Version 2 files are migrated automatically.
  • v2 (PivoGraph 1.6) — rich text values (plain string or runs with bold / italic / super- / subscript; no markup), title rotation in degrees, lineSpacing / textLineSpacing, xCategorical.labelBold, shape textGap default 3, partsExplode element + bar.explodeOffsetPt, curve.drawOrder, optional errorBar on stacked bars (on by default), line / text formatting fields on bracket annotations, summarySymbol element + style on scatter variants (with summaryLine now optional), heat map colormaps viridis / magma / cividis / purpleOrange, colour bar length default 1, edge spacing firstGapRatio / lastGapRatio as clear distance with "auto" default, grouped-rm-across-row staggered by default with connecting lines that follow the line style, rowIndex on bracket anchors, rowTTests analysis. Version 1 files are migrated automatically. Specification v2 stays available.
  • v1 — first public version (PivoGraph 1.3). Specification v1 stays available.