Musson Industrial’s Embr-Charts Module

Can you post a JSON export that shows your whole config (preferably with some sample data baked in)?

Here's an example of supplying an array to data.datasets[index].backgroundColor:

View JSON
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "meta": {
          "name": "Chartjs"
        },
        "position": {
          "height": 300,
          "width": 300,
          "x": 403,
          "y": 197
        },
        "props": {
          "data": {
            "datasets": [
              {
                "backgroundColor": [
                  "#36A2EB",
                  "#FFFFFF00",
                  "#FFCE56",
                  "transparent"
                ],
                "data": [
                  50,
                  20,
                  75,
                  25
                ],
                "label": "Dataset 1"
              },
              {
                "backgroundColor": [
                  "#E73C3C",
                  "#2ECC71",
                  "blue"
                ],
                "data": [
                  20,
                  80,
                  10
                ],
                "label": "Dataset 2"
              }
            ],
            "labels": [
              "A",
              "B",
              "C",
              "D"
            ]
          },
          "type": "doughnut"
        },
        "type": "embr.chart.chart-js"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}

Unfortunately the property browser complains about the schema, but AFAIK this isn't fixable without losing the color picker when backgroundColor is a single value.

Also, Chart.js supports hex color codes with alpha (#RRGGBBAA) as well as named colors (transparent).

1 Like

Thanks. I thought I was headed down the wrong path with datasets[n].backgroundColor and was focusing on options.plugins.colors.

1 Like

Would this be possible:

  • When the user pan and zooms into the chart, would we be able to know the starttime and endtime of the selected frame through a property or event?

  • The chart would default to a month of history with an aggregated query on Influx, but when zooming in close enough, we would need the data to be raw events.

Thanks for feedback!

1 Like

Yes, you can track the zoom/pan state by using the onPan and onZoom callback functions. Things have changed a bit since the last example, so I don't mind posting it again.

Setup:

  1. Add a custom object property on the chart called xAxis.
  2. Add the following arrow function to both options.plugsin.zoom.pan.onPan and options.plugsin.zoom.zoom.onZoom
  3. When the chart is panned or zoomed, the x-axis start and end will be written to the custom property.
(context) => { 
  const xAxis = context.chart.scales.x
  const customProps = this.store.custom

  customProps.write('xAxis.start', xAxis.start)
  customProps.write('xAxis.end', xAxis.end)
}

You can alternatively use onPanComplete and onZoomComplete if you only care about the end state.

It's entirely possible, but it's up to you to script this to meet your needs.
You can either drive the chart from the gateway or the client:

  1. Gateway
    • Write the zoom/pan state back to the gateway.
    • Use the JavaScript proxy for control of the chart, including inserting/appending data and re-rendering the chart.
  2. Client
    • Use inline plugins to have the client directly request the data it needs when zooming/panning.
    • Data requests could be through Perspective messages or through REST API calls.
4 Likes

I found the reason for my bug from a while back with the tooltip plugin not showing tooltips when displaying the x scale in timestack. The times need to be unix timestamps, not datetime strings. This would make sense (and will behave across timezones)... As soon as I change the timestamp strings to unix timestamps (millis), it works again. fwiw: I found it because I noticed errors being produced in the designer console that mentioned something about failing when running fromMillis.

Question for the group:

If Embr-Charts finds the Kyvis-Lab’s ApexCharts installed, should it forcefully uninstall the Kyvis-Labs version? Or is that too intrusive/suprising?

This.

3 Likes

I would agree with @pturmel ,

I can see someone "trying" something out and then uninstalling Embr for some reason and not realizing hey Apex Charts is gone and not understanding why.

2 Likes

A teaser; I've been able to majorly reduce the time-to-first render.

"Legacy" component (identical client-side code as Kyvis Labs Apex Charts 1.0.22):

Embr Charts version:

Head to head comparison:

Beta:
Embr-Charts-3.0.0-beta.modl (3.3 MB)

The remaining work is mainly polish:

  • Complete the property tree schema (or as good as possible).
  • Some tweaks to the example variants.
  • General code organization/cleanup.

The ApexCharts (Legacy) component included in version 3.0.0 will be identical to the latest Kyvis Labs version.

A following 3.1.0 version will include tweaks to the client-side code.

While the Kyvis Labs client-side code is written in TypeScript, it doesn't meet the aggressive linting settings used in Embr and some sections need to be rewritten in order to make the build system (and myself) happy.

18 Likes

Very exciting, glad there is a bit of a migration path for ApexCharts to this.

2 Likes

Decided to try some of this out and well, I am not sure what I am doing wrong. . .

When I print it all looks exactly right but never is updating the chart.

	if self.view.params.chartId == payload["chart"]:
		#self.getChild("Chartjs").props.data.datasets = payload["data"]
		#system.perspective.print(payload["data"])
		component = self.getChild("Chartjs")
		chart = component.getJavaScriptProxy('chart')
		data = payload["data"]
		try:
			updateChart = '''() => this.update('none')'''

			for ds in range(len(data)):
				
				addLabels = '''(label,i) => {
					this.data.datasets[i] = {data:[],label:[]}
					console.log(this.data.datasets)
					this.data.datasets[i].label = label
				}'''
				chart.runBlocking(addLabels, { 'label': data[ds]["label"],"i":ds })

				addData = '''(data,i) => {
					const oldData = this.data.datasets[i]?.data ?? []
					const newData = oldData.concat(data)
					this.data.datasets[i].data = newData
				}'''

				distributedData = BlueRidge.Utils.distributeEvenly(data[ds]["data"], 100)
				for d in distributedData:
					chart.runBlocking(addData, {'data': d,"i":ds })
					
			
			chart.runBlocking(updateChart)
		except Exception,e:
			system.perspective.print(e)
		self.view.custom.loading = False

I should note the commented out code works and renders the chart. Just seeing if I can make it more smooth.

Can you shared what distributedData looks like?

Here's a version that works for me, the only thing different is the distributedData:

# Root Message Handler
def onMessageReceived(self, payload):

	#self.getChild("Chartjs").props.data.datasets = payload["data"]
	#system.perspective.print(payload["data"])
	component = self.getChild("Chartjs")
	chart = component.getJavaScriptProxy('chart')
	data = payload["data"]
	try:
		updateChart = '''() => this.update('none')'''
	
		for ds in range(len(data)):
			
			addLabels = '''(label,i) => {
				this.data.datasets[i] = {data:[],label:[]}
				console.log(this.data.datasets)
				this.data.datasets[i].label = label
			}'''
			chart.runBlocking(addLabels, { 'label': data[ds]["label"],"i":ds })
	
			addData = '''(data,i) => {
				const oldData = this.data.datasets[i]?.data ?? []
				const newData = oldData.concat(data)
				this.data.datasets[i].data = newData
			}'''
	
	#		distributedData = BlueRidge.Utils.distributeEvenly(data[ds]["data"], 100)
			distributedData = [data[ds]["data"]]
			for d in distributedData:
				chart.runBlocking(addData, {'data': d,"i":ds })
				
		
		chart.runBlocking(updateChart)
	except Exception,e:
		system.perspective.print(e)
	self.view.custom.loading = False
# Button Action
def runAction(self, event):
	payload = {
		"data": [
			{
				"label": "Data1",
				"data": [
					{ "x": 0, "y": 1 },
					{ "x": 10, "y": 2 }
				]
			},
			{	
				"label": "Data2",
				"data": [
					{ "x": 0, "y": 10 },
					{ "x": 10, "y": 20 }
				]
			}
		]
	}
		
		
		
	system.perspective.sendMessage('update-chart', payload)

Here is the data.

Data
[
  [
    {
      "x": "1748750414830L",
      "y": 313.0
    },
    {
      "x": "1748751135880L",
      "y": 317.0
    },
    {
      "x": "1748751796702L",
      "y": 309.0
    },
    {
      "x": "1748752398090L",
      "y": 306.0
    },
    {
      "x": "1748752998893L",
      "y": 311.0
    },
    {
      "x": "1748753659548L",
      "y": 311.0
    }
  ],
  [
    {
      "x": "1748750475291L",
      "y": 314.0
    },
    {
      "x": "1748751195954L",
      "y": 310.0
    },
    {
      "x": "1748751856562L",
      "y": 311.0
    },
    {
      "x": "1748752458068L",
      "y": 304.0
    },
    {
      "x": "1748753058809L",
      "y": 312.0
    },
    {
      "x": "1748753719692L",
      "y": 313.0
    }
  ],
  [
    {
      "x": "1748750535198L",
      "y": 310.0
    },
    {
      "x": "1748751255871L",
      "y": 311.0
    },
    {
      "x": "1748751917525L",
      "y": 308.0
    },
    {
      "x": "1748752518358L",
      "y": 309.0
    },
    {
      "x": "1748753118941L",
      "y": 310.0
    },
    {
      "x": "1748753779788L",
      "y": 307.0
    }
  ],
  [
    {
      "x": "1748750655221L",
      "y": 313.0
    },
    {
      "x": "1748751316024L",
      "y": 306.0
    },
    {
      "x": "1748751977713L",
      "y": 310.0
    },
    {
      "x": "1748752578355L",
      "y": 308.0
    },
    {
      "x": "1748753178789L",
      "y": 313.0
    },
    {
      "x": "1748753839925L",
      "y": 306.0
    }
  ],
  [
    {
      "x": "1748750715421L",
      "y": 314.0
    },
    {
      "x": "1748751376143L",
      "y": 307.0
    },
    {
      "x": "1748752037744L",
      "y": 304.0
    },
    {
      "x": "1748752638399L",
      "y": 309.0
    },
    {
      "x": "1748753239050L",
      "y": 305.0
    },
    {
      "x": "1748753959951L",
      "y": 305.0
    }
  ],
  [
    {
      "x": "1748750775480L",
      "y": 313.0
    },
    {
      "x": "1748751436244L",
      "y": 317.0
    },
    {
      "x": "1748752097908L",
      "y": 311.0
    },
    {
      "x": "1748752698618L",
      "y": 311.0
    },
    {
      "x": "1748753298986L",
      "y": 308.0
    }
  ],
  [
    {
      "x": "1748750835588L",
      "y": 312.0
    },
    {
      "x": "1748751496245L",
      "y": 310.0
    },
    {
      "x": "1748752157992L",
      "y": 313.0
    },
    {
      "x": "1748752758405L",
      "y": 308.0
    },
    {
      "x": "1748753359113L",
      "y": 313.0
    }
  ],
  [
    {
      "x": "1748750955682L",
      "y": 313.0
    },
    {
      "x": "1748751556248L",
      "y": 305.0
    },
    {
      "x": "1748752218003L",
      "y": 309.0
    },
    {
      "x": "1748752818503L",
      "y": 310.0
    },
    {
      "x": "1748753419163L",
      "y": 312.0
    }
  ],
  [
    {
      "x": "1748751015673L",
      "y": 315.0
    },
    {
      "x": "1748751616317L",
      "y": 308.0
    },
    {
      "x": "1748752278067L",
      "y": 306.0
    },
    {
      "x": "1748752878819L",
      "y": 308.0
    },
    {
      "x": "1748753539295L",
      "y": 307.0
    }
  ],
  [
    {
      "x": "1748751075695L",
      "y": 312.0
    },
    {
      "x": "1748751736563L",
      "y": 311.0
    },
    {
      "x": "1748752338002L",
      "y": 308.0
    },
    {
      "x": "1748752938485L",
      "y": 305.0
    },
    {
      "x": "1748753600120L",
      "y": 309.0
    }
  ]
]

I just put them into even arrays and then loop through and append. Seems straight forward but maybe I am missing something.

I think this is the problem: the x-values are strings instead of longs.

Here's a working example with your data.

view.json
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "meta": {
          "name": "Chartjs"
        },
        "position": {
          "height": 300,
          "width": 300,
          "x": 131,
          "y": 217
        },
        "props": {
          "options": {
            "normalized": false,
            "plugins": {
              "zoom": {
                "pan": {
                  "modifierKey": null
                }
              }
            },
            "scales": {
              "x": {
                "type": "time"
              },
              "y": {
                "type": "linear"
              }
            }
          }
        },
        "type": "embr.chart.chart-js"
      },
      {
        "events": {
          "component": {
            "onActionPerformed": {
              "config": {
                "script": "\t\t\n\tpayload \u003d {}\n\tsystem.perspective.sendMessage(\u0027update-chart\u0027, payload)"
              },
              "scope": "G",
              "type": "script"
            }
          }
        },
        "meta": {
          "name": "Button"
        },
        "position": {
          "height": 34,
          "width": 80,
          "x": 405,
          "y": 617
        },
        "type": "ia.input.button"
      }
    ],
    "meta": {
      "name": "root"
    },
    "scripts": {
      "customMethods": [
        {
          "name": "getData",
          "params": [],
          "script": "\treturn [\n\t[\n\t    {\n\t      \"x\": 1748750414830L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748751135880L,\n\t      \"y\": 317.0\n\t    },\n\t    {\n\t      \"x\": 1748751796702L,\n\t      \"y\": 309.0\n\t    },\n\t    {\n\t      \"x\": 1748752398090L,\n\t      \"y\": 306.0\n\t    },\n\t    {\n\t      \"x\": 1748752998893L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748753659548L,\n\t      \"y\": 311.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750475291L,\n\t      \"y\": 314.0\n\t    },\n\t    {\n\t      \"x\": 1748751195954L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748751856562L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748752458068L,\n\t      \"y\": 304.0\n\t    },\n\t    {\n\t      \"x\": 1748753058809L,\n\t      \"y\": 312.0\n\t    },\n\t    {\n\t      \"x\": 1748753719692L,\n\t      \"y\": 313.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750535198L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748751255871L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748751917525L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748752518358L,\n\t      \"y\": 309.0\n\t    },\n\t    {\n\t      \"x\": 1748753118941L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748753779788L,\n\t      \"y\": 307.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750655221L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748751316024L,\n\t      \"y\": 306.0\n\t    },\n\t    {\n\t      \"x\": 1748751977713L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748752578355L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748753178789L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748753839925L,\n\t      \"y\": 306.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750715421L,\n\t      \"y\": 314.0\n\t    },\n\t    {\n\t      \"x\": 1748751376143L,\n\t      \"y\": 307.0\n\t    },\n\t    {\n\t      \"x\": 1748752037744L,\n\t      \"y\": 304.0\n\t    },\n\t    {\n\t      \"x\": 1748752638399L,\n\t      \"y\": 309.0\n\t    },\n\t    {\n\t      \"x\": 1748753239050L,\n\t      \"y\": 305.0\n\t    },\n\t    {\n\t      \"x\": 1748753959951L,\n\t      \"y\": 305.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750775480L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748751436244L,\n\t      \"y\": 317.0\n\t    },\n\t    {\n\t      \"x\": 1748752097908L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748752698618L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748753298986L,\n\t      \"y\": 308.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750835588L,\n\t      \"y\": 312.0\n\t    },\n\t    {\n\t      \"x\": 1748751496245L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748752157992L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748752758405L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748753359113L,\n\t      \"y\": 313.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748750955682L,\n\t      \"y\": 313.0\n\t    },\n\t    {\n\t      \"x\": 1748751556248L,\n\t      \"y\": 305.0\n\t    },\n\t    {\n\t      \"x\": 1748752218003L,\n\t      \"y\": 309.0\n\t    },\n\t    {\n\t      \"x\": 1748752818503L,\n\t      \"y\": 310.0\n\t    },\n\t    {\n\t      \"x\": 1748753419163L,\n\t      \"y\": 312.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748751015673L,\n\t      \"y\": 315.0\n\t    },\n\t    {\n\t      \"x\": 1748751616317L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748752278067L,\n\t      \"y\": 306.0\n\t    },\n\t    {\n\t      \"x\": 1748752878819L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748753539295L,\n\t      \"y\": 307.0\n\t    }\n\t  ],\n\t  [\n\t    {\n\t      \"x\": 1748751075695L,\n\t      \"y\": 312.0\n\t    },\n\t    {\n\t      \"x\": 1748751736563L,\n\t      \"y\": 311.0\n\t    },\n\t    {\n\t      \"x\": 1748752338002L,\n\t      \"y\": 308.0\n\t    },\n\t    {\n\t      \"x\": 1748752938485L,\n\t      \"y\": 305.0\n\t    },\n\t    {\n\t      \"x\": 1748753600120L,\n\t      \"y\": 309.0\n\t    }\n\t  ]\n\t] "
        }
      ],
      "extensionFunctions": null,
      "messageHandlers": [
        {
          "messageType": "update-chart",
          "pageScope": true,
          "script": "\n\tcomponent \u003d self.getChild(\"Chartjs\")\n\tchart \u003d component.getJavaScriptProxy(\u0027chart\u0027)\n\n\tdata \u003d self.getData()\n\t\n\tupdateChart \u003d \u0027\u0027\u0027() \u003d\u003e this.update(\u0027none\u0027)\u0027\u0027\u0027\n\taddData \u003d \u0027\u0027\u0027(data, i) \u003d\u003e {\n\t\tconst dataset \u003d this.data.datasets[i] ?? {}\n\t\t\n\t\tconst oldData \u003d dataset?.data ?? []\n\t\tdataset.data \u003d oldData.concat(data)\n\t\t\n\t\tthis.data.datasets[i] \u003d dataset\n\t}\u0027\u0027\u0027\n\n\tfor i, d in enumerate(data):\n\t\tchart.runAsync(addData, {\u0027data\u0027: d, \u0027i\u0027: i })\n\t\t\t\n\tchart.runAsync(updateChart)\n\t",
          "sessionScope": false,
          "viewScope": false
        }
      ]
    },
    "type": "ia.container.coord"
  }
}

Also, you can get away with using runAsync instead of runBlocking for a perceived speed boost.

  1. JavaScript is single threaded.
  2. The order of runAsync calls is guaranteed to be sequential.
  3. Therefore: sequential runAsync calls will always run in the client in the order they are submitted.

By "firing and forgetting" with runAsync calls, you cut out a network ping-pong for each addData invocation while still being guaranteed that your data is added in order and the updateChart is called at the end.

And if you do want to know when the update is totally done, you could leave the addData's as async and only block on the final updateChart call.

3 Likes

Embr-Charts v3.0.0

The ApexCharts Update

This update adds two new components:

  1. ApexCharts - [documenation]
    • This is a new implementation of the ApexCharts charting library as an Ignition component.
      • Simplified rendering lifecycle (i.e. quicker to render/update)
      • JavaScript proxy support for direct chart interaction.
      • Improved designer property schema support.
      • Expanded selection of default component variants.
  2. ApexCharts (Legacy) - [documention]

Other Changes

Major Changes

  • 36a7970: (JavaScript Proxy) getJavaScriptProxy no longer requires a propertyName.
    • Previously, getJavaScriptProxy(propertyName) allowed a component delegate to proxy multiple properties. However, since users couldn't interact with multiple proxy targets simultaneously, this design proved ineffective—requiring multiple proxy objects for multiple properties.Now, a component delegate may only return a single proxied object. This encourages bundling proxyable state into one object, improving usability for component consumers.The getJavaScriptProxy(propertyName) overload is still supported, but the propertyName is ignored.

Patch Changes

  • 36a7970: Migrate from deprecated moduleDependencies to supported moduleDependencySpecs in build.gradle.kts.

Sponsorship

If you benefit from this module for commercial use, we ask you to consider sponsoring our efforts through GitHub sponsors. Your support will allow us to continue development of open-source modules and tools that benefit the entire community (plus there are some bonuses for sponsors :slightly_smiling_face:).

Download

Embr-Charts-3.0.0.modl

9 Likes

do the Apex rendering enhancements affect the legacy module? I'm guessing no, but I don't want to uninstall the legacy module and refactor on several gateways if it's not needed.

Actually, kind of yes. Travis just fixed the initial render bug in the Kyvis-Lab’s 1.0.23 release, which is the version included in Embr-Charts.

1 Like

What's the best way to install this when I have Kyvis installed (older version 10.0.19). I'm not using it in a critical way, but it is used in a few projects where you can shift between XYChart and ApexChart for testing purposes.

Should I:
Uninstall Kyvis first, then install this?
Install this, then uninstall Kyvis?
Install this and keep Kyvis installed?

Sounds like it doesn't really matter as this will "overwrite" Kyvis. Just want to make sure and keep the gateway as "clean" as possible :slight_smile:

1 Like

The best way is your first option:

  1. Uninstall the Kyvis-Labs module.
  2. Install Embr-Charts.
1 Like

Been a busy week or so and I am just now getting around to this.

That worked for me.

Makes all of these chart render pretty smooth

At least 100k datapoints or more per graph. Embr-Charts are awesome :man_shrugging:

4 Likes