Musson Industrial’s Embr-Charts Module

Yeah... Using Github is not a strength, lol. Think I got it.

1 Like

Embr is released as a suite; Embr v8.1 - 2026.01.21 was the latest 8.1 release. Under that GitHub release you will find downloads for all the 8.1 modules.

The module files have either Ignition81 or Ignition83 in the name to help :wink:

2 Likes

I would like to use Chart.js or an Apex chart to provide an Easy Chart type of experience for our technicians. The tags in question do not have history enabled, but the data is captured when the device is powered on via a Transaction Group.

The Easy Chart pulls the data in via the DB Pens.

I have tried binding a dataset from a named query to a few different things, but after reading the docs it looks like I have to transform the dataset into an array.

EDIT: This chart is Chart.js Line

After going in circles with AI, I got my two chosen columns, t_stamp and Col01, to display on the chart, but it does not look like a trend line like Easy Chart. It's just plotting (x,y), and when I add some sort of callback on the scales: y, the y-axis labels disappear. I'm assuming it's because these values are very small (1.0E-4 to -6 range).
AI was trying to get me to add a scales: x: time: unit:'minute', but I don't think time is an allowed attribute of scales.

The dataset binding is returned as JSON, here is the script transform:

rows = value or []
	
	# Build [{x: <epoch_ms>, y: <float>}, ...]
	points = []
	for r in rows:
	    ts = r.get('t_stamp')
	    y_raw = r.get('CZ01')
	    if ts is None or y_raw is None:
	        continue
	    try:
	        x = int(ts)  # epoch in ms; Chart.js time scale accepts this
	        y = float(y_raw)
	        points.append({"x": x, "y": y})
	    except Exception:
	        # skip any malformed values
	        continue
	
	# Sort by time just in case
	points.sort(key=lambda p: p["x"])
	
	# Return Chart.js-style data object
	return {
	    "labels": [],  # not used when we provide {x,y} points
	    "datasets": [{
	        "label": "CZ01",
	        "data": points,
	        "borderColor": "#4f46e5",
	        "backgroundColor": "rgba(79,70,229,0.12)",
	        "tension": 0.2,
	        "pointRadius": 0,
	        "fill": False
	    }]
	}

Here is what the chart looks like with the top 200 data points:

AI provided the script transform, one of several versions.

And this is the props.options:

{
  "animation": {
    "duration": 1000,
    "easing": "easeOutQuart",
    "delay": 0,
    "loop": false
  },
  "animations": {
    "numbers": {
      "properties": [
        "x",
        "y",
        "borderWidth",
        "radius",
        "tension"
      ],
      "type": "number"
    },
    "colors": {
      "properties": [
        "color",
        "borderColor",
        "backgroundColor"
      ],
      "type": "color"
    }
  },
  "elements": {
    "point": {},
    "line": {},
    "bar": {},
    "arc": {}
  },
  "events": [
    "mousemove",
    "mouseout",
    "click",
    "touchstart",
    "touchmove"
  ],
  "font": {
    "size": 12,
    "style": "normal",
    "weight": "normal"
  },
  "interaction": {
    "mode": "nearest",
    "intersect": true,
    "axis": "x",
    "includeInvisible": false
  },
  "layout": {
    "autoPadding": true,
    "padding": 0
  },
  "maintainAspectRatio": false,
  "normalized": true,
  "parsing": false,
  "plugins": {
    "annotation": false,
    "autocolors": false,
    "colors": {
      "enabled": true
    },
    "crosshair": false,
    "datalabels": false,
    "dragData": false,
    "decimation": {
      "enabled": true,
      "threshold": 1,
      "algorithm": "lttb"
    },
    "filler": {
      "drawTime": "beforeDraw",
      "propagate": false
    },
    "legend": {
      "display": true,
      "position": "top",
      "align": "center"
    },
    "subtitle": {
      "align": "center",
      "display": false,
      "position": "top",
      "text": "Title"
    },
    "title": {
      "align": "center",
      "display": false,
      "position": "top",
      "text": "Title"
    },
    "tooltip": {
      "enabled": true,
      "position": "average",
      "callbacks": {
        "label": "function(ctx){ return \u0027CZ01: \u0027 + Number(ctx.parsed.y).toExponential(6);"
      },
      "backgroundColor": "#000000cc",
      "titleColor": "#ffffff",
      "titleAlign": "left",
      "bodyColor": "#ffffff",
      "bodyAlign": "left",
      "footerColor": "#ffffff",
      "footerAlign": "left",
      "padding": 6,
      "displayColors": true,
      "borderColor": "#000000",
      "borderWidth": 0
    },
    "zoom": {
      "pan": {
        "enabled": true,
        "mode": "x",
        "modifierKey": null
      },
      "zoom": {
        "wheel": {
          "enabled": true,
          "modifierKey": "ctrl"
        },
        "drag": {
          "enabled": true,
          "modifierKey": "ctrl"
        },
        "pinch": {
          "enabled": true
        },
        "mode": "x"
      }
    }
  },
  "resizeDelay": 0,
  "responsive": true,
  "scales": {
    "x": {
      "type": "time"
    },
    "y": {
      "type": "linear",
      "ticks": {
        "backdropColor": "rgba(255, 255, 255, 0.75)",
        "backdropPadding": 2,
        "display": true,
        "major": {},
        "padding": 3,
        "showLabelBackdrop": false,
        "textStrokeColor": "",
        "textStrokeWidth": 0,
        "z": 0
      }
    }
  },
  "transitions": {
    "active": {
      "animation": {
        "duration": 400
      }
    },
    "hide": {
      "animations": {
        "colors": {
          "properties": [
            "borderColor",
            "backgroundColor"
          ],
          "type": "color",
          "from": "transparent"
        },
        "visible": {
          "type": "boolean",
          "duration": "easeInExpo"
        }
      }
    },
    "reset": {},
    "resize": {
      "animation": {
        "duration": 400
      }
    },
    "show": {
      "animations": {
        "colors": {
          "properties": [
            "borderColor",
            "backgroundColor"
          ],
          "type": "color",
          "from": "transparent"
        },
        "visible": {
          "type": "boolean",
          "duration": 0
        }
      }
    }
  }
}

So, what I would like to do from here is to be able to:

  • select/deselect multiple tags
  • display the data as more of a trend rather than points
  • expand/reduce the time frame, say 5 minutes to 2 hours.

Notes:
The datapoints are randomly generated values between 1E-4 and 1E-5. I understand I will probably need to use runPrepQuery or build a robust Sproc in SQL Server to handle the querying.
I hope I am the right track with this!

ChartTest.zip (64.3 KB)

Thanks for the quick reply. In attached a simplified part of my project. I use a view with a chart, embedded in another view: the binding of props.series generates the errors in the logger.

The errors appear when opening the web page, but not if reloading it. I’m using Chrome.

Are there any plans to support chartjs-plugin-streaming?

I found the site here:chartjs-plugin-streaming

At the moment, you have to build all this functionality yourself. There’s no quick way at it.
I know members of the community who have built these features into their own projects, but most people want to keep their charting implementations to themselves.

I’ve had ideas for creating an “Power Chart”-like component to include in Embr Charts, but have decided against it for several reasons:

  1. Users want to combine realtime and historical data. Doing this in a storage-agnostic way is difficult.
  2. I am not in a position to support a component with the required complexity.

According to my previous self, this plugin has some performance issues. This commit note from 2 years ago:

Remove Streaming Plugin
2024/5/29
On a 100k point stress test, removing the streaming plugin resulted in an increase of 60+ fps when using the zoom plugin to pan.

Since there is no good way to currently utilize the streaming plugin on the base chart, the plugin was removed.

I’m open to revisiting the inclusion of this plugin though, since it’s been a couple years and Embr’s JavaScript capabilities might be mature enough to support some type of streaming.

This is caused by the classic Perspective binding startup race.

The errors in the logs are from the component complaining that series[0] is null. Because your series binding isn't persistent, it's beening evaluated as null when the chart first loads. Once the binding results evaluate, the chart re-renders and everything continues as expected.

The fix is to persist reasonable default values for your bindings (like a completely empty series configuration).

Ok, I changed the values to persistent and now I don’t get errors. Thank you.

How do I update the chart data from interactions performed by custom plugins?

What is the correct

1 Like

Okay figured it out , you use in the plugin perspective.sendMessage('scheduleUpdate'… and then create a normal message hander on the component to recieve the updates.

1 Like

That’s one good way to do it, you could also directly write to the component’s property tree.

I think it’d be: this.store.custom.write(“path.to.key”, value)

That’s a very cool use case!