Here is what you see. You bound two custom properties, props.custom.processTempData and props.custom.outputTempData, into props.series[0].data and props.series[1].data. The bindings evaluate without a quality error. The chart is blank or draws one trace. The component also throws a schema error saying it expected an object and got an array.
Start here: the data is in the wrong property. The query and the custom properties are fine. The one-source version of this chart already works, so the problem is how the chart is wired.
Skip the Fixes That Waste Time
These are the usual first attempts. Each one fails for a specific reason.
-
Binding the dataset straight into
series[n].data. That property is a small mapping object that tells the series which data source to read and which fields are X and Y. An array there overwrites the mapping. The series loses its pointer to the data, and the schema check rejects the type. - Wrapping the array in an object to clear the error. The red text goes away, but the chart still draws nothing. The keys you invented are not the keys the series reads.
- Re-running the query or rebuilding the custom properties. That is not the fault. The data arrives correctly, and it lands in a property the renderer does not treat as data.
- Plotting two years on a date axis with raw timestamps. Both traces render, but side by side, with one year on the left and the next year on the right. They do not overlay.
- Stripping the year from the timestamp and hoping the points line up. Both series share one axis, but weekly buckets from different years rarely fall on the same calendar date. You get interleaved categories with gaps, not an overlay.
Learn How the XY Chart Separates Data from Rendering
The XY Chart splits the job across three property groups:
-
props.dataSourcesis an object. Each key is a named dataset, either a dataset or a list of row objects. Bind your query or transform output here. -
props.seriesis an array of render definitions. Each series names a data source key, the field used for X, the field used for Y, the axis it plots against, the render type, and its appearance. -
props.xAxesandprops.yAxesdefine axis type (date, category, value) and names that series reference.
Data flows one way: data source key, then series[n].data.source, then the x and y field names inside each row. One source can feed several series. Several sources can each feed their own series. In every case, the bindings go on dataSources, never on series.
Match the Symptom to the Cause
| What the chart shows | Cause | Fix |
|---|---|---|
| Schema error: object expected, array found | Dataset bound into series[n].data
|
Move the binding to a key under props.dataSources
|
| Blank chart, no error |
data.source does not match a data source key, or the x/y field names do not match row keys (case-sensitive) |
Copy the key and field names exactly |
| One trace draws, the second does not | Second series points at the first source, or its y field does not exist in that source |
Point series[1].data.source at the second key |
| Two years render side by side | Date axis with full timestamps; the X domains do not overlap | Normalize X to a year-independent key, or give each series its own X axis |
| Overlay has gaps and zig-zags | Year-stripped MM-dd keys differ between years, so the category axis takes the union |
Key on week number, not calendar date |
X labels like 02-26 do not appear or collapse |
String X value on a date axis | Switch that X axis to category |
| Weeks out of order | Unpadded week strings sort as text (W10 before W2) |
Zero-pad (W02) or use an integer on a value axis |
Decide Between One Merged Source and Two Separate Sources
Make this call before you touch the property tree.
-
Merge into one source when both measurements share the same X domain row for row. Build one row per
t_stampcarrying both values, for example[{"t_stamp": "2025-02-26 00:00:00", "output_temp": 123, "process_temp": 456}, ...]. Then create two series on the same source with differentyfields. This is the cleanest option: one binding, one query, guaranteed X alignment. -
Keep two sources when X values do not coincide, such as different sample rates, different tables, or different years. Create two keys under
dataSourcesand two series, each pointing at its own key. -
Add a second X axis when the domains differ and you want each series on its own time scale. Define a second entry in
xAxesand assign the second series to it by name. - Normalize X when you want different years overlaid on one scale. Neither raw timestamps nor a second date axis gives a true overlay. Convert X to a key that means the same thing every year.
Wire Two Data Sources Into the Chart
- Delete the bindings on
props.series[0].dataandprops.series[1].data. Restore each to an object withsource,x, andykeys. - Under
props.dataSources, add two keys, for exampleprocessTempandoutputTemp. Bind each one to its query, either directly or throughprops.custom. - Set
series[0].data.sourcetoprocessTempandseries[1].data.sourcetooutputTemp. - Set
xon both series to the same field name, such ast_stamp. Setyto the value field in each source. - Check the X axis type. Use a date axis for real
Datevalues. Use a category axis for string keys likeMM-ddor week labels. - Point both series at the same X axis name for an overlay, or at different X axes for separate time scales.
Resulting structure:
dataSources:
processTemp: [ {"t_stamp": ..., "process_temp": ...}, ... ]
outputTemp: [ {"t_stamp": ..., "output_temp": ...}, ... ]
series[0].data: {"source": "processTemp", "x": "t_stamp", "y": "process_temp"}
series[1].data: {"source": "outputTemp", "x": "t_stamp", "y": "output_temp"}
Overlay Two Years on One X Axis
Comparing weekly values year over year is the case that trips people up. A working approach is a transform that filters the query by a code from a sibling text field, drops the year from WEEK_START, formats it as MM-dd, and returns a list of dicts. Bind one of these to each data source key, one per year. Both series then share a category X axis, and they overlay.
The weakness is the key itself. A weekly bucket starts on a fixed weekday, and that weekday falls on a different calendar date each year. A week starting on 02-24 one year starts on 02-23 or 02-22 the next. MM-dd keys from two years almost never match, so the category axis builds the union of both key sets. The result is two traces that alternate categories and leave gaps. Key on week-of-year instead.
Transform body for a dataSources binding. Here value is the query dataset:
Why each change matters:
-
Week key, not date key.
W09means the same slot every year, so both series land on the same category. - Zero-padded label. The string sort and the category order stay numeric.
-
getWeekYear()instead ofYEAR. A late-December week can belong to week 1 of the following year.YEARwould tag it with the old year and put a stray point at the start of the wrong series. - Explicit week rules. follows the gateway locale unless you set first day and minimal days. If you leave them unset, a locale change shifts every week by one.
-
Imports and
Calendarcreated once. This avoids re-importing and re-allocating on every row.
For a single merged source, pivot instead. Return one row per week with a field per year, for example {"t_stamp": "W09", "y2024": ..., "y2025": ...}. Then point both series at that source with y set to each year field. Use this when the year pair is fixed. Use two sources when the user picks the years at runtime.
Add a Legend Keyed to the Year
- Enable the chart legend under
props.legend. - Set each series name or label text to the year it plots. Legend entries take their text from the series, not from the data source key.
- Bind that text to the same year parameter that drives the data source query. The legend then follows the user's selection instead of showing a stale hard-coded year.
- Set each series stroke and fill color explicitly. Default palette colors are assigned by series index, so reordering series swaps colors between years.
Verify the Chart Before You Hand It Over
- In the property editor, expand each
dataSourceskey. Confirm it holds rows, and that the row keys match thexandynames exactly, including case. - Confirm each
series[n].datais still an object withsource,x,y, and that no binding icon remains on it. - Count X categories. A full year of weekly data gives 52 or 53 labels with no duplicates and no
MM-ddnear-misses. - Hover one week. The tooltip or cursor shows values from both years at the same category.
- Change the
TextField_3value. Both traces and both legend entries refresh. - Check the Designer output console and the gateway logs for transform exceptions. A script error leaves the last good value in place and hides the failure.
FAQ
How do I bind two datasets to one Perspective XY Chart?
Add two keys under props.dataSources and bind each to its query. Then set series[0].data.source and series[1].data.source to those keys, with matching x and y field names. Never bind the data into series[n].data itself.
How do I fix the XY Chart error that says it expected an object instead of an array?
Remove the binding from series[n].data and restore it to an object with source, x, and y. Move the array to a key under props.dataSources. Wrapping the array in an object clears the error but still draws nothing.
How do I overlay two years of weekly data on the same X axis?
Transform each year's timestamps into a year-independent key, such as a zero-padded week number (W01 to W53), and plot both series on one category X axis. Avoid MM-dd keys, because weekly buckets start on different calendar dates each year and will not line up.
When should I stop and escalate to Inductive Automation support?
Escalate if the data source keys hold valid rows, the series mappings match the field names exactly, no transform errors are logged, and the chart still renders blank or throws a schema error. Send support the exported view JSON, your Ignition version, and the exact error text from the Designer console.