Musson Industrial’s Embr-Charts Module

Here's the current docs for scriptable options: Scriptable Options | Musson Industrial

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:

// 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 :man_shrugging:. 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.

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:

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

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

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

Version 2.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.

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 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:

# 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:

# 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.

Third-Party Module Showcase

We're now listed on the Third-Party Module Showcase!

Download

Quick-link: Embr-Charts-2.0.0.modl

As always, the latest release can be found here.

6 Likes

Wow! This is impressive.

1 Like

For the nerds:

Client-side: ComponentDelegateJavaScriptProxy.ts

Gateway-side: ComponentDelegateJavaScriptProxy.kt

3 Likes

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.

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 :man_shrugging:)

3 Likes

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

1 Like

Documentation has been updated, with several examples.

1 Like

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

Smoothie Charts

Smoothie Charts is a very simple charting library for streaming data (think sparklines).

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 to update the state of the chart.

// 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, like so:

# 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 for more information.

# 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 (2.0 MB)

6 Likes

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.
type ChartDataPoint = number | { value: number; timestamp: number }
type ChartSeriesData = ChartDataPoint | ChartDataPoint[]
appendData: (values: ChartSeriesData[]) => void

Examples

# (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)
# (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
{
  "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 (2.0 MB)

5 Likes

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 :wink:

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

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?

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.

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:

  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

1 Like

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

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

{
  "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"
  }
}

Cross linking the resolution from here.

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.

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:

and the graph bugs out like this:

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.

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.

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.

2 Likes

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. :confused:

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?

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.