# Musson Industrial’s Embr-Charts Module

**URL:** <https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618>\
**Category:** 3rd Party Modules\
**Tags:** perspective\
**Created:** [June 10, 2024, 12:53pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618 "2024-06-10T12:53:46Z")\
**Posts on this page:** 20\
**Page:** 5

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 13, 2024, 3:33am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/81 "2024-12-13T03:33:35Z")

</div>

> [@Sebastian\_Vasco](#):
>
> I tried embedding JavaScript code before but it just broke the chart.

Here's the current docs for scriptable options: [Scriptable Options | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-charts/components/chart-js/scriptable-options)

It is JavaScript, but currently the return semantics are kinda messed up.

You must provide an arrow function, but currently (v1.x.x), the converted functions do not support direct expressions. The `return` keyword must be used.

For example, in the `tick.callback` earlier you had to use the `return` keyword, even though the function should be able to run as an expression.

I am planning to fix this in 2.0, and it will be a breaking change:

```js
// 1.X.X (Arrow function syntax, anonymous function return semantics)
callback: '(value) => return value.toFixed(1)' # valid
callback: '(value) => { return value.toFixed(1) }' # valid
callback: '(value) => value.toFixed(1)' # invalid

// 2.X.X (Arrow function syntax/semantics)
callback: '(value) => return value.toFixed(1)' # invalid
callback: '(value) => { return value.toFixed(1) }' # valid
callback: '(value) => value.toFixed(1)' # valid

```

It should have been that way from the beginning and I didn't catch it 🤷‍♂️. If you want to future proof yourself, use the middle syntax (braces block with an explicit return).

Also worth mentioning is the global `self` and `client` parameters will be replaced by a `this` context. You will still be able to access the component and the client stores, but you'll do it through `this`.

---

<div class="post-metadata">

**Author:** ![Sebastian\_Vasco](https://avatars.discourse-cdn.com/v4/letter/s/ee7513/32.png) [@Sebastian\_Vasco](https://forum.inductiveautomation.com/u/Sebastian_Vasco)\
**Post date:** [December 13, 2024, 9:21pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/82 "2024-12-13T21:21:13Z")

</div>

Ok now that units are sorted out with a callback function, how can I fix the amount of ticks in a time X axis and format it? I notice that sometimes it draw the ticks diagonally and take up a lot more of space. Here is my current X axis config:

```python
x = {
  "type": "time",
  "grid": {
    "color": "transparent",
    "display": true,
    "tickColor": "var(--neutral-40)",
    "drawTicks": true
  },
  "ticks": {
    "display": true,
    "source": "auto"
  },
  "time": {
    "minUnit": "minute"
  }
}

```

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/a/a/aa983d940b4ab4f16d11b5face904018805847e5.png)

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 17, 2024, 10:12pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/83 "2024-12-17T22:12:03Z")

</div>

If you don't want the ticks to go diagonal at all, you can specific a `minRotation` and a `maxRotation` of zero.

 ![CU26zGA8pk](https://us1.discourse-cdn.com/inductiveautomation/original/3X/5/9/59d8b0c4a6588daee821bfb8223e921733cbc071.png)

The `scales[id].ticks.maxTicksLimit` property could also be useful to you, if you're trying to get things evenly spaced.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 20, 2024, 10:23pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/84 "2024-12-20T22:23:34Z")

</div>

# [Version 2.0.0](https://github.com/mussonindustrial/embr/releases/tag/%40embr-modules%2Fcharts%402.0.0)

This version brings majors changes to scriptable options, and introduces a method for directly interacting with the `Chart.js` chart instance.

### Breaking Changes

- The previously available `self` and `client` globally scoped objects available in Scriptable Options are gone. They are replaced with a single `perspective` object that will be the home of future Perspective specific utility functions. Use `perspective.context` to access the current `page`, `view`, `client`, and `component` objects.
- The `this` argument in Scriptable Options is now the component itself.
- Scriptable Options now behave as normal arrow functions, and do not require return statements for expressions. [See this post for more details.](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/81)

### New Feature, JavaScriptProxy Objects

I think this one is pretty cool.

I've been holding off on adding methods to the `Chart.js` component, because I couldn't find a way to make them practically useful. There's lots of methods on the `chart` instance that would be great to call from Perspective, but:

1. There's a lot of them (+ functions added by plugins).
2. Most of them require chaining with other methods in order to be useful.

This update adds a single component method `getJavaScriptProxy(property)`, that returns a `JavaScriptProxy` object for the given property key. Using this proxy object you can call `runAsync(function, args, callback)` or `runBlocking(function, args)` in order to run JavaScript code with the proxied property as the `this` context.

This follows the same signature as the `runJavaScript~` functions in our [Embr-Periscope](https://forum.inductiveautomation.com/t/musson-industrials-embr-periscope-module/99047) module:

- `function` is JavaScript code as a string containing a JavaScript arrow function. If this function is `async` or returns a `Promise` it will do what you expect, returning on the gateway when resolved.
- `args` is an optional dictionary, where the keys correspond to the names of the function arguments.
- `callback` is an optional function to be called when the async version of the function completely. The callback function should take a single argument that will contain the return value of the JavaScript function.

Currently the only proxy-able property is `chart` - the component's `Chart.js` chart instance.

#### Examples

Reset the zoom state of the chart:

```python
# Button onActionPerformed
def runAction(self, event):
	component = self.getSibling("Chartjs")
	chart = component.getJavaScriptProxy('chart')
	chart.runAsync("() => this.resetZoom('resize')")

```

Zooming to a specific part of the chart:

```python
# Button onActionPerformed
def runAction(self, event):
	component = self.getSibling("Chartjs")
	chart = component.getJavaScriptProxy('chart')
	
	args = {
		'scale': 'x',
		'bounds': {
			'min': 5,
			'max': 10
		}
	}
	
	chart.runAsync('''(scale, bounds) => {
		console.log(`Zooming scale ${scale} to bounds:`, bounds)
		this.zoomScale(scale, bounds, 'resize')
	}''', args)

```

The sky is the limit here.

Full documentation coming soon at [docs.mussonindustrial.com](https://docs.mussonindustrial.com/).

### Third-Party Module Showcase

We're now listed on the [Third-Party Module Showcase](https://inductiveautomation.com/moduleshowcase/module/musson-industrial-embr-charts)!

### Download

Quick-link: [Embr-Charts-2.0.0.modl](https://github.com/mussonindustrial/embr/releases/download/%40embr-modules%2Fcharts%402.0.0/Embr-Charts-2.0.0.modl)

As always, the latest release [can be found here](https://github.com/mussonindustrial/embr/releases).

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [December 20, 2024, 10:35pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/85 "2024-12-20T22:35:03Z")

</div>

> [@bmusson](#):
>
> This update adds a single component method `getJavaScriptProxy(property)`, that returns a `JavaScriptProxy` object for the given property key. Using this proxy object you can call `runAsync(function, args, callback)` or `runBlocking(function, args)` in order to run JavaScript code with the proxied property as the `this` context.

Wow! This is impressive.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 20, 2024, 10:42pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/86 "2024-12-20T22:42:21Z")

</div>

For the nerds:

[Client-side: ComponentDelegateJavaScriptProxy.ts  
 ](https://github.com/mussonindustrial/embr/blob/7d8e1100f260ae993f1c2054e257acaa0618565e/libraries/javascript/perspective-client/src/components/ComponentDelegateJavaScriptProxy.ts)

[Gateway-side: ComponentDelegateJavaScriptProxy.kt](https://github.com/mussonindustrial/embr/blob/7d8e1100f260ae993f1c2054e257acaa0618565e/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/component/ComponentDelegateJavaScriptProxy.kt)

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 21, 2024, 10:31pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/87 "2024-12-21T22:31:31Z")

</div>

Here's an example of loading _extremely large_ datasets into the chart by bypassing the component props and directly updating the data of the chart instance.

I'm generating my data here, but it's not hard to imagine using a query instead.

```python
def runAction(self, event):
	from java.lang import Math

	component = self.parent.parent.getChild("Chartjs")
	chart = component.getJavaScriptProxy('chart')
	
	elements = self.view.custom.actions.elements
	chunkSize = self.view.custom.actions.chunkSize
	chunks = elements / chunkSize
	
	def setState(state):
		self.view.custom.actions.state = state
		
	addData = '''(data) => {
		const oldData = this.data.datasets[0].data
		const newData = oldData.concat(data)
		this.data.datasets[0].data = newData
	}'''
	
	updateChart = '''() => this.update('none')'''
		
	def getData(index, size):
		return [ {
			'x': i, 
			'y': 100 * Math.sin( i / 1000.0)
		} for i in range(index, index+size) ]
					
			
	for index in range(chunks):
		setState('Sending chunk %s/%s' % (index, chunks))
		data = getData(index*chunkSize, chunkSize)
		chart.runBlocking(addData, { 'data': data })
	
	chart.runBlocking(updateChart)
	setState('Finished.')

```

(The chunking code is crude, so what 🤷‍♂️)

---

<div class="post-metadata">

**Author:** ![Benjamin\_Furlani](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/benjamin_furlani/32/66084_2.png) [@Benjamin\_Furlani](https://forum.inductiveautomation.com/u/Benjamin_Furlani)\
**Post date:** [December 22, 2024, 4:28pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/88 "2024-12-22T16:28:55Z")

</div>

This is exactly what I needed this Christmas! Excited to try this out!

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [December 23, 2024, 1:10am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/89 "2024-12-23T01:10:51Z")

</div>

Documentation has been updated, with several examples.

> **[getJavaScriptProxy | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-charts/components/chart-js/methods/getJavaScriptProxy/)**
>
> Returns an object that allows running client-side JavaScript against a component property.

> **[proxy.runAsync | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-core/javascript-proxy/methods/JavaScriptProxy.runAsync/)**
>
> The proxy.runAsync function allows you to asynchronously run JavaScript code on the client side within a Perspective session.

> **[proxy.runBlocking | Musson Industrial](https://docs.mussonindustrial.com/ignition/embr-core/javascript-proxy/methods/JavaScriptProxy.runBlocking/)**
>
> The proxy.runBlocking function allows you to run JavaScript code on the client side within a Perspective session.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [January 6, 2025, 10:41pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/90 "2025-01-06T22:41:45Z")

</div>

Got a snow day, so here's a new chart type:

## Smoothie Charts

[Smoothie Charts](http://smoothiecharts.org/) is a very simple charting library for streaming data (think [sparklines](https://en.wikipedia.org/wiki/Sparkline)).

This component is implemented differently: the data for the chart is not provided through `props`, but instead through a Component Event callback.

The component event `onChartUpdate` is called on a regular interval, based on the value in `options.update.interval`. In this event you are given a [JavaScript proxy object](https://docs.mussonindustrial.com/ignition/embr-core/javascript-proxy/) to update the state of the chart.

```ts
// chart.runAsync, `this` context: 
this: {
  appendData: (values: number[]) => void
  canvas: HTMLCanvasElement | null
  chart: SmoothieChart | null
  props: ComponentProps<SmoothieChartProps>
  series: TimeSeries[]
}

```

There's not much "hand-holding" in this first iteration, but I have included an `appendData` method that takes a list of values, and appends them to the available series (i.e. `values[0]` is appended to `series[0]`, `values[1]` is appended to `series[1]`, etc.).

You can also access this `chart` proxy from anywhere using [component.getJavaScriptProxy](https://docs.mussonindustrial.com/ignition/embr-core/javascript-proxy/accessing), like so:

```python
# Button onActionPerformed
# Manually add data
def runAction(self, event):
	component = self.getSibling("SmoothieChart")
	chart = component.getJavaScriptProxy('chart')
	
	values = [100, 2025]
	
	chart.runAsync('''(values) => {
		this.appendData(values)
	}''', { 'values': values })

```

See the [SmoothieCharts documentation](http://smoothiecharts.org/tutorial.html) for more information.

 ![java_lUMCk1iJ3d](https://us1.discourse-cdn.com/inductiveautomation/original/3X/7/b/7bc1c8b96a5975c0ede24adb174832070ac2bafa.png)

```python
# Above Example
def runAction(self, event):
	tagPaths = [
		'[default]SmoothieChart/Sin',
		'[default]SmoothieChart/Cos'
	]

	results = system.tag.readBlocking(tagPaths)
	values = [qv.value for qv in results]

	event.chart.runAsync('''(values) => {
		this.appendData(values)		
	}''', { 'values': values} )

```

This is still a pre-release, any/everything is liable to change.

I'm very open to feedback on this, I recognize that the current `event.chart.runAsync` system is a burden for simple use cases.

[Embr-Charts-2.0.0-SmoothieChart.modl](https://forum.inductiveautomation.com/uploads/short-url/tdA90NPXhVrWHxEY5Z9I5s9AK0O.modl) (2.0 MB)

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [January 8, 2025, 10:00pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/91 "2025-01-08T22:00:28Z")

</div>

New version with usability improvements.

The `event` object has some extra properties:

- `first` - (boolean) True on the first chart render.
- `memo` - (dictionary) Created on the first update, and shared between all subsequent updates. You can put things here to access between updates.

The `event.chart` object also has an additional property:

- `appendData` - (function) Direct access to the chart's `appendData` function, without the JavaScripting layer. Now supports backfilling.

```typescript
type ChartDataPoint = number | { value: number; timestamp: number }
type ChartSeriesData = ChartDataPoint | ChartDataPoint[]
appendData: (values: ChartSeriesData[]) => void

```

### Examples

```python
# (onChartUpdate) Memoization example
# Single series chart, that draws an incrementing counter.
def runAction(self, event):
	if (event.first):
		event.memo['count'] = 0
		return
		
	event.memo['count'] += 1
	
	values = [event.memo['count'] ]
	event.chart.appendData(values)

```

```python
# (onChartUpdate) Backfill example
# Realtime tag chart, with backfilling using tag history.
def runAction(self, event):
	if (event.first):
		tags = [v[0] for v in self.custom.values ]
		

		endDate = system.date.now()
		startDate = system.date.addSeconds(endDate, -15)	
		
		results = system.tag.queryTagHistory(tags, startDate, endDate)
		values = [[] for tag in tags ]
		for row in range(results.rowCount):
			for tag in range(len(tags)):
				timestamp = results.getValueAt(row, 0)
				values[tag].append({
					'timestamp': system.date.toMillis(timestamp),
					'value': results.getValueAt(row, tag + 1)
				})
				
		event.chart.appendData(values)
		return
						
	values = [v[1] for v in self.custom.values]
	event.chart.appendData(values)

```

> **Summary**
>
> ```python
> {
> "custom": {},
> "params": {},
> "props": {},
> "root": {
> "children": [
> {
> "custom": {
> "values": [
> null,
> null
> ]
> },
> "events": {
> "component": {
> "onChartUpdate": {
> "config": {
> "script": "\t\t\t\n\tif (event.first):\n\t\tevent.memo[\u0027count\u0027] \u003d 0\n\t\treturn\n\t\t\n\tevent.memo[\u0027count\u0027] +\u003d 1\n\t\n\tvalues \u003d [event.memo[\u0027count\u0027] ]\n\tevent.chart.appendData(values)"
> },
> "scope": "G",
> "type": "script"
> }
> }
> },
> "meta": {
> "name": "SmoothieChart"
> },
> "position": {
> "height": 200,
> "width": 239,
> "x": 51,
> "y": 157
> },
> "propConfig": {
> "custom.values": {
> "access": "PRIVATE"
> },
> "custom.values[0]": {
> "binding": {
> "config": {
> "fallbackDelay": 2.5,
> "mode": "direct",
> "tagPath": "[default]SmoothieChart/Sin"
> },
> "type": "tag"
> }
> },
> "custom.values[1]": {
> "binding": {
> "config": {
> "fallbackDelay": 2.5,
> "mode": "direct",
> "tagPath": "[default]SmoothieChart/Cos"
> },
> "type": "tag"
> }
> }
> },
> "props": {
> "options": {
> "delayMillis": 100,
> "scrollBackwards": false,
> "tooltip": true,
> "tooltipLine": {
> "strokeStyle": "red"
> },
> "update": {
> "interval": 100
> }
> },
> "redraw": false,
> "series": [
> {
> "fillStyle": "#0000FF38",
> "interpolation": "linear",
> "lineWidth": 2,
> "strokeStyle": "#3653CB",
> "tooltipLabel": "Series 0: "
> }
> ]
> },
> "type": "embr.chart.smoothie-chart"
> },
> {
> "custom": {
> "tags": [
> "[default]SmoothieChart/Sin",
> "[default]SmoothieChart/Sin2"
> ]
> },
> "events": {
> "component": {
> "onChartUpdate": {
> "config": {
> "script": "\tif (event.first):\n\t\ttags \u003d [v[0] for v in self.custom.values ]\n\t\t\n\n\t\tendDate \u003d system.date.now()\n\t\tstartDate \u003d system.date.addSeconds(endDate, -15)\t\n\t\t\n\t\tresults \u003d system.tag.queryTagHistory(tags, startDate, endDate)\n\t\tvalues \u003d [[] for tag in tags ]\n\t\tfor row in range(results.rowCount):\n\t\t\tfor tag in range(len(tags)):\n\t\t\t\ttimestamp \u003d results.getValueAt(row, 0)\n\t\t\t\tvalues[tag].append({\n\t\t\t\t\t\u0027timestamp\u0027: system.date.toMillis(timestamp),\n\t\t\t\t\t\u0027value\u0027: results.getValueAt(row, tag + 1)\n\t\t\t\t})\n\t\t\t\t\n\t\tevent.chart.appendData(values)\n\t\treturn\n\t\t\t\t\t\t\n\tvalues \u003d [v[1] for v in self.custom.values]\n\tevent.chart.appendData(values)"
> },
> "scope": "G",
> "type": "script"
> }
> }
> },
> "meta": {
> "name": "SmoothieChart_0"
> },
> "position": {
> "height": 200,
> "width": 239,
> "x": 322,
> "y": 157
> },
> "propConfig": {
> "custom.values": {
> "access": "PRIVATE",
> "binding": {
> "config": {
> "expression": "tags({this.custom.tags}, 100)"
> },
> "type": "expr"
> }
> }
> },
> "props": {
> "options": {
> "delayMillis": 100,
> "scrollBackwards": false,
> "tooltip": true,
> "tooltipLine": {
> "strokeStyle": "red"
> },
> "update": {
> "interval": 100
> }
> },
> "redraw": false,
> "series": [
> {
> "fillStyle": "#0000FF38",
> "interpolation": "bezier",
> "lineWidth": 2,
> "strokeStyle": "#3653CB",
> "tooltipLabel": "Series 0: "
> },
> {
> "fillStyle": "#FF654769",
> "interpolation": "bezier",
> "lineWidth": 2,
> "strokeStyle": "#FF6547"
> }
> ]
> },
> "type": "embr.chart.smoothie-chart"
> }
> ],
> "meta": {
> "name": "root"
> },
> "type": "ia.container.coord"
> }
> }
> 
> ```

Shoutout to @pturmel's `tags` expression function.

[Embr-Charts-2.0.0-SmoothieCharts-2.modl](https://forum.inductiveautomation.com/uploads/short-url/sFKz4B0ZDGhbEy2GsloWlgVAtQ9.modl) (2.0 MB)

---

<div class="post-metadata">

**Author:** ![YF129701](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/yf129701/32/60498_2.png) [@YF129701](https://forum.inductiveautomation.com/u/YF129701)\
**Post date:** [January 20, 2025, 11:03pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/92 "2025-01-20T23:03:01Z")

</div>

I'm using the Embr modules for a "dendrogram" or tree graph and I was wondering if its possible (or if it would be easy to) use an embedded view as a tooltip for the graphs. I know you can specify external HTML for the tooltip - is this what the proxy JS object would be used to do?

Speaking more broadly, it would be great to be able to use any embedded view as a tooltip in Perspective - maybe that's an idea for your next Embr component 😉

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [January 21, 2025, 3:14pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/93 "2025-01-21T15:14:23Z")

</div>

> [@YF129701](#):
>
> wondering if its possible (or if it would be easy to) use an embedded view as a tooltip for the graphs

That's an interesting idea, but no it's not currently possible.

> [@YF129701](#):
>
> know you can specify external HTML for the tooltip - is this what the proxy JS object would be used to do?

No, the JavaScript proxy object is for running JavaScript in the client that can interact with the chart object (calling `Chart.js` methods, modifying properties, etc).

I want to keep 1-to-1 behavior with all existing Chart.js examples found online; that means not modifying the behavior of the native Chart.js external HTML tooltip. But maybe there's a way to startup a view inside the external tooltip callback?

---

<div class="post-metadata">

**Author:** ![YF129701](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/yf129701/32/60498_2.png) [@YF129701](https://forum.inductiveautomation.com/u/YF129701)\
**Post date:** [January 21, 2025, 4:06pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/94 "2025-01-21T16:06:35Z")

</div>

Ahh, I see. I was more implying to replace the meta.tooltip that comes with Perspective components with one of your new Periscope embedded views - that way we can pass in any view for the tooltip so that we can get some modern functionality. For instance, hovering over a tree node gets you a summary of that node's data, or hovering over a material in a genealogy tree can display the inventory levels of that material.

Those are just examples - I have no idea that feasible that is. Probably something much easier for IA to do than to try overwriting their native behavior.

---

<div class="post-metadata">

**Author:** ![Benjamin\_Furlani](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/benjamin_furlani/32/66084_2.png) [@Benjamin\_Furlani](https://forum.inductiveautomation.com/u/Benjamin_Furlani)\
**Post date:** [January 21, 2025, 8:49pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/95 "2025-01-21T20:49:10Z")

</div>

> [@bmusson](#):
>
> But maybe there's a way to startup a view inside the external tooltip callback?

Although you want to keep it 1-1 you may be able to just have another property in the tooltip plugin (viewPath for lack of a better name) and If it is not null then you would just set the external property of the tooltip to be a function in the component that does a setInternalHtml to the `<View>` component.

Allows for it to function as intended, but also gives some power to it from an Ignition standpoint.

something like this:

```Javascript
  function installEmbeddedTooltips(transformedProps:PerspectiveChartProps){
    if(transformedProps.options.plugins.tooltip.viewPath){
      transformedProps.options.plugins.tooltip.external = someFunctionToInjectAnEmbeddedView
    }
  }

  const transformedProps = useMemo(() => {
    const { props: configProps, data } = extractPropsData(props.props)

    const transformedProps = transformProps(configProps, [
      getScriptTransform(props, props.store),
      getCSSTransform(chartRef.current?.canvas.parentElement),
    ]) as PerspectiveChartProps
    installEmbeddedTooltips(transformedProps) //Maybe here to allow for updates to be captured
    installPropsData(transformedProps, data)
    return transformedProps
  }, [props.props])

```

**Not Claiming this code is right or would work**

---

<div class="post-metadata">

**Author:** ![ryan.white](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ryan.white/32/73193_2.png) [@ryan.white](https://forum.inductiveautomation.com/u/ryan.white)\
**Post date:** [February 11, 2025, 11:15pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/96 "2025-02-11T23:15:00Z")

</div>

I'm getting an NPE attempting to write the x axis span start and end when the x axis is set to `time` type.

I wouldn't be surprised if this was just due to the time axis not having the same members available as the normal cartesian axis.

Edit: Yeah, that's what it was. Inspecting the output from console log on the `context.chart.scales.x` shows that time axis does not have `start` or `end`.

Version 2.0.1

```python
java.lang.NullPointerException: Element cannot be null

at java.base/java.util.Objects.requireNonNull(Unknown Source)

at com.inductiveautomation.perspective.gateway.property.PropertyTree$ValueNode.(PropertyTree.java:1286)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.nodeFromJson(PropertyTree.java:445)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.nodeFromJson(PropertyTree.java:411)

at com.inductiveautomation.perspective.gateway.property.PropertyTree$ValueNode.write(PropertyTree.java:1349)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.lambda$write$1(PropertyTree.java:543)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.writeImpl(PropertyTree.java:586)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.write(PropertyTree.java:543)

at com.inductiveautomation.perspective.gateway.property.PropertyTree.writeAll(PropertyTree.java:537)

at com.inductiveautomation.perspective.gateway.model.ComponentModel.onPropertySync(ComponentModel.java:557)

at com.inductiveautomation.perspective.gateway.model.ViewModel.lambda$onPropSync$6(ViewModel.java:362)

at com.inductiveautomation.perspective.gateway.model.ComponentModel.dispatch(ComponentModel.java:502)

at com.inductiveautomation.perspective.gateway.model.ComponentModel.dispatch(ComponentModel.java:508)

at com.inductiveautomation.perspective.gateway.model.ViewModel.onPropSync(ViewModel.java:359)

at com.inductiveautomation.perspective.gateway.model.PageModel$Handlers.lambda$onPropSync$29(PageModel.java:1250)

at java.base/java.util.concurrent.CompletableFuture.uniAcceptNow(Unknown Source)

at java.base/java.util.concurrent.CompletableFuture.uniAcceptStage(Unknown Source)

at java.base/java.util.concurrent.CompletableFuture.thenAccept(Unknown Source)

at com.inductiveautomation.perspective.gateway.model.PageModel$Handlers.lambda$onPropSync$31(PageModel.java:1248)

at com.inductiveautomation.perspective.gateway.api.LoggingContext.lambda$mdcWrap$0(LoggingContext.java:41)

at com.inductiveautomation.ignition.common.util.TimedRunnable.run(TimedRunnable.java:21)

at com.inductiveautomation.ignition.common.util.ExecutionQueue$PollAndExecute.run(ExecutionQueue.java:239)

at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)

at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)

at java.base/java.lang.Thread.run(Unknown Source)

```

Page Example

```python
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "custom": {
          "xAxis": {
            "end": 73.0968209836805,
            "start": -6.903179016319503
          }
        },
        "meta": {
          "name": "Chartjs"
        },
        "position": {
          "height": 300,
          "width": 633,
          "x": 69,
          "y": 242
        },
        "props": {
          "data": {
            "datasets": [
              {
                "data": [
                  {
                    "x": 0,
                    "y": 20.790464349448122
                  },
                  {
                    "x": 1,
                    "y": 27.541905413990484
                  },
                  {
                    "x": 2,
                    "y": 9.044548907913263
                  },
                  {
                    "x": 3,
                    "y": 63.58544584440057
                  },
                  {
                    "x": 4,
                    "y": 40.57122042169531
                  },
                  {
                    "x": 5,
                    "y": 46.17945691520222
                  },
                  {
                    "x": 6,
                    "y": 5.272367698783542
                  },
                  {
                    "x": 7,
                    "y": 78.62029735550435
                  },
                  {
                    "x": 8,
                    "y": 1.384758302338518
                  },
                  {
                    "x": 9,
                    "y": 8.094105544748587
                  },
                  {
                    "x": 10,
                    "y": 93.29711393437752
                  },
                  {
                    "x": 11,
                    "y": 78.54999817500953
                  },
                  {
                    "x": 12,
                    "y": 30.022085778849018
                  },
                  {
                    "x": 13,
                    "y": 68.38927062938775
                  },
                  {
                    "x": 14,
                    "y": 93.84973459203651
                  },
                  {
                    "x": 15,
                    "y": 85.15266831524416
                  },
                  {
                    "x": 16,
                    "y": 3.3272574045278946
                  },
                  {
                    "x": 17,
                    "y": 3.4738230710669837
                  },
                  {
                    "x": 18,
                    "y": 76.78237778113568
                  },
                  {
                    "x": 19,
                    "y": 76.13468451530511
                  },
                  {
                    "x": 20,
                    "y": 88.63284915490885
                  },
                  {
                    "x": 21,
                    "y": 92.74877419440537
                  },
                  {
                    "x": 22,
                    "y": 94.12886888152015
                  },
                  {
                    "x": 23,
                    "y": 97.06398992204936
                  },
                  {
                    "x": 24,
                    "y": 46.71625167184086
                  },
                  {
                    "x": 25,
                    "y": 13.224799335767212
                  },
                  {
                    "x": 26,
                    "y": 35.052260048430696
                  },
                  {
                    "x": 27,
                    "y": 29.46821488097423
                  },
                  {
                    "x": 28,
                    "y": 90.27395392440542
                  },
                  {
                    "x": 29,
                    "y": 7.832816289114186
                  },
                  {
                    "x": 30,
                    "y": 15.799390058813623
                  },
                  {
                    "x": 31,
                    "y": 80.66585380750348
                  },
                  {
                    "x": 32,
                    "y": 4.629220116475031
                  },
                  {
                    "x": 33,
                    "y": 24.00958904699422
                  },
                  {
                    "x": 34,
                    "y": 8.133906226255029
                  },
                  {
                    "x": 35,
                    "y": 81.93116748921958
                  },
                  {
                    "x": 36,
                    "y": 82.82333811028309
                  },
                  {
                    "x": 37,
                    "y": 95.37635776015469
                  },
                  {
                    "x": 38,
                    "y": 74.00252644137744
                  },
                  {
                    "x": 39,
                    "y": 27.90694570663671
                  },
                  {
                    "x": 40,
                    "y": 30.569029278167793
                  },
                  {
                    "x": 41,
                    "y": 11.104072480586257
                  },
                  {
                    "x": 42,
                    "y": 68.15581447149303
                  },
                  {
                    "x": 43,
                    "y": 27.051454957054943
                  },
                  {
                    "x": 44,
                    "y": 48.477001171626725
                  },
                  {
                    "x": 45,
                    "y": 73.62917163080661
                  },
                  {
                    "x": 46,
                    "y": 7.528701180738118
                  },
                  {
                    "x": 47,
                    "y": 21.794180888547032
                  },
                  {
                    "x": 48,
                    "y": 17.0019331614802
                  },
                  {
                    "x": 49,
                    "y": 80.4420964468141
                  }
                ],
                "label": "Dataset 1"
              },
              {
                "data": [
                  {
                    "x": 25,
                    "y": 99.00283352088223
                  },
                  {
                    "x": 26,
                    "y": 36.743648170189644
                  },
                  {
                    "x": 27,
                    "y": 33.58269158236888
                  },
                  {
                    "x": 28,
                    "y": 70.52608530981715
                  },
                  {
                    "x": 29,
                    "y": 78.48664561425423
                  },
                  {
                    "x": 30,
                    "y": 99.80258881269653
                  },
                  {
                    "x": 31,
                    "y": 23.56577175483505
                  },
                  {
                    "x": 32,
                    "y": 27.6260054014127
                  },
                  {
                    "x": 33,
                    "y": 11.84780583914382
                  },
                  {
                    "x": 34,
                    "y": 78.95435294680595
                  },
                  {
                    "x": 35,
                    "y": 89.0983947949238
                  },
                  {
                    "x": 36,
                    "y": 87.63589420154018
                  },
                  {
                    "x": 37,
                    "y": 82.4714423791225
                  },
                  {
                    "x": 38,
                    "y": 46.29407127415893
                  },
                  {
                    "x": 39,
                    "y": 12.463880984334686
                  },
                  {
                    "x": 40,
                    "y": 91.82293604614682
                  },
                  {
                    "x": 41,
                    "y": 9.645637547490315
                  },
                  {
                    "x": 42,
                    "y": 23.87358763050218
                  },
                  {
                    "x": 43,
                    "y": 64.35308758482624
                  },
                  {
                    "x": 44,
                    "y": 61.30614039090162
                  },
                  {
                    "x": 45,
                    "y": 60.123113026673906
                  },
                  {
                    "x": 46,
                    "y": 11.262924017594045
                  },
                  {
                    "x": 47,
                    "y": 36.72684549715057
                  },
                  {
                    "x": 48,
                    "y": 19.144808134626057
                  },
                  {
                    "x": 49,
                    "y": 11.499648689135334
                  },
                  {
                    "x": 50,
                    "y": 48.862954644422615
                  },
                  {
                    "x": 51,
                    "y": 44.0188922198424
                  },
                  {
                    "x": 52,
                    "y": 1.4731641405698048
                  },
                  {
                    "x": 53,
                    "y": 76.62654204425371
                  },
                  {
                    "x": 54,
                    "y": 97.09498548555429
                  },
                  {
                    "x": 55,
                    "y": 68.88623969428103
                  },
                  {
                    "x": 56,
                    "y": 9.347347267187478
                  },
                  {
                    "x": 57,
                    "y": 27.59945299037486
                  },
                  {
                    "x": 58,
                    "y": 38.62680310622668
                  },
                  {
                    "x": 59,
                    "y": 64.48467246665813
                  },
                  {
                    "x": 60,
                    "y": 99.50148874944306
                  },
                  {
                    "x": 61,
                    "y": 59.35003045509352
                  },
                  {
                    "x": 62,
                    "y": 7.407803152895475
                  },
                  {
                    "x": 63,
                    "y": 62.077958133975066
                  },
                  {
                    "x": 64,
                    "y": 19.550924069869623
                  },
                  {
                    "x": 65,
                    "y": 59.290461562229346
                  },
                  {
                    "x": 66,
                    "y": 15.46272365498117
                  },
                  {
                    "x": 67,
                    "y": 27.411246578970218
                  },
                  {
                    "x": 68,
                    "y": 59.515203740704415
                  },
                  {
                    "x": 69,
                    "y": 76.39871363134311
                  },
                  {
                    "x": 70,
                    "y": 20.255435491968
                  },
                  {
                    "x": 71,
                    "y": 57.26165130029921
                  },
                  {
                    "x": 72,
                    "y": 73.41996375745644
                  },
                  {
                    "x": 73,
                    "y": 68.57751118650746
                  },
                  {
                    "x": 74,
                    "y": 25.40840081665815
                  }
                ],
                "label": "Dataset 2"
              }
            ]
          },
          "options": {
            "plugins": {
              "zoom": {
                "pan": {
                  "modifierKey": null,
                  "onPan": "(context) \u003d\u003e { \n\nconst xAxis \u003d context.chart.scales.x\nconst customProps \u003d this.store.custom\n\ncustomProps.write(\u0027xAxis.start\u0027, xAxis.start)\ncustomProps.write(\u0027xAxis.end\u0027, xAxis.end)\n}"
                },
                "zoom": {
                  "onZoom": "(context) \u003d\u003e { \n\nconst xAxis \u003d context.chart.scales.x\nconst customProps \u003d this.store.custom\n\ncustomProps.write(\u0027xAxis.start\u0027, xAxis.start)\ncustomProps.write(\u0027xAxis.end\u0027, xAxis.end)\n}"
                }
              }
            },
            "scales": {
              "x": {
                "type": "time"
              },
              "y": {
                "type": "linear"
              }
            }
          }
        },
        "type": "embr.chart.chart-js"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}

```

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [February 12, 2025, 1:42am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/97 "2025-02-12T01:42:35Z")

</div>

Cross linking the resolution from here.

> [@\[BUG?\] Perspective Power Chart Bug - Embr-Chart as a Replacement?](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-time-series-unequal-major-gap/102084/34):
>
> Yeah, it seems like getZoomedScaleBounds() only returns the scales that have been zoomed. If you allow for panning and zooming on both axes then you'll have data for both x and y (and presumably y2 and y3 as well). Yup that's exactly it, you can't write undefined to a property. This is a good place to use JavaScript's ?? coalescing operator.

The NPE is coming from trying to assign `undefined` to a PropertyTree node, the solution is to make sure all the properties exist before writing.

---

<div class="post-metadata">

**Author:** ![andrew\_sf](https://avatars.discourse-cdn.com/v4/letter/a/4491bb/32.png) [@andrew\_sf](https://forum.inductiveautomation.com/u/andrew_sf)\
**Post date:** [February 12, 2025, 4:36pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/98 "2025-02-12T16:36:56Z")

</div>

Hi Ben,

I've been working with this module for a couple months now and it's working great overall.

Right now I'm running into an edge case where when I change the data/scales props while hovering with the crosshair, I get this getPixelForValue error in the browser console:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/5/b5613fb51c49cea3777f9fa91d03989257cc3442.png)

and the graph bugs out like this:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/7/b70977722d96bb29ccd3668b2dd13c26b8164a91.png)

If I reset the data, it still does not appear correctly. I believe the graph becomes in a permanent 'crashed' state that doesn't get fixed even if I refresh the page. It only fixes itself if I open a new window.

From the research I've done, this likely has more to do with changing the scales while hovering with the crosshair than changing the data prop.

I've tried solutions where I disable the crosshair then re-enable it after the options have been set. I've also tried solutions where I disable/reenable the 'display' prop of the component. I've even tried adding sleep timers after disabling/before reenabling the crosshair/display prop, but in all cases there are so many bindings in the previously-existing view that it's difficult to get the timing right of when to disable/re-enable them (due to bindings triggering multiple times/in different orders), and I'm not sure how well it'd work even if the binding hierarchy was simpler.

I've also tried using the this.destroy()/this.reset() functions from the ChartJS API in the Proxy object you added to handle cases where the chart crashes, but I don't think I can use them in the module. In any case, I'm not sure it'd work even if I could.

I found a workaround where, in the relatively rare case when the specific operations causing it to crash are triggered, I navigate to another tab in the tab container the chart exists in before all the props are updated. This will work for now but it obviously isn't ideal.

Do you have any suggestions on how I might get around this issue? I expect it's more an issue with ChartJS/the crosshair plugin being finnicky than the module itself, but if you happened to have run into this issue before, any advice would be greatly appreciated. Thanks.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [February 12, 2025, 5:21pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/99 "2025-02-12T17:21:54Z")

</div>

> [@andrew\_sf](#):
>
> I expect it's more an issue with ChartJS/the crosshair plugin being finnicky than the module itself, but if you happened to have run into this issue before, any advice would be greatly appreciated. Thanks.

Yeah the crosshair plugin is pretty garbage, I’d really like to remove it lol. However I won’t do that until there’s an appropriate replacement option available.

> [@andrew\_sf](#):
>
> I've also tried using the this.destroy()/this.reset() functions from the ChartJS API in the Proxy object you added to handle cases where the chart crashes, but I don't think I can use them in the module. In any case, I'm not sure it'd work even if I could.

Try enabling the `redraw` property on the chart, it basically does a destroy and reset anytime the properties change. It’s a pretty heavy handed solution, but it usually fixes these weird issues.

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/d/d/ddf78f89b759859e50e42074836ec6c1ee6e53d6.png)

---

<div class="post-metadata">

**Author:** ![ryan.white](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ryan.white/32/73193_2.png) [@ryan.white](https://forum.inductiveautomation.com/u/ryan.white)\
**Post date:** [February 13, 2025, 6:08pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/100 "2025-02-13T18:08:06Z")

</div>

Edit: Looked into this some more and it appears to be a custom implementation on the examples site. I think that means it would have to be added to the module to be available. 😕

Some sample charts from chart.js have an `actions` configuration that controls buttons that appear below the chart area. Is this configuration accessible through the chart.js component and if so, how would we access it?

> **[Combo bar/line | Chart.js](https://www.chartjs.org/docs/latest/samples/other-charts/combo-bar-line.html)**
>
> Open source HTML5 Charts for your website

My end use case is to add buttons to shift the bounds of the current graph forward/back by small and large increments, and I'd rather all this be done with javascript on the actual component. I'm trying keep the actions feeling more responsive and avoid the round trip to the gateway.

Ideally I'd like to also control size, position, and icons of these buttons if possible.

[Previous page](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618.md?page=4)

[Next page](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618.md?page=6)
