Name change tracker over time

I am currently tracking program task name (comes in as a String tag, historized On Change).
I want to display the last 5 minutes of task names, but cannot find a component that I would intuitively choose to solve this problem. I would implement this is a horizontal strip that spans my view. Task name changes would naturally appear on the far right and move left over time in the 5 minute window that the strip spans. Ideally I could model them as points with lines and with colors and hover tooltips to see what name it is, but honestly any working implementation works and I can figure out the UI later.
|-------------------5 minutes-----------------------|
| blue | red | yellow | green (current) |
| ProgA | ProgB | ProgC | ProgD |

Any ideas on how to accomplish this would be appreciated.

My initial thought would be either a flex repeater or maybe equipment schedule component? Do you want the spans of each item displayed to correlate to the amount of time the tag was that value?

That would be ideal yes. But each span would still be moving left and eventually leave the view, since I only want to show the most recent 5minutes of task names.

You might be able to get something to work with the XY chart following a gnatt style. You might need to do a little scripting to get the data in the correct format, and then run that at a set rate (once every 5s?) I don't think you'll be able to get a smooth scroll effect with built in components.

If you are allowed to add modules, consider adding pturmel's Integration Toolkit.(Free) It adds functions to allow iteration in an expression. This would allow you to build the data for this xy chart entirely in an expression.

Ok that looks promising.
This is pretty much what I am going for:

Differences would be the dates always span the most recent 5min and I would want to be updating pretty frequently (~1 second). This could be reasonably scriptable.

I haven’t gone into the documentation you just shared yet, but off the top of your head do you think I could apply tooltips to each of those individual colored rectangles or have text boxes within them? If so this would be perfect.

Looks like the tooltip is more or less covered in the example, you might need to tweak it a bit to display the actual value.

You'll probably need to include the tag's value during the span as an additional key in the span dictionary.

For labels on the boxes, you'll need to duplicate the series configuration that you have for the boxes, change the render type to line, and make the line invisible. From there, configure the line bullets to display labels, and set the label text to the key of the tag value in the span dictionaries. There are a few examples scattered through the forum.

Quick example picture:

Example dict for a span with additional key:

{
  "name": "John",
  "fromDate": "2018-01-01 08:00",
  "toDate": "2018-01-01 10:00",
  "color": "#00FF30",
  "tagValue": "valueA"
}

I've included an example view down below, copy the entire thing, then shift + right click on an empty view in your project tree and select paste JSON. This will overwrite the entire view.

Example View
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "meta": {
          "name": "XYChart"
        },
        "position": {
          "basis": "375px"
        },
        "props": {
          "cursor": {
            "series": "key"
          },
          "dataSources": {
            "key": [
              {
                "color": "#00FF30",
                "fromDate": "2018-01-01 08:00",
                "name": "John",
                "tagValue": "valueA",
                "toDate": "2018-01-01 10:00"
              },
              {
                "color": "#FF0030",
                "fromDate": "2018-01-01 12:00",
                "name": "John",
                "tagValue": "valueB",
                "toDate": "2018-01-01 15:00"
              },
              {
                "color": "#00FF30",
                "fromDate": "2018-01-01 15:30",
                "name": "John",
                "tagValue": "valueA",
                "toDate": "2018-01-01 21:30"
              },
              {
                "color": "#00FF30",
                "fromDate": "2018-01-01 09:00",
                "name": "Jane",
                "toDate": "2018-01-01 12:00"
              },
              {
                "color": "#FF0030",
                "fromDate": "2018-01-01 13:00",
                "name": "Jane",
                "toDate": "2018-01-01 17:00"
              },
              {
                "color": "#00FF30",
                "fromDate": "2018-01-01 11:00",
                "name": "Peter",
                "toDate": "2018-01-01 16:00"
              },
              {
                "color": "#00FF30",
                "fromDate": "2018-01-01 16:00",
                "name": "Peter",
                "toDate": "2018-01-01 19:00"
              }
            ]
          },
          "legend": {
            "enabled": false
          },
          "scrollBars": {
            "horizontal": {
              "series": "key"
            }
          },
          "series": [
            {
              "candlestick": {
                "appearance": {
                  "deriveFieldsFromData": {
                    "fill": {
                      "color": "",
                      "opacity": ""
                    },
                    "stroke": {
                      "color": "",
                      "opacity": "",
                      "width": ""
                    }
                  },
                  "fill": {
                    "color": "",
                    "opacity": 1
                  },
                  "heatRules": {
                    "dataField": "",
                    "enabled": false,
                    "max": "",
                    "min": ""
                  },
                  "stacked": false,
                  "stroke": {
                    "color": "",
                    "opacity": 1,
                    "width": 1
                  }
                },
                "high": {
                  "x": "",
                  "y": ""
                },
                "low": {
                  "x": "",
                  "y": ""
                },
                "open": {
                  "x": "",
                  "y": ""
                }
              },
              "column": {
                "appearance": {
                  "deriveFieldsFromData": {
                    "fill": {
                      "color": "color",
                      "opacity": ""
                    },
                    "stroke": {
                      "color": "",
                      "opacity": "",
                      "width": ""
                    }
                  },
                  "fill": {
                    "color": "",
                    "opacity": 1
                  },
                  "heatRules": {
                    "dataField": "",
                    "enabled": false,
                    "max": "",
                    "min": ""
                  },
                  "height": null,
                  "stacked": false,
                  "stroke": {
                    "color": "",
                    "opacity": 1,
                    "width": 1
                  },
                  "width": null
                },
                "open": {
                  "x": "fromDate",
                  "y": ""
                }
              },
              "data": {
                "source": "key",
                "x": "toDate",
                "y": "name"
              },
              "defaultState": {
                "visible": true
              },
              "hiddenInLegend": false,
              "label": {
                "text": "name"
              },
              "line": {
                "appearance": {
                  "bullets": [
                    {
                      "deriveFieldsFromData": {
                        "fill": {
                          "color": "",
                          "opacity": ""
                        },
                        "rotation": "",
                        "stroke": {
                          "color": "",
                          "opacity": "",
                          "width": ""
                        }
                      },
                      "enabled": false,
                      "fill": {
                        "color": "",
                        "opacity": 1
                      },
                      "heatRules": {
                        "dataField": "",
                        "enabled": false,
                        "max": 100,
                        "min": 2
                      },
                      "height": 10,
                      "label": {
                        "position": {
                          "dx": 0,
                          "dy": 0
                        },
                        "text": "{value}"
                      },
                      "render": "circle",
                      "rotation": 0,
                      "stroke": {
                        "color": "",
                        "opacity": 1,
                        "width": 1
                      },
                      "tooltip": {
                        "background": {
                          "color": "",
                          "opacity": 1
                        },
                        "cornerRadius": 3,
                        "enabled": true,
                        "pointerLength": 4,
                        "text": "{name}: [bold]{valueY}[/]"
                      },
                      "width": 10
                    }
                  ],
                  "connect": true,
                  "fill": {
                    "color": "",
                    "opacity": 0
                  },
                  "minDistance": 0.5,
                  "stroke": {
                    "color": "",
                    "dashArray": "",
                    "opacity": 1,
                    "width": 3
                  },
                  "tensionX": 1,
                  "tensionY": 1
                },
                "open": {
                  "x": "",
                  "y": ""
                }
              },
              "name": "schedule data",
              "render": "column",
              "stepLine": {
                "appearance": {
                  "bullets": [
                    {
                      "deriveFieldsFromData": {
                        "fill": {
                          "color": "",
                          "opacity": ""
                        },
                        "rotation": "",
                        "stroke": {
                          "color": "",
                          "opacity": "",
                          "width": ""
                        }
                      },
                      "enabled": true,
                      "fill": {
                        "color": "",
                        "opacity": 1
                      },
                      "heatRules": {
                        "dataField": "",
                        "enabled": false,
                        "max": 100,
                        "min": 2
                      },
                      "height": 10,
                      "label": {
                        "position": {
                          "dx": 0,
                          "dy": 0
                        },
                        "text": "{value}"
                      },
                      "render": "circle",
                      "rotation": 0,
                      "stroke": {
                        "color": "",
                        "opacity": 1,
                        "width": 1
                      },
                      "tooltip": {
                        "background": {
                          "color": "",
                          "opacity": 1
                        },
                        "cornerRadius": 3,
                        "enabled": true,
                        "pointerLength": 4,
                        "text": "{name}: [bold]{valueY}[/]"
                      },
                      "width": 10
                    }
                  ],
                  "connect": true,
                  "fill": {
                    "color": "",
                    "opacity": 0
                  },
                  "minDistance": 0.5,
                  "stroke": {
                    "color": "",
                    "dashArray": "",
                    "opacity": 1,
                    "width": 3
                  },
                  "tensionX": 1,
                  "tensionY": 1
                },
                "open": {
                  "x": "",
                  "y": ""
                }
              },
              "tooltip": {
                "background": {
                  "color": "",
                  "opacity": 1
                },
                "cornerRadius": 3,
                "enabled": true,
                "pointerLength": 4,
                "text": "{tagValue}: [bold]{fromDate} - {toDate}[/]"
              },
              "visible": true,
              "xAxis": "time",
              "yAxis": "Operator",
              "zIndex": 0
            },
            {
              "candlestick": {
                "appearance": {
                  "deriveFieldsFromData": {
                    "fill": {
                      "color": "",
                      "opacity": ""
                    },
                    "stroke": {
                      "color": "",
                      "opacity": "",
                      "width": ""
                    }
                  },
                  "fill": {
                    "color": "",
                    "opacity": 1
                  },
                  "heatRules": {
                    "dataField": "",
                    "enabled": false,
                    "max": "",
                    "min": ""
                  },
                  "stacked": false,
                  "stroke": {
                    "color": "",
                    "opacity": 1,
                    "width": 1
                  }
                },
                "high": {
                  "x": "",
                  "y": ""
                },
                "low": {
                  "x": "",
                  "y": ""
                },
                "open": {
                  "x": "",
                  "y": ""
                }
              },
              "column": {
                "appearance": {
                  "deriveFieldsFromData": {
                    "fill": {
                      "color": "color",
                      "opacity": ""
                    },
                    "stroke": {
                      "color": "",
                      "opacity": "",
                      "width": ""
                    }
                  },
                  "fill": {
                    "color": "",
                    "opacity": 1
                  },
                  "heatRules": {
                    "dataField": "",
                    "enabled": false,
                    "max": "",
                    "min": ""
                  },
                  "height": null,
                  "stacked": false,
                  "stroke": {
                    "color": "",
                    "opacity": 1,
                    "width": 1
                  },
                  "width": null
                },
                "open": {
                  "x": "fromDate",
                  "y": ""
                }
              },
              "data": {
                "source": "key",
                "x": "toDate",
                "y": "name"
              },
              "defaultState": {
                "visible": true
              },
              "hiddenInLegend": false,
              "label": {
                "text": "name"
              },
              "line": {
                "appearance": {
                  "bullets": [
                    {
                      "deriveFieldsFromData": {
                        "fill": {
                          "color": "",
                          "opacity": ""
                        },
                        "rotation": "",
                        "stroke": {
                          "color": "",
                          "opacity": "",
                          "width": ""
                        }
                      },
                      "enabled": true,
                      "fill": {
                        "color": "",
                        "opacity": 1
                      },
                      "heatRules": {
                        "dataField": "",
                        "enabled": false,
                        "max": 100,
                        "min": 2
                      },
                      "height": 10,
                      "label": {
                        "position": {
                          "dx": 0,
                          "dy": 0
                        },
                        "text": "{tagValue}"
                      },
                      "render": "label",
                      "rotation": 0,
                      "stroke": {
                        "color": "",
                        "opacity": 1,
                        "width": 1
                      },
                      "tooltip": {
                        "background": {
                          "color": "",
                          "opacity": 1
                        },
                        "cornerRadius": 3,
                        "enabled": false,
                        "pointerLength": 4,
                        "text": "{name}: [bold]{valueY}[/]"
                      },
                      "width": 10
                    }
                  ],
                  "connect": false,
                  "fill": {
                    "color": "",
                    "opacity": 0
                  },
                  "minDistance": 0.5,
                  "stroke": {
                    "color": "",
                    "dashArray": "",
                    "opacity": 1,
                    "width": 0
                  },
                  "tensionX": 1,
                  "tensionY": 1
                },
                "open": {
                  "x": "fromDate",
                  "y": ""
                }
              },
              "name": "schedule data",
              "render": "line",
              "stepLine": {
                "appearance": {
                  "bullets": [
                    {
                      "deriveFieldsFromData": {
                        "fill": {
                          "color": "",
                          "opacity": ""
                        },
                        "rotation": "",
                        "stroke": {
                          "color": "",
                          "opacity": "",
                          "width": ""
                        }
                      },
                      "enabled": true,
                      "fill": {
                        "color": "",
                        "opacity": 1
                      },
                      "heatRules": {
                        "dataField": "",
                        "enabled": false,
                        "max": 100,
                        "min": 2
                      },
                      "height": 10,
                      "label": {
                        "position": {
                          "dx": 0,
                          "dy": 0
                        },
                        "text": "{value}"
                      },
                      "render": "circle",
                      "rotation": 0,
                      "stroke": {
                        "color": "",
                        "opacity": 1,
                        "width": 1
                      },
                      "tooltip": {
                        "background": {
                          "color": "",
                          "opacity": 1
                        },
                        "cornerRadius": 3,
                        "enabled": true,
                        "pointerLength": 4,
                        "text": "{name}: [bold]{valueY}[/]"
                      },
                      "width": 10
                    }
                  ],
                  "connect": true,
                  "fill": {
                    "color": "",
                    "opacity": 0
                  },
                  "minDistance": 0.5,
                  "stroke": {
                    "color": "",
                    "dashArray": "",
                    "opacity": 1,
                    "width": 3
                  },
                  "tensionX": 1,
                  "tensionY": 1
                },
                "open": {
                  "x": "",
                  "y": ""
                }
              },
              "tooltip": {
                "background": {
                  "color": "",
                  "opacity": 1
                },
                "cornerRadius": 3,
                "enabled": true,
                "pointerLength": 4,
                "text": "{tagValue}: [bold]{fromDate} - {toDate}[/]"
              },
              "visible": true,
              "xAxis": "time",
              "yAxis": "Operator",
              "zIndex": 1
            }
          ],
          "xAxes": [
            {
              "appearance": {
                "font": {
                  "size": "",
                  "weight": 500
                },
                "grid": {
                  "color": "",
                  "dashArray": "",
                  "minDistance": 60,
                  "opacity": 1,
                  "position": 0.5
                },
                "inside": false,
                "labels": {
                  "color": "",
                  "horizontalCenter": "middle",
                  "opacity": 1,
                  "rotation": 0,
                  "verticalCenter": "middle"
                },
                "opposite": false
              },
              "category": {
                "break": {
                  "enabled": false,
                  "endCategory": "",
                  "size": 0.05,
                  "startCategory": ""
                }
              },
              "date": {
                "baseInterval": {
                  "count": 1,
                  "enabled": false,
                  "skipEmptyPeriods": false,
                  "timeUnit": "minute"
                },
                "break": {
                  "enabled": false,
                  "endDate": "",
                  "size": 0.05,
                  "startDate": ""
                },
                "format": "HH:mm:ss a",
                "inputFormat": "M/d/yyyy HH:mm:ss",
                "range": {
                  "max": "",
                  "min": "",
                  "useStrict": false
                }
              },
              "inversed": false,
              "label": {
                "color": "",
                "enabled": false,
                "text": "Time"
              },
              "name": "time",
              "render": "date",
              "tooltip": {
                "background": {
                  "color": "",
                  "opacity": 1
                },
                "cornerRadius": 3,
                "enabled": true,
                "pointerLength": 4,
                "text": ""
              },
              "value": {
                "break": {
                  "enabled": false,
                  "endValue": 100,
                  "size": 0.05,
                  "startValue": 0
                },
                "format": "#,###.##",
                "logarithmic": false,
                "range": {
                  "max": "",
                  "min": "",
                  "useStrict": false
                }
              },
              "visible": true
            }
          ],
          "yAxes": [
            {
              "appearance": {
                "font": {
                  "size": "",
                  "weight": 500
                },
                "grid": {
                  "color": "",
                  "dashArray": "",
                  "minDistance": null,
                  "opacity": 1,
                  "position": 0
                },
                "inside": false,
                "labels": {
                  "color": "",
                  "horizontalCenter": "middle",
                  "opacity": 1,
                  "rotation": 0,
                  "verticalCenter": "middle"
                },
                "opposite": false
              },
              "category": {
                "break": {
                  "enabled": false,
                  "endCategory": "",
                  "size": 0.05,
                  "startCategory": ""
                }
              },
              "date": {
                "baseInterval": {
                  "count": 1,
                  "enabled": false,
                  "skipEmptyPeriods": false,
                  "timeUnit": "hour"
                },
                "break": {
                  "enabled": false,
                  "endDate": "",
                  "size": 0.05,
                  "startDate": ""
                },
                "format": "M/d/yyyy HH:mm:ss",
                "inputFormat": "M/d/yyyy HH:mm:ss",
                "range": {
                  "max": "",
                  "min": "",
                  "useStrict": false
                }
              },
              "inversed": false,
              "label": {
                "color": "",
                "enabled": false,
                "text": ""
              },
              "name": "Operator",
              "render": "category",
              "tooltip": {
                "background": {
                  "color": "",
                  "opacity": 1
                },
                "cornerRadius": 3,
                "enabled": true,
                "pointerLength": 4,
                "text": ""
              },
              "value": {
                "break": {
                  "enabled": false,
                  "endValue": 100,
                  "size": 0.05,
                  "startValue": 0
                },
                "format": "#,###.##",
                "logarithmic": false,
                "range": {
                  "max": "",
                  "min": "",
                  "useStrict": false
                }
              },
              "visible": true
            }
          ]
        },
        "type": "ia.chart.xy"
      }
    ],
    "meta": {
      "name": "root"
    },
    "props": {
      "direction": "column"
    },
    "type": "ia.container.flex"
  }
}

If you are going to be running this at a one second pace, make sure the driving query and related script will finish in under 1s.

Thanks Ryan!

This method seems like it will work with some finicky scripting and bindings. Marking as solved for now.

For the fun of it, if you have the Integration Toolkit installed:

where(
	forEach(
		{view.custom.nameHistory},
		asMap(
			"tagValue", it()['value'],
			"name", "NameChange",
			"color", {view.custom.colorSource}[idx() % len({view.custom.colorSource})],
			"fromDate", dateFormat(it()['timestamp'], "yyyy-MM-dd kk:mm:ss"),
			"toDate",  dateFormat(
				if(idx() < len({view.custom.nameHistory}) - 1,
				{view.custom.nameHistory}[(idx() + 1)]['timestamp'],
				now(0)), "yyyy-MM-dd kk:mm:ss")
		)
	), 
	dateDiff(it()['toDate'], it()['fromDate'], "sec") != 0
)

That expression will take tag history from a view custom property named nameHistory, that is assumed to have a tag history binding set for 'asStored' and 'tall' format, and build the gnatt data from it. No script transform required. It also assumes you have a list of colors to use in view.custom.colorSource.

The outer where statement trims any items that have a 0s length, these seem to break the gnatt chart.

2 Likes

You could also do this via the view canvas - if you just set a five minute duration on the class for position transitions, you don't have to track any state - just start an event view at one side, and tell it to move (off screen) to the other side. Then set up an async cleanup in some manner so you're not leaking components over time.

Not to say that the chart based approach isn't potentially better.

I couldn’t figure out exactly where to put this expression setup (or maybe I didn’t have the toolkit configured correctly), but I followed the same logic with some custom scripted properties and was able to achieve this!

image

I do have some bugs/questions if you guys don’t mind helping again. Right now I am using a dynamically updated dataset that, using a script transform, turns itself into the following format to fit into the XYChart dataSources property.


The path to the program name tag is also dynamic, I have multiple robots and am switching between them via a dropdown menu, so the program name I am currently tracking’s path can change.
However when I switch between robots, the chart gets wiped and no longer shows the historical name changes from the past 5 minutes. It is probably an issue with my scripting one either datasources or one of my properties, but I can’t identify it.
Additionally, if the same task has been running for 5min, as soon as it crosses the 5min mark, it gets wiped and the chart is empty. This happens because the start time goes out of scope, so I assume I need some custom logic to fix it to the start and end time of the 5min window until a new task starts?

def transform(self, value, quality, timestamp):


    import system
    if value is None or value.getRowCount() == 0:
        return []  # no history yet

    # Color mapping for known tasks
    colorMap = {
        "INT_ANS": "#00FF30",
        "PRG0601": "#FF0030",
        "WUC1_030": "#30A0FF",
        "WUC2_030": "#FFD030",
        "PNS0061": "#A030FF",
        "WCL1_030": "#FF8030",
        "WCL2_030": "#00C0C0"
    }

    rows = value.getRowCount()
    data = []
    now = system.date.now()

    for i in range(rows):
        tagValue = value.getValueAt(i, "value")
        startTs = value.getValueAt(i, "timestamp")

        # End time = next row's timestamp, or 'now' if last row
        if i < rows - 1:
            endTs = value.getValueAt(i + 1, "timestamp")
        else:
            endTs = now

        segment = {
            "color": colorMap.get(tagValue, "#888888"),  # default gray if missing
            "fromDate": system.date.format(startTs, "yyyy-MM-dd HH:mm:ss"),
            "toDate": system.date.format(endTs, "yyyy-MM-dd HH:mm:ss"),
            "name": "Program",
            "tagValue": tagValue,
            # Tooltip shows task name + time span
            "tooltip": "{}\n{} → {}".format(
                tagValue,
                system.date.format(startTs, "HH:mm:ss"),
                system.date.format(endTs, "HH:mm:ss")
            )
        }
        data.append(segment)

    return {"key": data}

The expression was meant to be put in an expression binding on a custom view property. Was it giving you an error when you were attempting to set it up? Hovering the error in the binding setup should give you a more detailed message.

Can you show how your robot selection is set up(bindings and where they go/are used)? As well as the tag history query that has the script transform applied? (Redacting as necessary)

When you select a different robot, does your base tag history query return any data?

Ah yes, I just tested this myself, the tag history query seems to return a single point with a timestamp matching the end of the time span. In this case its the same as what is returned by now(0), which causes that segment to have a 0s timespan, which the gnatt chart doesn't like.

replace your assignment of startTs with this:

		if rows == 1:
			startTs = system.date.addMinutes(now, -5)
		else:
			startTs = value.getValueAt(i, "timestamp")

If you ever change the span of time that you are showing, you'll need to update system.date.addMinutes to match the new span length. Same if you make the span dynamic.

Small side note, you don't need to import system in a transform, all system functions for that scope are available by default.

Updated Expression
where(
	forEach(
		{view.custom.nameHistory},
		asMap(
			"tagValue", it()['value'],
			"name", "NameChange",
			"color", {view.custom.colorSource}[idx() % len({view.custom.colorSource})],
			"fromDate", 
			dateFormat(
				if(len({view.custom.nameHistory}) = 1,
					addMinutes(now(0), -5),
					it()['timestamp']
				), "yyyy-MM-dd kk:mm:ss"
			),
			"toDate",  
			dateFormat(
				if(idx() < len({view.custom.nameHistory}) - 1,
					{view.custom.nameHistory}[(idx() + 1)]['timestamp'],
					now(0)
				), "yyyy-MM-dd kk:mm:ss")
		)
	), dateDiff(it()['fromDate'], it()['toDate'], 'sec') != 0
)