Musson Industrial’s Embr-Charts Module

This is the Component Events update, featuring a whole bunch of user configurable client-side events.

The event property is broken into two categories: lifecycle and dom events.

Lifecycle Events (events.lifecycle)

Several component lifecycle events are now configurable. Supported events:

  • onMount
  • onUpdate
  • onUnmount

DOM Events (events.dom)

All React Synthetic Event handlers are now configurable through events.dom. Not all of these events are meaningful/supported by the underlying canvas element, but the full list is provided anyways.

These events are registered through the supported SDK interface and do not interfere with the normal gateway-side events (e.g. you can still add Component Events).

Supported events (go crazy):

  • onCopy
  • onCut
  • onPaste
  • onCompositionEnd
  • onCompositionStart
  • onCompositionUpdate
  • onFocus
  • onBlur
  • onChange
  • onBeforeInput
  • onInput
  • onReset
  • onSubmit
  • onInvalid
  • onLoad
  • onError
  • onKeyDown
  • onKeyPress
  • onKeyUp
  • onAbort
  • onCanPlay
  • onCanPlayThrough
  • onDurationChange
  • onEmptied
  • onEncrypted
  • onEnded
  • onLoadedData
  • onLoadedMetadata
  • onPause
  • onPlay
  • onPlaying
  • onProgress
  • onRateChange
  • onResize
  • onSeeked
  • onSeeking
  • onStalled
  • onSuspend
  • onTimeUpdate
  • onVolumeChange
  • onWaiting
  • onAuxClick
  • onClick
  • onContextMenu
  • onDoubleClick
  • onDrag
  • onDragEnd
  • onDragEnter
  • onDragExit
  • onDragLeave
  • onDragOver
  • onDragStart
  • onDrop
  • onMouseDown
  • onMouseEnter
  • onMouseLeave
  • onMouseMove
  • onMouseOut
  • onMouseOver
  • onMouseUp
  • onSelect
  • onTouchCancel
  • onTouchEnd
  • onTouchMove
  • onTouchStart
  • onPointerDown
  • onPointerMove
  • onPointerUp
  • onPointerCancel
  • onPointerEnter
  • onPointerLeave
  • onPointerOver
  • onPointerOut
  • onScroll
  • onWheel
  • onAnimationStart
  • onAnimationEnd
  • onAnimationIteration
  • onTransitionEnd

Changes

  • The events.beforeRender property has been deprecated and is no longer suggested in the property schema.

New Sponsors

Shoutout to Artek for the new sponsorship!

If you’re interested in sponsoring this project, check out our GitHub sponsorship page.

7 Likes

This module is really cool, just need help with one thing-I can't quite get a plugin declared correctly:


What am I missing?
Have tried w/without the "'" on the type.

Appreciate it!

To configure the annotation plugin, put your properties under options.plugins.annotation.

The top level plugins property is for defining your own plugins from scratch (that's why all the property hints are for different callback functions).

2 Likes

That did it! Next question: I'm having trouble with them persisting. Do I need to create custom property 'annotations' and bind to that to keep persistence, or is there another trick?

What exactly do you mean?
As long as you don’t have any bindings set the properties saved in the designer will be saved in the view.

Whoops-user error. I had put in a startup event that was overwriting the properties.

1 Like

Loading data via the proxy object has got me curious.
My chart shows two hours worth of machine event data from a query binding on a custom prop, and while the amount of data seems modest (7 labels (machines) and 688 datasets total), the performance is a bit sluggish. Namely zooming and panning around.
Compared to what @ryan.white was attempting to do with 10k points, I would have thought to have had better performance on my end.
I have parsing enabled otherwise the chart doesn't work.
Should I be trying to get the parsing option turned off to gain performance or load data via proxy?

1 Like

Datasets or rows? I hope you meant rows.

I was loading entirely via javascript proxy so there was no waiting for the tree props to get sent to the client session. Tree props only contained dataset configurations and no data. I also have parsing turned off. Granted, I was using a line chart so your chart type might be more heavyweight.

Also, make sure your datasets are ordered x value ascending for the timestamps if you have parsing turned off.

Are you loading more data on zoom/pan? If not I'd say that points more to how the chart is handling the data it already has.

If you are, are you providing a debounce for the zoom/pan motion so the start/end values are not jumping and triggering multiple queries while you are zooming or panning?

Each event is a "dataset". Where typically you would have a single dataset representing a list of values, I am unable to do that to get the chart I need.
See the image I posted above of my data schema.

 data: {
      labels: ["Machine 1", "Machine 2"],
      datasets: [
      {
        label: 'Faulted',
        data: [{"x": [1742238000000, 1742238427752], "y": "Machine 1"}],
        borderWidth: 1
      },
      {
        label: 'Down',
        data: [{"x": [1742238000100, 1742238422741], "y": "Machine 2"}],
        borderWidth: 1
      },
      {
        label: 'In Cycle',
        data: [{"x": [1742238427752, 1742238428752], "y": "Machine 1"}],
        borderWidth: 1
      }
    ]
}

I wanted to allow loading more data dynamically as a user panned/zoomed but just have not figured that one out yet. I don't think it would perform very well in my current state, anyways.

Can you tell me what the difference is between loading data via the proxy object vs a binding, in regard to how the data is sent to the client? In my mind, the data has to be sent from the gateway, because it's Perspective.
Wouldn't the client have to wait on the data from the gateway in either case, or does the proxy object have a "direct-line" to the client somehow?

somanydata

:grimacing: I did not catch that first time around. I feel like that is contributing a lot to your delay. What chart type are you using currently? There has got to be a way to build your data to condense it down to 1 dataset per machine.

We've tried making Gantt style charts using Chart.js, and the experience has just been bad.

I think a new chart type would be required in order to make the rendering performance acceptable; the number of datasets required to make a Gantt chart using the bar type just crushes the rendering cycle (and naturally the pan/zoom function) :man_shrugging:.

If I needed a Gantt chart today, I'd use IA's XY Chart.

The property trees (props, custom, position, meta) send their data to the client through a change detection/delivery method.

When a change in a gateway-side property tree is detected (from a binding, a script, or anything else) the change delta is serialized to JSON and sent to the client over the WebSocket, where it is received and the change delta is applied to the client-side version of the same property tree.

Once the client-side version of the tree receives the updates, the client-side components receive their new properties and React to update their state. This whole process also happens in reverse when the client changes property tree values.

Usually this is a great system, but it can crap out when you need to send large datasets to a chart component:

  1. You don't have any control over when/how the data gets sent. If you put 10M data points into the property tree, all 10M data points are going to be serialized into a single JSON message and sent over the WebSocket :fire:.
  2. You usually don't need the benefits of a property tree for chart data. That means all the work the client is doing to track changes to your chart's data is extra work (and a potentially significant amount of work for large datasets).

So instead, we want to send our data directly into the chart and bypass the property tree.

The "normal" way that components accomplish this type of interaction is through Custom Component Methods. However, I was not happy with using Custom Components Methods for several reasons:

  1. They are rigid, and I don't want to manage custom methods for every single method supported by the underlying JavaScript component.
  2. They cannot be chained without a separate gateway round-trip.
  3. They don't allow for any custom client-side logic.

The proxy object is really just a fancy way of allowing you to call your own client-side custom component methods. The function and the parameters are serialized to JSON and sent over the WebSocket; you get control of the code ran on the client and the exact data that's sent.

By passing your data through a property tree, the client will basically hang until all the data is sent/received.

By passing through the proxy object, the client is allowed to load/stay responsive and receive chart data in reasonable batches. Also, the proxy object allows to run your own custom client-side code, which can include loading data from non-Perspective sources (like a WebDev API endpoint, for example).

3 Likes

This sounds like the functionality of my Time Series Database Cache in Vision. :grin:

1 Like

But your chart module is mo’ betta. lol
I have done it in an XY Chart and it’s just not as nice and less control.
If I had an inkling of an idea of how to build a module, I’d try bringing in something like vis-timeline.

1 Like

It’s way lamer though :joy:

For kicks and giggles, I’ll try loading it in via WebDev API.

Make a feature request on GitHub, could end up in Embr Charts.

I'd bet my last dollar that performance will be the same. From my experience the lag is all from rendering way too many individual datasets.

The conclusion from the Chart.js dev community is that Gantt charts aren't a core chart type, and therefore should be implemented as a plugin (which no-one has done yet).

If anyone can provide an implementation I'll be more than happy to include it in Embr Charts.

1 Like

I don’t suppose I could install this plugin in my chart?

The backend code would need updating to register it, I’m guessing?

In theory you could embed it as an inline plugin using the plugins property, but it won’t be a good time.

I remember looking at that repo, a couple things stood out:

  1. It doesn’t list the Chart.js version compatibility, but since it has been over 6 years since the last commit it’s definitely not built for V4.
  2. I don’t like that it requires new X axis types specifically for rendering the Gantt chart. AFAIK there’s no reason why new axes types are required, it should be able to work with any numeric/time X axis.
  3. There are no examples of category Y axes, which is my most common Gantt chart use case.

Edit: I will say that the code base looks pretty good though, it wouldn’t take much to turn it into a solid solution. Which probably explains the high forks to stars ratio on GitHub :man_shrugging:

1 Like

This might be heresy, but you could also do this via an embedded grafana view, where grafana directly pulls the data.

I had that thought last night.
Embr Charts is giving me the chart I need (and more).
The only issue with my particular application is the number of events generated. Currently, Ignition logs all events (no matter how small to some degree), and I can filter out events less than x duration. Doing that though would require massaging all events to fill in the gaps and combine like events if they are next to each other. Essentially, make the events fill the span of the x axis, while also making their sum durations equal the span.