Musson Industrial’s Embr-Charts Module

@bmusson How would I go about passing the context? of what was clicked on the chart to another perspective component?

(e.g. If I click on a slice of a pie chart I would get the data.labels[x] and and data.datasets[0].data[x] properties of what was clicked. In a standard component I would expect that to be in the event object from an onClick event but I'm guessing that's not the case here.)

What I'm trying to accomplish is to send that to a bar chart that breaks that slice 'category' down into sub-categories.

edit: maybe it's something to do with options.events?

edit2: I've managed to make a little progress and uncovered the pieces I need, but can't send them somewhere else in the component.

// props.options.onClick
(event, elements, chart) => { 
		if (elements.length > 0) {
			const payload = {
				datasetIndex: elements[0].datasetIndex,
				index: elements[0].index
			};
			console.log('Sending payload:', payload);

			// this component.notify is the closest thing I could get to 
			// system.perspective.sendMessage() but I think it works in a 
			// different way because I keep getting an error:
			// level: WARNING
			// message: "\"ComponentStore\" cannot notify \"chartClick\" state change. No registered listeners or handler."

			perspective.context.component.notify('chartClick', payload);
		} else {
			console.log('No elements clicked');
		}
	}

closer...

It probably comes down to determining the 'correct' listener name?

Looking in the contexts, by adding a an action to the onClick mouse event in the designer, a new item is added to the perspective.context.component.domEvents node called onClick. That seems to be hard bound to whatever function is defined in the designer but there seems to be at least 1 layer of abstraction in there.

Completely misunderstood your goal here. I wouldn't be surprised at all if the entirety of the messaging system in perspective is entirely gateway side with none of the actual messaging occuring at the client level, which is where your javascript is running.

that's what I was afraid of. Until I can figure out how to get something from the chart events out to a session/page/view message handler, I'm afraid I'll have to use Apex charts for this one.

as a side note to your edit, listeners in the component don't seem to be the same as listeners in the project and seem to be isolated, or as you said client-side and the same goes for perspective.context.component.notify()

I appreciate your reply.

If you are using chartjs for the second chart, you might be able to define a custom event listener on that chart and pass it a CustomEvent via dispatchEvent.

You could use that to transfer the necessary data from the main chart to the secondary chart. It would also bypass the round trip through the gateway.

Basic flow would be to add an item to the secondary chart's props.plugins with a definition for install that defines and attaches the event listener to the chart canvas. Most basic listener would look for new data in the event and shove that data into the data.datasets of the chart.

In your main chart's props.options.onClick definition, you would create a new CustomEvent that would contain the data you want to send, and maybe some other info to drive what the chart should do, and then pass this CustomEvent to the eventListener on the secondary chart.

Including this which shows basic creation and attaching of eventListeners:

1 Like

Your only option currently is to write the payload to the property tree, then use an onChange script.

Like this:

// props.options.onClick
(event, elements, chart) => { 
  if (elements.length > 0) {
    const payload = {
      datasetIndex: elements[0].datasetIndex,
      index: elements[0].index
    }
    
    // Ensure that you add a custom property on the chart component
    // named 'clickPayload'.
    this.store.custom.write('clickPayload', payload)

  } else {
    console.log('No elements clicked')
  }
}

This.

It would be nice to fire messages from client side JavaScript through... let me mull it over.

1 Like

messaginTs

That was fast :laughing:. Guess I should hurry my attempt to implement DecimalFormat in js.

1 Like

Sometime you get lucky, and things are easy :slight_smile:

The perspective global object now includes a function for firing gateway-side messages.

perspective.sendMessage(type: string, payload: JsObject, scope = 'page') => void

Your example can now become:

(event, elements, chart) => { 
  if (elements.length > 0) {
    const payload = {
      datasetIndex: elements[0].datasetIndex,
      index: elements[0].index
    }

    perspective.sendMessage('chartClicked', payload, 'view')

  } else {
    console.log('No elements clicked')
  }
}

This is also available in Periscope.

7 Likes

Not sure if it helps you at all, but I have elements that when clicked on, it just writes the data I want to a custom property on the view. I also send the index and put a change event script on that property. I use the change event to open popups with the context data as params. Then reset the index.
It’s probably the “poor-boy” way to do it, but it works.

I would have gone with this.store.custom.write() but now with the update I'm using the sendMessage() function and I've already set up the charts. I did have the Apex charts set up, but they are so slow compared to Embr.

2025-03-07_08-24-49

5 Likes

Awesome :grinning_face:

Also, you might find this plugin useful:

1 Like

Totally missed the this.store.custom.write() function. May have been in a newer version than what I have.
I was doing it this way: (i think Ben helped me out with this)

(event, elements, chart) => {
    if (elements.length !== 0) {
        const selected = elements.map((e) => ({
        datasetIndex: e.datasetIndex,
        index: e.index,
        }));
        const data = this.props.data.datasets[selected[0].datasetIndex]
        const selectedEvent = {
            'index': selected[0].datasetIndex,
            'data': data
        }

        const view = perspective.context.view;
        view.custom.write("selectedEvent", selectedEvent);
    }
}
2 Likes

is that included in your module? I could set options.scales.x.type to heirarchical and then bring in the data dictionary like the example?

Yup, exactly. If you run into issues just post them here.

2 Likes

just got a chance to play with this again, I'm getting this error:

level: LEVEL_ERROR
message: "Error: \"hierarchical\" is not a registered scale."
line_number: 121
source: "https://...:8043/res/perspective/js/react-dom-18.2.0.js"

props.data JSON

{
  "labels": [
    "A"
  ],
  "datasets": [
    {
      "data": [],
      "label": "Test",
      "tree": [
        1
      ]
    }
  ]
}

props.options.scales JSON

{
  "x": {
    "type": "hierarchical"
  },
  "y": {
    "type": "linear"
  }
}

Looking at the source code it looks like that scale is added to the plugin registry but not specifically registered? Could just be a nuance with how external plugins are registered.

import { HierarchicalScale } from 'chartjs-plugin-hierarchical'
Chart.registry.addElements(HierarchicalScale)

vs

import { TimestackScale } from 'chartjs-scale-timestack'
Chart.register(TimestackScale)

Edit: After looking at the implementation instructions from the plugin source I think this is the cause. I've opened issue #248 on the github for this.

2 Likes

Example View JSON
{
  "custom": {},
  "params": {},
  "props": {},
  "root": {
    "children": [
      {
        "meta": {
          "name": "Chartjs"
        },
        "position": {
          "basis": "300px"
        },
        "props": {
          "data": {
            "datasets": [
              {
                "label": "Test",
                "tree": [
                  1,
                  {
                    "children": [
                      3,
                      {
                        "children": [
                          4.1,
                          4.2
                        ],
                        "value": 4
                      },
                      5
                    ],
                    "value": 2
                  },
                  {
                    "children": [
                      7,
                      8,
                      9,
                      10
                    ],
                    "value": 6
                  },
                  11
                ]
              }
            ],
            "labels": [
              "A",
              {
                "children": [
                  "B1.1",
                  {
                    "children": [
                      "B1.2.1",
                      "B1.2.2"
                    ],
                    "label": "B1.2"
                  },
                  "B1.3"
                ],
                "expand": false,
                "label": "B1"
              },
              {
                "children": [
                  "C1.1",
                  "C1.2",
                  "C1.3",
                  "C1.4"
                ],
                "label": "C1"
              },
              "D"
            ]
          },
          "options": {
            "animations": {
              "numbers": {
                "properties": [
                  "borderWidth",
                  "radius",
                  "tension",
                  "radius",
                  "tension"
                ]
              }
            },
            "layout": {
              "autoPadding": false,
              "padding": {
                "bottom": 60
              }
            },
            "scales": {
              "x": {
                "type": "hierarchical"
              },
              "y": {
                "type": "linear"
              }
            }
          },
          "type": "bar"
        },
        "type": "embr.chart.chart-js"
      }
    ],
    "meta": {
      "name": "root"
    },
    "props": {
      "direction": "column"
    },
    "type": "ia.container.flex"
  }
}
6 Likes

Thanks so much, I really appreciate your quick attention to this!

1 Like

A Question For The Class

For my own use, I'd like to include an ApexCharts component in Embr-Charts.

The Good

It would have all the nice JavaScripting features from the Chart.js component (more powerful events/method access), as well as reduced overhead compared to the Kyvis-Labs version (their component does some property processing that I think is silly).

The Bad

ApexCharts is bundled in a way that makes it impossible to have two different versions running simultaneously. Therefore you would not be able to have the Kyvis-Labs ApexCharts module installed alongside Embr-Charts.

The Embr-Charts version of the component would not be a "drop in replacement" for existing projects.

Options

  1. Include an ApexCharts component in Embr-Charts, and ignore the consequences.
  2. Include an ApexCharts component in Embr-Charts, but do not register it if the Kyvis-Labs module is installed (Kyvis-Labs component gets priority).
  3. Create a whole separate module for the Embr version of the component.
  4. Include a fork of Kyvis-Labs component in Embr-Charts.
  5. Try to become a regular contributor to the Kyvis-Labs module, in order to keep the ApexCharts dependencies in sync between both modules.

All options are annoying/bad in their own unique ways :tada:.

#3 seems like the simplest/best option.

1 Like

I recommend #5, if they'll take your pull requests, otherwise #4.

My Tag Report Utility project currently allows switching between IA's TS Chart and synchronized Apex charts, to compare performance. I intend to add Embr as an option, with switching between any of the three (if installed). Not being able to load Apex alongside Embr would break that.

If the modules clash, some developers would choose Apex, and others would choose Embr, and there would be no migration path between them if one wanted to switch for any reason. That would be a disaster, I think.

If you do #4, please make ensure your changes don't interfere with it being a drop-in replacement for the Kyvis Labs version.

4 Likes

Yeah, I agree that needs to be avoided at all costs.

Ultimately if there's not a satisfactory solution that's good for the community as a whole, we will just keep it to ourselves :man_shrugging:.

2 Likes