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

<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 24, 2025, 5:49pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/141 "2025-02-24T17:49:57Z")

</div>

Another option is to build a WebDev endpoint for accessing the data; have the client query the endpoint and put the results into the Chart's datasets.

I haven't tested it (I don't do much with the WebDev module), but I would imagine taking the query load out of the Perspective session would be a good thing.

---

<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 24, 2025, 6:23pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/142 "2025-02-24T18:23:55Z")

</div>

Since I have no concept of moderation:  
15 pens, 1 week span, 5 second interval size, decimation turned off.  
 ![Maxxed Embr](https://us1.discourse-cdn.com/inductiveautomation/original/3X/a/1/a142799725f006c4c05bf716bf97605d50743960.png)

Panning immediately sends my browser to 100% cpu.

 ![Maxxed Embr2](https://us1.discourse-cdn.com/inductiveautomation/original/3X/e/c/ecf585735d18578042d971eee3f003c1db395276.png)

BUT! if you are zoomed in to ~ 1 day wide span, the delay is not that bad. I would say its usable.

Response from fellow engineer:  
 ![maxed embr3](https://us1.discourse-cdn.com/inductiveautomation/original/3X/1/a/1a4bcad1d322d757e3e73fb55eb52d7d7f60609f.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:** [February 24, 2025, 6:28pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/143 "2025-02-24T18:28:12Z")

</div>

Have you seen this page?

> **[Performance | Chart.js](https://www.chartjs.org/docs/latest/general/performance.html)**
>
> Open source HTML5 Charts for your website

`parsing: false` and `normalized: true` are the big ones, plus disabling animations with `options.animation: false`.

---

<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 24, 2025, 6:44pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/144 "2025-02-24T18:44:23Z")

</div>

> [@bmusson](#):
>
> `parsing: false` and `normalized: true`

These are already set this way, I was just curious how many points I could actually have on the graph at once. Again, miles ahead of anything vanilla.

Turning on decimation makes it pretty snappy again, even when using 5k points per line.

---

<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 24, 2025, 9:50pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/145 "2025-02-24T21:50:30Z")

</div>

Not sure if you can control this but the tooltip in the 'Add Object Member' menu for `options.plugins.tooltip.external` takes up the entire width of my screen:

![Lonk tooltip](https://us1.discourse-cdn.com/inductiveautomation/optimized/3X/a/0/a03a9413b52537ce94ebd0bbef003771ac241959_2_690x38.png)

 ![Lonk tooltip2](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/7/b7ebdb363586d4e7917e2d561b76ee226ab33414.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:** [February 24, 2025, 10:39pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/146 "2025-02-24T22:39:31Z")

</div>

Yup, it'd be nice if it wrapped on its own, but we can add some new lines in there.

The best place to report these kind of issues is on [GitHub Issues](https://github.com/mussonindustrial/embr/issues); it's much easier to see what's outstanding and to get flagged when issues are resolved.

Also, PRs are absolutely welcome.

---

<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 25, 2025, 9:35pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/147 "2025-02-25T21:35:25Z")

</div>

Opened [Issue #239](https://github.com/mussonindustrial/embr/issues/239) and I'm going a little ham on seeing what tooltips are overly wide.

Its also interesting, as it seems that tooltips for properties in the tree are auto wrapped?  
 ![wrapped tooltip](https://us1.discourse-cdn.com/inductiveautomation/original/3X/3/0/3064103313943aae1705d6d724f38265fbd48d14.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 26, 2025, 6:18pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/148 "2025-02-26T18:18:34Z")

</div>

Data loading via JS proxy seems pretty heavyweight, at least on our gateway. How many cores do you have on your gateway when testing/developing the module?

Data fetch from our db is taking about 1/5th to 1/10th the time it takes to actually load the data into the chart via JS proxy. (200 ms fetch vs 2s js execute)

Did you do any performance comparison between having an `append` method on the chart component vs doing the append via a JS Proxy script?

---

<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 26, 2025, 10:48pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/149 "2025-02-26T22:48:37Z")

</div>

> [@ryan.white](#):
>
> Data loading via JS proxy seems pretty heavyweight, at least on our gateway.

Are you seeing load on the gateway, or on the client?

> [@ryan.white](#):
>
> How many cores do you have on your gateway when testing/developing the module?

Nothing crazy, an Intel Core Ultra 7 165H and 96GB RAM.

> [@ryan.white](#):
>
> Did you do any performance comparison between having an `append` method on the chart component vs doing the append via a JS Proxy script?

The only timing difference between a dedicated component method, and building your own using the proxy methods is construction of the JavaScript function from a string, which is pretty negligible for "one-off" function calls.

Either way, your array of `data` points needs to be serialized and sent over the WebSocket. How many points are you trying to send at once?

```kotlin
// https://github.com/mussonindustrial/embr/blob/240bc46220477d5aabab6e5d0fc17499cdd8ef4a/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/component/ComponentDelegateJavaScriptProxy.kt
private fun runJavaScript(
    function: String,
    args: PyDictionary?,
    callback: PyFunction? = null,
): CompletableFuture<PyObject?> {
  ...
  delegate.fireEvent(MESSAGE_RUN, JavaScriptRunMsg(id, property, function, args).getPayload())
  ...
}

class JavaScriptRunMsg(
    private val id: String,
    private val property: String,
    private val function: String,
    private val args: PyDictionary?
) {
    fun getPayload(): JsonObject {
        return JsonObject().apply {
            addProperty("id", id)
            addProperty("property", property)
            addProperty("function", function)
            add("args", TypeUtilities.pyToGson(args))
        }
    }
}

```

---

<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 26, 2025, 11:02pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/150 "2025-02-26T23:02:30Z")

</div>

> [@bmusson](#):
>
> Are you seeing load on the gateway

Gateway side.

> [@bmusson](#):
>
> How many points are you trying to send at once?

My current batch logic executes in chunks of 10k or less, but runs as many times as needed for the total amount of points.

Currently somewhere in the neighborhood of ~40k total points sticks our gateway cpu to ~27% from the designer session and an active web browser session

Anything higher than 150k total seems to max our gateway cpu and I'll get drift warnings if I have it running at a set rate.

I believe we are running 2 cores 4 threads on our VM ( small deployment overall, \<25 machines total, most are pulling general state info and alarms and nothing else). It may just be that I'm trying to do too much with too little.

> [@bmusson](#):
>
> be serialized

I'm guessing this is probably what is chewing through the cpu time the most.

---

<div class="post-metadata">

**Author:** ![Alex\_Godfrey](https://avatars.discourse-cdn.com/v4/letter/a/e56c9b/32.png) [@Alex\_Godfrey](https://forum.inductiveautomation.com/u/Alex_Godfrey)\
**Post date:** [February 27, 2025, 12:56am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/151 "2025-02-27T00:56:30Z")

</div>

Hi Ben,

I've just installed the Embr-Charts module on an edge platform but I'm getting the licensing error in the snippet below. As this is a free module i didn't think there would be a option to buy a license. Also, the edge platform already has a valid license running. Would this issue be in the chart module or in the Edge?

'Faulted. not eligible for use in Ignition Edge unless licensed'

 ![Screenshot 2025-02-27 105021](https://us1.discourse-cdn.com/inductiveautomation/original/3X/9/d/9d91431dd6deb8c65d41e699877d71b2fdd1ffeb.png)

Thank you in advance for your assistance.  
Alex Godfrey.

---

<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 27, 2025, 1:16am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/152 "2025-02-27T01:16:30Z")

</div>

@Alex_Godfrey embr charts is not one of the permitted third party modules for edge.

> **[Ignition Edge | Ignition User Manual](https://docs.inductiveautomation.com/docs/8.1/other-editions/ignition-edge)**
>
> A lightweight, lean version of Ignition with software solutions designed for devices used in the field and edge of the network.

From the docs:

> Edge's core functionality and products largely determine which modules run on the platform. Third-party modules will not run on an Edge Gateway, with the exception of any third-party module explicitly stated by a product, such as the [Edge IIoT](https://docs.inductiveautomation.com/docs/8.1/other-editions/ignition-edge/edge-iiot) product.

---

<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 27, 2025, 4:15am UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/153 "2025-02-27T04:15:31Z")

</div>

> [@Alex\_Godfrey](#):
>
> Would this issue be in the chart module or in the Edge?

It’s an Edge issue. There’s no technical reason why the module won’t work on Edge, except for IA’s restrictive Edge licensing model.

While there’s been some movement on 3rd party modules for Edge, I don’t expect open-source modules to ever be approved and it’s not something I’m interested in pursuing.

---

<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:** [February 27, 2025, 12:36pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/154 "2025-02-27T12:36:58Z")

</div>

> [@bmusson](#):
>
> I don’t expect open-source modules to ever be approved

Indeed.

Module IDs associated with open source projects could easily be abused by outsiders to violate Edge's license limitations, like DB access.

---

<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 27, 2025, 6:01pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/155 "2025-02-27T18:01:52Z")

</div>

After further testing the actual load seems to be coming from the history query, NOT the javascript proxy.

The proxy does seem to take a while to serialize the larger datasets but the data fetch seems to be what is hammering my gateway.

I'll have to see if I can convince our IT to bump the number of cores for the VM.

---

<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:** [February 27, 2025, 6:38pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/156 "2025-02-27T18:38:03Z")

</div>

Are you running the DB in the same VM as Ignition? Tsk, tsk.

---

<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 27, 2025, 6:44pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/157 "2025-02-27T18:44:03Z")

</div>

> [@pturmel](#):
>
> Tsk, tsk.

Actually this dev server is set up mostly™ correct (Same for our prod). Only Ignition lives in this VM, DB is a separate VM (likely same physical server, IT hasn't really given details.) No idea if they provisioned the VM correctly though.

---

<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 27, 2025, 6:53pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/158 "2025-02-27T18:53:11Z")

</div>

> [@ryan.white](#):
>
> After further testing the actual load seems to be coming from the history query, NOT the javascript proxy.

Thanks for the update.

One thing all the Embr modules could benefit from is improved trace logging, there’s not much information emitted to help with debugging these kinds of issues.

---

<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 28, 2025, 7:49pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/159 "2025-02-28T19:49:23Z")

</div>

Possible challenge for you if you want it: Tooltips spread across multiple vertically stacked scales. I'm finding a lot of requests online for the ability to be added to the tooltips plugin, but no actual solutions.

I'm plugging away at an approach I think will work by using the `external` callback to create as many HTML tooltips as there are used scales. (While typing this I just thought of the issue of determining if the scale is actually vertically stacked or grouped with another scale. Problems for later)

> <https://github.com/chartjs/Chart.js/issues/12007>
>
> \### Feature Proposal
> 
> Couldn't find any exemple, or figure out if it's actually …possible by reading the documentation.
> But it would be nice if we could split the tooltip for each stacked axis.
> 
> Here is the current results:
> !\[Image\](https://github.com/user-attachments/assets/c4510228-b55b-4dc0-af25-099846ebc2fe)
> 
> Here is what I would like:
> !\[Image\](https://github.com/user-attachments/assets/130b0ac2-7a51-4a03-bebf-b9822ed7ac68)
> 
> \### Possible Implementation
> 
> \_No response\_

edit: I might be on to something. And because they are HTML elements I can assign style classes to them.

> **Multi Tool Tip**
>
> ![multiTooltip](https://us1.discourse-cdn.com/inductiveautomation/original/3X/2/4/24bd143c2dba5f44cab48bd1d71e09790f84d2ca.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:** [March 3, 2025, 11:09pm UTC](https://forum.inductiveautomation.com/t/musson-industrial-s-embr-charts-module/91618/160 "2025-03-03T23:09:20Z")

</div>

Alright, I managed to get multiple tooltips working, but I'm not entirely happy on the method used to create the table of items for each tooltip. This pass does not have titles or pre/post body text.

I based it on the HTML tooltip example for Chart.js which removes all the table row items from the DOM then appends new ones.

I tried doing modifications to the text value of the rows instead of replacing the items but I would lose the colored square for each item.

My math for determining if I should flip which side of the x-trace line the tooltip also leaves something to be desired.

This goes into the `external` property of `props.plugins.tooltip`.

> **External Multiple Tooltip Handler**
>
> ```javascript
> (context) => {
>  
> const getOrCreateTooltip = (chart, axisId) => {
> let toolTipEl = chart.canvas.parentNode.querySelector('div #tt_'+axisId);
> 
> if (!toolTipEl) {
> toolTipEl = document.createElement('div');
> toolTipEl.id = "tt_" + axisId;
> toolTipEl.classList.add('psc-trendsurfer\/Tooltip\/container')
> toolTipEl.style.opacity = 1;
> toolTipEl.style.pointerEvents = 'none';
> toolTipEl.style.position = 'absolute';
> toolTipEl.style.transform = 'translate(-50%, 0)';
> toolTipEl.style.transition = 'all .1s ease';
> toolTipEl.style.fontSize = '12px';
> 
> chart.canvas.parentNode.appendChild(toolTipEl);
> };
> 
> return toolTipEl;
> };
>   
> 	
> const createTooltipItem = (colors, itemText, options) => {
>   
> const colorBlock = document.createElement('span');
> colorBlock.classList.add('psc-trendsurfer\/Tooltip\/itemIndicator');
> colorBlock.style.background = colors.backgroundColor;
> colorBlock.style.borderColor = colors.borderColor;
> colorBlock.style.borderWidth = '2px';
> colorBlock.style.marginRight = '10px';
> colorBlock.style.height = options.boxHeight + 'px' || '10px';
> colorBlock.style.width = options.boxWidth + 'px' || '10px';
> colorBlock.style.display = 'inline-block';
> 
> const tr = document.createElement('tr');
> tr.style.backgroundColor = 'inherit';
> tr.style.borderWidth = 0;
> tr.classList.add('psc-trendsurfer\/Tooltip\/item');
> 
> const td = document.createElement('td');
> td.style.borderWidth = 0;
> 
> const text = document.createTextNode(itemText);
> 
> td.appendChild(colorBlock);
> td.appendChild(text);
> tr.appendChild(td);
> 		
> return tr;
> };
> const getCssStyle = (element, prop) => {
> return window.getComputedStyle(element, null).getPropertyValue(prop);
> }
> 	
> const getTextWidth = (text, font) => {
> const canvas = context.chart.canvas;
> const ctx = canvas.getContext('2d');
> ctx.font = font;
> const metrics = ctx.measureText(text);
> return metrics.width;
> }
> 	
> const getCanvasFont = (el) => {
> const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
> const fontSize = getCssStyle(el, 'font-size') || '16px';
> const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
> 
> return `${fontWeight} ${fontSize} ${fontFamily}`;
> }
> 	
> const createTooltipHeader = (titleLines) => {
> 	
> const tableHead = document.createElement('thead');
> 		
> titleLines.forEach(title => {
> const tr = document.createElement('tr');
> tr.style.borderWidth = 0;
> 
> const th = document.createElement('th');
> th.style.borderWidth = 0;
> const text = document.createTextNode(title);
> 
> th.appendChild(text);
> tr.appendChild(th);
> tableHead.appendChild(tr);
> });
> return tableHead; 
>   
> };
>   
> const {chart, tooltip} = context;
> const usedScales = [];
> const itemsByParent = {};
> var widestTTItem = 0;
> let widthPadding = 0;
> 	
> if (!tooltip || !tooltip.dataPoints) {return};
> 	
> tooltip.dataPoints.forEach( (element, ndx) => {
> let scaleId = element.dataset.yAxisID || 'y';
> let colors = tooltip.labelColors[ndx];
> let itemText = tooltip.body[ndx].lines;
> 		
> let itemWidth = getTextWidth(itemText, getCanvasFont(document.body));
> itemWidth + tooltip.options.boxWidth + 4 + 10
> if (itemWidth > widestTTItem) {widestTTItem = itemWidth;};
> 		
> if (!usedScales.includes(scaleId)) {
> usedScales.push(scaleId);
> };
> 		
> if (!(scaleId in itemsByParent)) {
> itemsByParent[scaleId] = [];
> };
> let ttItem = createTooltipItem(colors, itemText, tooltip.options);
> itemsByParent[scaleId].push(ttItem);
> 
> });
> 	
> var flipTipSide = false;
> const caretSize = tooltip.options.caretSize;
> const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas;
> const maxWidth = widestTTItem + (2 * tooltip.options.padding) + 5
> 	
> // Final cleanup, sizing, and placment loop through the tooltip boxes
> usedScales.forEach( (element, ndx) => {
> 		
> let toolTipEl = getOrCreateTooltip(chart, element);
> toolTipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
> toolTipEl.style.minWidth = maxWidth + 'px'
> //toolTipEl.style.width = maxWidth + 'px';
> 
> // Get or create tooltip item table
> let ttTable = toolTipEl.querySelector("table");
> if (!ttTable) {
> ttTable = document.createElement("table");
> ttTable.classList.add("psc-trendsurfer\/Tooltip\/table");
> toolTipEl.append(ttTable);
> };
> 		
> // Get or create tooltip table body
> let tBody = ttTable.querySelector('tbody');
> if (!tBody) {
> tBody = document.createElement('tbody');
> ttTable.append(tBody);
> };
> 		
> let scale = chart.scales[element] || null;
> if (!scale) {return};
> 		
> // Drop all children of table
> tBody.replaceChildren();
> 		
> 		
> // Hide tooltip if not being shown
> if (tooltip.opacity === 0) {
> toolTipEl.style.opacity = 0;
> return;
> };
> 		
> // Then add all new children
> let newChildren = itemsByParent[element];
> tBody.replaceChildren(...newChildren);
> 
> // Determine tooltip placement side (left or right of xTrace line)
> let expectedX = positionX + tooltip.caretX + caretSize;
> let expectedY = (scale.top + 20);
> let expectedRight = expectedX + maxWidth + 5;
> 		
> toolTipEl.style.opacity = 1;
> toolTipEl.style.top = (scale.top + 20) + 'px';
> 		
> 		
> if (expectedRight >= chart.chartArea.right) {
> flipTipSide = true;
> return;
> };
> 		
> toolTipEl.style.left = positionX + tooltip.caretX + caretSize + (maxWidth/2) + 'px';
> });
> 	
> if (flipTipSide) {
> usedScales.forEach( (element) => {
> const toolTipEl = chart.canvas.parentNode.querySelector('div #tt_'+ element);
> toolTipEl.style.left = positionX + tooltip.caretX - caretSize - (maxWidth/2) + 'px';
> })};
> }
> 
> ```

> **Tooltip Example**
>
> ![multiTooltip.png](https://us1.discourse-cdn.com/inductiveautomation/original/3X/4/c/4c3a35b3e8261d465e6987384ca4edbeb6300010.png)

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

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