Where does the target value travel between the data source and the plot?
The data path has four stages. The binding or script fills props.dataSources with one object per time period. Each entry in props.series points at that source through data.source, data.x (timePeriod), and data.y (a key such as Machine 1 Production). The X axis then turns every distinct timePeriod into a category cell. The render type decides where inside that cell each value is drawn.
The misalignment appears at the last stage. On a category axis, column series are clustered. The chart splits each category cell into one slot per unstacked column series, in the order the series appear in the array. Line and step-line series do not use those slots. Each line point sits at the category location, which is the center of the whole cell, and a step line draws its horizontal segment across the cell. A target plotted as line or stepLine for Machine 1 therefore lands in the middle of the period, not over Machine 1's column. You can hand-tune an offset, but the slot positions move whenever the machine count changes. The machine count changes with the selected time frame and the returned data, so any manual offset breaks as soon as a user picks a different range.
There is also a presentation problem. A line series joins adjacent periods, which tells the reader there is a trend. A per-period target is a reference value for a single bar. It is not a trend, so a marker or bar segment tied to each production column communicates it more accurately.
Check 1: Is this a Perspective XY Chart or a Reporting chart?
Confirm the module before changing anything. The Perspective module and the Reporting module are unrelated. They use different chart components with different configuration models.
| Reading | Meaning | Next |
|---|---|---|
Component exposes props.dataSources and props.series with render, column, line, stepLine blocks |
Perspective XY Chart, which is the scope of this procedure | Check 2 |
| Chart lives inside a report in the Report Designer | Reporting module chart with a separate property model | Stop here, because the series JSON below does not apply |
Check 2: Does every series find its X and Y on the same row?
Open props.dataSources in the Property Editor and inspect one entry. The working shape is wide format, with one object per period and one key per machine per measure:
{
"example": [
{
"timePeriod": "2025-01",
"Machine 1 Production": 120,
"Machine 1 Target": 160,
"Machine 2 Production": 80,
"Machine 2 Target": 100
}
]
}
| Reading | Meaning | Next |
|---|---|---|
| Wide rows as above | Every series can reference timePeriod plus its own key |
Check 3 |
| Long rows (one row per machine per period) | Series cannot share a category cleanly, and columns scatter or duplicate categories | Pivot to wide format in the query or a script transform, then Check 3 |
| Keys differ between periods | A machine missing from one period leaves an empty slot or breaks the stack | Emit every machine key in every row, with 0 when there is no data |
Check 3: How is the target series rendered?
Read the render value on every target series. Each option places the target in a different position relative to the production slot:
render / config |
Where the target lands | Symptom | Verdict |
|---|---|---|---|
line |
Category center, joined to the next period | Points float between columns and the line implies a trend | Misaligned by design |
stepLine |
Horizontal segment spanning the whole cell | Step cannot be centered over one machine's column | Misaligned by design |
line with bullets only (stroke hidden) |
Category center | Dot above the group rather than above the machine | Works only with one machine per category |
column, stacked: false
|
Its own slot beside production | Aligned, but column count doubles and the comparison is side by side | Usable, but visually noisy |
column, stacked: true, value = target minus production |
Same slot as production, sitting on top of it | Top of the stack equals the target | Resolving branch |
The stacked branch works because a stacked column does not claim a new slot. It reuses the slot of the series immediately before it in the array and starts at that series' value. Plot production first and the shortfall (target - production) on top of it. The combined column height equals the target, and the boundary between the two colors marks actual production. Alignment follows the column engine automatically for any number of machines or periods.
If any target is still on line or stepLine, go to Check 4 before rebuilding.
Check 4: Can production exceed target in any period?
The shortfall stack only renders cleanly when the shortfall is zero or positive. When production beats target, target - production goes negative. The stacked segment then draws downward from the top of the production column, which overlaps the production bar and misrepresents both values.
| Reading | Data model | Series per machine |
|---|---|---|
| Production never exceeds target (for example, target is a ceiling) | shortfall = target - production |
2: production, shortfall (stacked) |
| Production can exceed target |
base = min(production, target), shortfall = max(target - production, 0), overage = max(production - target, 0)
|
3: base, shortfall (stacked), overage (stacked) |
In the three-segment model, the top of base + shortfall always equals the target. When the machine is under target, base is the production value and the shortfall fills the gap. When the machine is over target, base equals the target, shortfall is 0, and the overage segment rises above it in a third color. Total bar height is always the larger of production and target, so neither value is hidden.
How do I build the stacked-shortfall chart?
-
Transform the data. Add a script transform to the binding that feeds
props.dataSources.example. The version below handles both branches of Check 4. It keeps the original target and production keys for tooltips and adds the derived keys:
If the upstream binding returns a dataset instead of a list of objects, convert it withdef transform(self, value, quality, timestamp): rows = [] for rec in value: out = {'timePeriod': rec['timePeriod']} for key in rec.keys(): if not key.endswith(' Production'): continue machine = key[:-len(' Production')] prod = rec[key] or 0 tgt = rec.get(machine + ' Target', 0) or 0 out[machine + ' Production'] = prod out[machine + ' Target'] = tgt out[machine + ' Base'] = min(prod, tgt) out[machine + ' Shortfall'] = max(tgt - prod, 0) out[machine + ' Overage'] = max(prod - tgt, 0) rows.append(out) return rowssystem.dataset.toPyDataSetand read the columns by name before running this logic. In the two-segment model, plotProductionas the base and skipOverage. -
Order the series array per machine. Stacking is positional, so the order in
props.seriesdecides which slot each segment joins. For each machine, emit: base or production (column.appearance.stacked: false), then shortfall (stacked: true), then overage (stacked: true) if used. Every second series in the two-segment model is stacked. Every second and third series in the three-segment model are stacked. -
Point each series at its key. Keep
render: "column"on all of them and change onlyname,data.y, the stack flag, and the fill:
Use one fill color for production and a contrasting color (the orange originally intended for the target) for shortfall on every machine. That keeps the legend readable when the machine count grows.{ "name": "Machine 1 Shortfall", "data": { "source": "example", "x": "timePeriod", "y": "Machine 1 Shortfall" }, "xAxis": "x", "yAxis": "y", "render": "column", "column": { "appearance": { "fill": { "color": "#FFA500", "opacity": 1 }, "stroke": { "color": "", "opacity": 1, "width": 0 }, "stacked": true } } } -
Add a transparent spacer series. With many clustered slots, the groups for adjacent periods run together. Append one extra copy of a production series at the end of the array with
stacked: false,fill.opacity: 0, stroke width 0,hiddenInLegend: true, and its tooltip disabled. It claims a blank slot in every category and opens a visible gap between periods. -
Fix the tooltip semantics. On a stacked series,
{valueY}reports the segment value (the shortfall), not the cumulative height. Word the tooltip text as "Shortfall" so operators do not read it as the target. - Regenerate the series on time-frame change. Your series builder already runs per selection. Update it so it emits 2 series per machine (or 3 with overage) plus 1 spacer. Derive the machine list from the transformed row keys so that a machine absent from the query does not leave a stacked series pointing at a missing key.
How do I verify the targets are aligned?
- Load a period with known values. From the sample row, Machine 1 should show a 120 production segment with a 40 shortfall on top, stacking to 160. Machine 2 should show 80 plus 20, stacking to 100.
- Hover each segment and confirm the tooltip value matches the transformed data in the Property Editor under
props.dataSources.example. - Switch the selector through weekly, monthly, and yearly. Confirm the stack tops still match the targets when the category count changes and the series array is rebuilt.
- Force a test row where production exceeds target. Confirm that no segment draws downward, the shortfall is 0, and, in the three-segment model, the overage segment sits above a base equal to the target.
- Count the column slots in any single category. The count must equal the number of machines plus one blank spacer slot, and each shortfall segment must sit directly on its own machine's production segment.
FAQ
How do I align a target line over a specific column in the Ignition Perspective XY Chart?
You cannot do this reliably with a line or step line. Those render types plot at the category center, while clustered columns occupy offset slots. Replace the target with a stacked column series whose value is target minus production, placed directly after that machine's production series.
How do I stack one column series on another in a Perspective XY Chart?
Set column.appearance.stacked to true on the upper series and place it immediately after the base series in props.series. The stacked series reuses the previous series' slot and starts at its value.
How do I add space between category groups in a Perspective XY Chart?
Append one extra unstacked copy of a column series with fill opacity 0, stroke width 0, legend hidden, and tooltip disabled. It occupies an empty slot in every category and opens a gap between periods.
How do I handle periods where production exceeds target in a stacked target chart?
A raw target-minus-production value goes negative and draws downward. Split the bar into min(production, target), max(target - production, 0), and max(production - target, 0), all stacked, so the top of the first two segments always marks the target.