What is the working fix?
Keep series[0].column.appearance.deriveFieldsFromData.fill.color. The Gantt pattern depends on it. Point it at a field that you add to each data row with a script transform on the named query binding. The transform reads the integer state, looks it up in a state-to-color dictionary, and writes a color string into a new key such as stateColor. The chart then fills each bar from that key. You do not need a color column in the query itself.
How do you read the symptom on the chart?
Start with the data, not the chart. The signal chain has three stages. The named query returns rows with start, end, lane, and state. The binding and transform shape those rows. The XY Chart renders one bar per row and takes each bar's fill from whatever field deriveFieldsFromData.fill.color names. When a bar shows the wrong color, one of those stages is passing a wrong value. Open the component's dataSources in the property editor and inspect a real row before you touch any styling.
| Signal | Source | Wrong-value symptom |
|---|---|---|
state (integer) |
Named query column | Every bar shows the default color because the key name differs in case or spelling, or the value is null. |
stateColor (string) |
Script transform output | Bars fall back to the series fill because the field is missing from the rows or the transform threw an error. |
deriveFieldsFromData.fill.color |
Series property | All bars share one color. The property holds a color literal or a mistyped field name instead of stateColor. |
| Color map entry | Dictionary in transform or custom property | One state renders grey or default because its key is missing, or because the key is the string "2" while the lookup uses the integer 2. |
| Start/end timestamps | Named query columns | Bars disappear or collapse after the transform runs because the date fields were altered while the rows were rebuilt. |
Why can't you bind the column color directly?
A property binding on the series fill evaluates once for the whole series. A Gantt chart built on the XY Chart plots every lane and every time segment as columns inside a single series. A binding on series[0].column.appearance.fill.color therefore paints every bar the same color. A multi-state indicator works differently because it is one component with one value. Each Gantt bar is a data row, not a component, so no per-bar property exists to bind.
deriveFieldsFromData gives you per-row control. It does not hold a color. It holds the name of a field, and the chart renderer resolves that name against each row as it draws the column. Any color decision has to exist in the data before the chart receives it. Styling does not fix a missing field, so the fix belongs upstream in the binding.
How do you add the state-to-color mapping?
- On the XY Chart, open the binding on
props.dataSources(the source used by the Gantt series). Set the named query binding's return format to JSON so the transform receives a list of row objects. A Dataset also works if you use the conversion branch in the script below. - Add a Script transform to that binding.
- Paste a lookup that appends a color field to every row:
def transform(self, value, quality, timestamp): # Integer state -> color string. Edit to match your equipment states. colorMap = { 0: "#9E9E9E", # off / unknown 1: "#4CAF50", # running 2: "#FFC107", # idle 3: "#F44336" # faulted } default = "#BDBDBD" # Normalize input: Dataset or list of dicts if hasattr(value, "getColumnNames"): cols = list(value.getColumnNames()) rows = [dict(zip(cols, list(r))) for r in system.dataset.toPyDataSet(value)] else: rows = [dict(r) for r in value] for row in rows: s = row.get("state") try: row["stateColor"] = colorMap.get(int(s), default) except (TypeError, ValueError): row["stateColor"] = default return rows - Set
series[0].column.appearance.deriveFieldsFromData.fill.colorto the stringstateColor. Enter the field name only, not a color value. If you want the outline to follow the state as well, set the matching stroke color field in the samederiveFieldsFromDatablock. - If operators or engineers need to edit the palette without opening the script, move
colorMapinto a view custom property and read it withself.view.custom. Custom property keys are strings, so look upstr(int(s))in that case. A change to the custom property does not re-run the query binding. If palette edits must apply live, drive the whole thing from an expression structure binding that includes both inputs.
You can also map colors in SQL with a CASE on state or a join to a state-color table. That approach fits when several views share one palette and a database owns it. The transform approach keeps presentation out of the query and works without schema changes.
How do you verify the colors are right?
- Preview the view in the Designer. Expand
props.dataSourcesand confirm that every row now carriesstateColorwith a color string, and thatstatesurvived unchanged. - Check that the start and end fields still hold timestamps and the time axis spans the same range as before the transform. If bars vanished, the row rebuild changed the date types. Switch the binding return format and compare.
- Force each state value, using test rows in the query or a staging table, and confirm each one renders its mapped color. Confirm that an unmapped value renders the default rather than breaking the chart.
- Open the transform's error overlay or the Gateway logs to confirm no script exceptions occur on the polling interval. A transform that fails intermittently shows up as bars flickering back to the series default.
- Load the view in a live session in the browser. Designer preview and the session should match.
What goes wrong repeatedly with this pattern?
-
Integer vs. string keys. Databases, JSON return formats, and custom properties do not agree on types. Cast
stateexplicitly before the lookup, as the script does, or one state silently renders the default. -
Column name case. The query may return
StateorSTATEdepending on the database and alias. Alias the column in the named query so the transform reads a fixed key. - Theme variables in the color map. The chart renderer draws the bars itself and does not always resolve CSS variables the way styled components do. Use literal hex strings in the map and confirm them in a browser session.
- Series fill overriding intent. A static fill on the series still applies wherever the derived field is missing. Leave it set to a neutral color so missing data is visible, not disguised.
- Adding a second series per state. One series per state also produces colored bars, but it multiplies lane handling and legend entries, and you must edit the chart every time a state is added. The single-series lookup scales with the dictionary.
FAQ
Can I use a property binding instead of deriveFieldsFromData for Gantt bar colors?
Not for per-bar colors. A binding on the series fill applies one color to every column in series[0]. Per-bar color has to come from a field in each data row that deriveFieldsFromData.fill.color names.
Does the named query need to return a color column?
No. A script transform on the dataSources binding can add the color field after the query runs, using a dictionary keyed on the integer state. A SQL CASE or a join to a color table is the alternative when the database should own the palette.
Can Inductive Automation support help if the color field is correct but bars keep the default fill?
Escalate once you have confirmed that every row in props.dataSources carries a valid hex string, the field name in deriveFieldsFromData matches it exactly, and the transform logs no errors. At that point the fault is in the chart component, not the data. Open a case with Inductive Automation support and include your Ignition version, the exported view, and a sample of the rendered data.