# \[BUG?\] Perspective Power Chart Bug - Embr-Chart as a Replacement?

**URL:** <https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084>\
**Category:** Ignition\
**Tags:** perspective\
**Created:** [February 10, 2025, 5:42am UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084 "2025-02-10T05:42:11Z")\
**Posts on this page:** 11\
**Page:** 4

<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 4, 2025, 11:44pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/61 "2025-03-04T23:44:42Z")

</div>

It is something that also annoys me, so I do plan on seeing if I can override the placement without getting the weird thrashing behavior. It looks like there is a `labelOffset` option availble, will just need to see if I can configure a callback on it.

---

<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:** [March 5, 2025, 1:16am UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/62 "2025-03-05T01:16:06Z")

</div>

> [@ryan.white](#):
>
> Note the white space to the right of the last tick that isn't present in the screenshot in my earlier post.

What does it look like with `align: inner`? I would expect that to align center for all the labels but with the “edge” labels aligned to the start/end of the chart.

---

<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 5, 2025, 1:33am UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/63 "2025-03-05T01:33:35Z")

</div>

IIRC it gives me the same issue, I'll have to check tomorrow.

Update: `inner` does keep the labels inside the bounds but the end labels start offset and then jump to the proper position when the tick is not the last tick on that side of the axis. Overall I didn't care for the behavior.

 ![axisLabelOffset](https://us1.discourse-cdn.com/inductiveautomation/original/3X/2/e/2e820133e0e28379f0556bfeadeef81236938ff9.png)

I'll probably just aim to mimic citect and chop the label

---

<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 5, 2025, 6:24pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/64 "2025-03-05T18:24:56Z")

</div>

> [@ryan.white](#):
>
> You think I should right align the numeric values while I'm at it?

I have made the joyous discovery that vertically aligning in regards to the decimal (or whatever separator character you use) is not anything that can 'easily' be done.

I'll probably have to loop through all my formatted values in a box to see which one is the longest and pad the rest accordingly. Which sucks because that is yet another loop in my tooltip logic.

The other option I've seen is to split the float at the separator and place the integer value in one `td` and place the separator + decimal points in the next `td` element.

> [@ryan.white](#):
>
> formats as well.

Ignition uses java's DecimalFormat under the hood. Which does not exist in any capacity in javascript. Custom functions all around! 😭

---

<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:** [March 5, 2025, 9:05pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/65 "2025-03-05T21:05:37Z")

</div>

> [@ryan.white](#):
>
> Ignition uses java's DecimalFormat under the hood. Which does not exist in any capacity in javascript.

If someone can provide a JavaScript function that converts a number to a string using the DecimalFormat convention, I can add it under global `perspective` namespace.

How about `perspective.string.numberFormat(number, pattern)`?

---

<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 5, 2025, 9:10pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/66 "2025-03-05T21:10:36Z")

</div>

Best I could find was someone's 15 year old conversion of java's DecimalFormat to javascript at the time.

> [@bmusson](#):
>
> global `perspective` namespace

This would need to be accessible in the chart js context, since the tooltip is running in the `external` callback on the tooltip plugin. I'm also overriding the value formatter at the same time.

---

<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:** [March 5, 2025, 9:15pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/67 "2025-03-05T21:15:09Z")

</div>

> [@ryan.white](#):
>
> This would need to be accessible in the chart js context, since the tooltip is running in the `external` callback on the tooltip plugin.

Yup, it absolutely would be. The global objects are added to the scope when the string in the property tree is converted into a proper function. It’s how the current `perspective.context` stuff is available.

> <https://github.com/mussonindustrial/embr/blob/240bc46220477d5aabab6e5d0fc17499cdd8ef4a/modules/charts/web/src/util/getScriptTransform/getScriptTransform.ts>

---

<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 5, 2025, 9:26pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/68 "2025-03-05T21:26:19Z")

</div>

Ah right, brain saw the `perspective` and assumed `system.perspective`. In that case that would absolutely be helpful, as I imagine others would want to be able to pass tag value formatting info/pattterns into the JS context and be able to use it.

---

<div class="post-metadata">

**Author:** ![lrose](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/lrose/32/62724_2.png) [@lrose](https://forum.inductiveautomation.com/u/lrose)\
**Post date:** [March 5, 2025, 9:36pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/69 "2025-03-05T21:36:15Z")

</div>

How about Intl.NumberFormat() ?

Something like:

```python
function numberFormat(number,local = "en-US",options = {}) {
   if (isNaN(number)) {
       return 'Invalid Input'; //Some more appropriate error handling obviously
    }

    const formattedNumber = new Intl.NumberFormat(local,options).format(number)
    return formattedNumber
}

```

Input:

```python
console.log(numberFormat(12.3457))
console.log(numberFormat(12.3457,"da-DE",{style:"unit", unit:"liter", unitDisplay:"short", maximumFractionDigits:2}))
console.log(numberFormat(12.3457,"da-DE",{style:"unit", unit:"liter", unitDisplay:"long", maximumFractionDigits:2}))

```

Output:

```python
 >"12.346"
 >"12,35 l"
 >"12,35 liter"

```

This would allow you to use local to, it's not as convenient as just giving it a pattern, but is arguably more flexible.

Of course you could also do something like this:

_Not my code, and not tested so take with a grain of salt_

```python
function formatDecimal(number, pattern) {
  if (isNaN(number)) {
    return 'Invalid Number';
  }

  const parts = pattern.split('.');
  let integerPart = parts[0];
  const decimalPart = parts[1] || '';

  const numberString = number.toFixed(decimalPart.length);
  const [integerNum, decimalNum] = numberString.split('.');

  let formattedInteger = '';
  let integerIndex = integerNum.length - 1;
  let patternIndex = integerPart.length - 1;

  while (integerIndex >= 0) {
    formattedInteger = integerNum[integerIndex] + formattedInteger;
    integerIndex--;
    patternIndex--;

    if (patternIndex >= 0 && integerPart[patternIndex] === ',') {
      formattedInteger = ',' + formattedInteger;
    }
  }

  const formattedDecimal = decimalNum ? '.' + decimalNum : '';

  return formattedInteger + formattedDecimal;
}

```

---

<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 5, 2025, 9:54pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/70 "2025-03-05T21:54:15Z")

</div>

> [@lrose](#):
>
> How about Intl.NumberFormat() ?

The goal was to be able to take the format defined on the tag (or somewhere else in Ignition) and have it apply to the chart display labels.

I'm trying to make configuring tags for charts of this type as easy as possible (minimal js setup other than the main driving logic I have).

Its the difference between configuring an `Int.NumberFormat` for every tag or format that I'm using for the tags and then trying to correlate which dataset gets which `Intl.NumberFormat` vs just pulling the number format as defined by base Ignition and passing that through.

Obviously if we had a helper function that would probably be doing the `Intl.NumberFormat` stuff behind the scenes.

Edit: I just realized that Ignition base gets around this because they process the value and formatting in Java before passing the formatted string to whatever JS they use on for displaying the perspective page/view.

---

<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 5, 2025, 10:37pm UTC](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084/71 "2025-03-05T22:37:28Z")

</div>

Format control aside I have arrived at something that i think is 'decent'. I also tweaked my side decision logic to be sticky, so if I flipped the side that the tooltip was on, it stays on that side until it has enough space to go back to the other side, instead of always starting on the right and then deciding.

This also let me catch a transform declaration on the tooltip div that was messing with math and sizing. Overall much less jumpy box sizing and placement.

Labels, Process Values and Engineering units each have their own style class defined and applied, so you can tweak them via style classes in Ignition.

> **Tabled Tooltip**
>
> ![tooltipUpdate](https://us1.discourse-cdn.com/inductiveautomation/original/3X/d/a/daebb6abac35903b2264f5933a95a1468cc26e50.png)

One longstanding issue I have is that if I pan a large amount, the tooltip snaps to the last point way off screen and I get a huge scroolbar. (😝)

Edit: Managed to fix the scroolbar issue. Had to just do a very early check to see if the tooltip caret location was inside the viewbox and delete the tooltip elements if it wasn't. I still occasionally get a single flicker of scrollbar near the right edge of the screen but I can't nail down when it happens.

Yet Another Edit:  
Updated tooltip code for anyone who wants to play around with it.

> **Updated External Tooltip Handler**
>
> ```javascript
> (context) => {
> 
> // Helper function to get current locale separator character
> // separator types are decimal or group
> const getSeparator = (locale, separatorType) => {
> const numberWithGroupAndDecimalSeparator = 10000.1;
> return Intl.NumberFormat(locale)
> .formatToParts(numberWithGroupAndDecimalSeparator)
> .find(part => part.type === separatorType)
> .value;
> };
> 
> const getOrCreateTooltip = (chart, axisId) => {
> let tooltipId = "tt_" + axisId;
> let tooltipEl = chart.canvas.parentNode.querySelector('div #' + tooltipId);
> 
> if (!tooltipEl) {
> tooltipEl = document.createElement('div');
> tooltipEl.id = tooltipId;
> tooltipEl.classList.add('psc-trendsurfer\/Tooltip\/container'); // Your custom style here
> tooltipEl.style.opacity = 1;
> tooltipEl.style.pointerEvents = 'none';
> tooltipEl.style.position = 'absolute';
> tooltipEl.style.transition = 'all .1s ease';
> tooltipEl.style.fontSize = '12px';
> 
> let tooltipBody = document.createElement('div');
> tooltipBody.id = tooltipId + '_body';
> tooltipBody.classList.add('psc-trendsurfer\/Tooltip\/body');
> 
> let tooltipTable = document.createElement('table');
> tooltipTable.id = tooltipId + '_item_table';
> tooltipTable.style.tableLayout = 'fixed';
> tooltipTable.style.borderCollapse = 'collapse';
> tooltipTable.classList.add("psc-trendsurfer\/Tooltip\/table");
> 
> let tooltipTableBody = document.createElement('tbody');
> 
> tooltipTable.appendChild(tooltipTableBody);
> tooltipBody.appendChild(tooltipTable);
> tooltipEl.appendChild(tooltipBody);
> 
> chart.canvas.parentNode.appendChild(tooltipEl);
> }
> return tooltipEl;
> };
> 
> const getFormattedValueParts = (itemValue) => {
> let separatorNdx = itemValue.lastIndexOf(localeSeparator);
> let itemIntStr = '';
> let itemDecStr = '';
> 
> if (separatorNdx !== -1) {
> itemIntStr = itemValue.substring(0, separatorNdx);
> itemDecStr = itemValue.substring(separatorNdx + 1);
> } else {
> itemIntStr = itemValue;
> itemDecStr = '';
> }
> // if decimal portion is not blank then re-insert the separator char
> if (itemDecStr !== '') {
> itemDecStr = localeSeparator + itemDecStr;
> }
> return {itemIntStr: itemIntStr, itemDecStr:itemDecStr}
> };
> 
> const createTooltipItem = (tooltipItem, ndx) => {
> 
> let colors = tooltip.labelColors[ndx];
> let {boxHeight, boxWidth} = tooltip.options;
> 
> let itemLabel = tooltipItem.dataset.label || 'Dataset ' + tooltipItem.datasetIndex;
> itemLabel += ':';
> let itemEngUnit = tooltipItem.dataset.engUnit || '';
> 
> let {itemIntStr, itemDecStr} = getFormattedValueParts(tooltipItem.formattedValue);
> 
> const colorBlock = document.createElement('span');
> colorBlock.classList.add('psc-trendsurfer\/Tooltip\/Item\/Indicator'); // Your custom style here
> colorBlock.style.background = colors.backgroundColor;
> colorBlock.style.borderColor = colors.borderColor;
> colorBlock.style.borderWidth = '2px';
> colorBlock.style.borderStyle = 'solid';
> colorBlock.style.marginRight = '10px';
> colorBlock.style.height = boxHeight + 'px' || '12px';
> colorBlock.style.width = boxWidth + 'px' || '12px';
> 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\/Item');// Your custom style here
> 
> const colorTd = document.createElement('td');
> colorTd.style.borderWidth = 0;
> colorTd.appendChild(colorBlock);
> 
> const labelTd = document.createElement('td');
> labelTd.style.borderWidth = 0;
> labelTd.classList.add('psc-trendsurfer\/Tooltip\/Item\/Label');// Your custom style here
> labelTd.appendChild(document.createTextNode(itemLabel));
> 
> const valueIntTd = document.createElement('td');
> valueIntTd.style.borderWidth = 0;
> valueIntTd.style.paddingLeft = '2px';
> valueIntTd.style.textAlign = 'right';
> valueIntTd.classList.add('psc-trendsurfer\/Tooltip\/Item\/Value');// Your custom style here
> valueIntTd.appendChild(document.createTextNode(itemIntStr));
> 
> const valueDecTd = document.createElement('td');
> valueDecTd.style.borderWidth = 0;
> valueDecTd.style.paddingRight = '2px';
> valueDecTd.style.textAlign = 'left';
> valueDecTd.classList.add('psc-trendsurfer\/Tooltip\/Item\/Value');// Your custom style here
> valueDecTd.appendChild(document.createTextNode(itemDecStr));
> 
> const engUnitTd = document.createElement('td');
> engUnitTd.style.borderWidth = 0;
> engUnitTd.classList.add('psc-trendsurfer\/Tooltip\/Item\/Unit');// Your custom style here
> engUnitTd.appendChild(document.createTextNode(itemEngUnit));
> 
> tr.appendChild(colorTd);
> tr.appendChild(labelTd);
> tr.appendChild(valueIntTd);
> tr.appendChild(valueDecTd);
> tr.appendChild(engUnitTd);
> 
> return tr;
> };
> 
> const updateOrAddTooltipItem = (table, item, ndx, groupNdx) => {
> 
> let tableBody = table.querySelector('tbody');
> let itemEl = null;
> 
> if (groupNdx >= table.rows.length) {
> while (table.rows.length <= groupNdx) {
> itemEl = createTooltipItem(item, ndx)
> tableBody.appendChild(itemEl);
> }
> } else {
> let colors = tooltip.labelColors[ndx];
> let {boxHeight, boxWidth} = tooltip.options;
> let {itemIntStr, itemDecStr} = getFormattedValueParts(item.formattedValue);
> let itemLabel = item.dataset.label || 'Dataset ' + item.datasetIndex;
> itemLabel += ':';
> let itemEngUnit = item.dataset.engUnit || '';
> 
> itemEl = table.rows.item(groupNdx);
> let itemRow = table.rows[groupNdx];
> 
> // Update color block for item
> let colorBlock = itemRow.cells.item(0).firstChild;
> colorBlock.style.background = colors.backgroundColor;
> colorBlock.style.borderColor = colors.borderColor;
> colorBlock.style.height = boxHeight + 'px' || '12px';
> colorBlock.style.width = boxWidth + 'px' || '12px';
> 
> // Set updated values for item
> itemRow.cells.item(1).textContent = itemLabel;
> itemRow.cells.item(2).textContent = itemIntStr;
> itemRow.cells.item(3).textContent = itemDecStr;
> itemRow.cells.item(4).textContent = itemEngUnit;
> }
> return itemEl;
> };
> 
> const localeSeparator = getSeparator(navigator.language, 'decimal');
> const {chart, tooltip} = context;
> const {height, width} = chart.canvas;
> const {caretSize, padding, minWidth} = tooltip.options
> const {caretX, caretY, opacity} = tooltip;
> const isOnScreen= (caretX <= width) && (opacity == 1);
> let flipTipSide = context.chart.canvas.lastToolFlipped || false;
> 
> const usedScales = [];
> const tooltips = [];
> const itemsByParent = {};
> let topScaleId = null;
> let scaleWeight = 0;
> let widestTTItem = 0;
> 
> if (!tooltip || !tooltip.dataPoints) {return};
> 
> // Build fake tooltip for sizing checks
> const ruler = getOrCreateTooltip(chart, '__ruler');
> ruler.style.padding = padding + 'px ' + padding + 'px';
> 
> const rulerTable = document.createElement("table");
> rulerTable.style.tableLayout = 'fixed';
> rulerTable.style.borderCollapse = 'collapse';
> rulerTable.classList.add("psc-trendsurfer\/Tooltip\/table");// Your custom style here
> ruler.querySelector('div').append(rulerTable);
> 
> const rulerTableBody = document.createElement('tbody');
> rulerTable.appendChild(rulerTableBody);
> ruler.style.left = '5px';
> 
> // But only spawn it if we are going to be drawing the tooltip
> if (isOnScreen) {
> chart.canvas.parentNode.appendChild(ruler);
> // Check size of title
> let titleEl = document.createElement('span');
> titleEl.classList.add('psc-trendsurfer\/Tooltip\/title');// Your custom style here
> titleEl.appendChild(document.createTextNode(''));
> 
> ruler.insertBefore(titleEl, ruler.childNodes[0]);
> let itemWidth = Math.ceil(ruler.offsetWidth) + 2;
> ruler.removeChild(titleEl);
> 
> if(itemWidth > widestTTItem) {widestTTItem = itemWidth}
> 
> } else {
> chart.canvas.lastToolFlipped = false;
> }
> 
> tooltip.dataPoints.forEach( (element, ndx) => {
> let scaleId = element.dataset.yAxisID || 'y';
> let tooltipId = 'tt_' + scaleId;
> /* If the axis Id is not in the used Ids list
> * then other items that use it as a key do not exist
> * So create them as soon as we see a new axis id
> * If the tooltip is not on screen, delete existing
> * if it exists */
> if (!usedScales.includes(scaleId)) {
> usedScales.push(scaleId);
> itemsByParent[scaleId] = 0;
> // If the tooltip is not on screen and exists, delete it
> if (!isOnScreen){
> let tooltipEl = chart.canvas.parentNode.querySelector('div #' + tooltipId);
> if (tooltipEl) {
> chart.canvas.parentNode.removeChild(tooltipEl)
> }
> 
> return;
> }
> // Determine if scale is the top most scale
> if (chart.scales[scaleId].weight > scaleWeight) {
> scaleWeight = chart.scales[scaleId].weight;
> topScaleId = scaleId;
> }
> 
> let tooltipEl = getOrCreateTooltip(chart, scaleId);
> // apply padding changes
> tooltipEl.style.padding = padding + 'px ' + padding + 'px';
> // Stuff element into array to fetch easier later
> tooltips.push(tooltipEl);
> 
> } else if (!isOnScreen) {return}
> 
> let localGroupItemNdx = itemsByParent[scaleId];
> let tooltipEl = tooltips[usedScales.indexOf(scaleId)];
> let tooltipBodyEl = tooltipEl.querySelector('div #'+ tooltipId + '_body');
> let tooltipItemTable = tooltipBodyEl.querySelector('#'+ tooltipId + '_item_table');
> // Update the values for the items in the tooltip body or add them if they are missing
> // clone the returned item to let us use it for measurements
> let itemEl = updateOrAddTooltipItem(tooltipItemTable, element, ndx, localGroupItemNdx).cloneNode(true);
> // measurements
> rulerTableBody.appendChild(itemEl);
> let itemWidth = Math.ceil(ruler.offsetWidth) + 2;
> rulerTableBody.removeChild(itemEl);
> // compare measured width will all time largest
> if (itemWidth > widestTTItem) {widestTTItem = itemWidth}
> 
> itemsByParent[scaleId] ++;
> });
> 
> if (!isOnScreen) {
> chart.canvas.parentNode.removeChild(ruler);
> return;
> }
> // remove ruler
> chart.canvas.parentNode.removeChild(ruler)
> // calc minimum size
> const widestItemWidth = ((widestTTItem > minWidth) ? widestTTItem : minWidth);
> let expectedX = caretX + caretSize;
> let expectedRight = expectedX + widestItemWidth;
> let flippedTipExpectedX = (caretX - caretSize - widestItemWidth)
> // Sticky tooltip side switching
> if ((!flipTipSide) && (expectedRight >= (chart.chartArea.right - 5))) {
> flipTipSide = true;
> context.chart.canvas.lastToolFlipped = true;
> 
> } else if ((flipTipSide) && (expectedRight <= (chart.chartArea.right - 20))){
> flipTipSide = false;
> context.chart.canvas.lastToolFlipped = false;
> }
> // loop for final placement and title addition
> usedScales.forEach( (element, ndx) => {
> let tooltipEl = tooltips[ndx];
> let tooltipItemCount = itemsByParent[ndx];
> let tooltipId = 'tt_' + element;
> // Grab associated scale
> let scale = chart.scales[element] || null;
> if (!scale) {return}
> // Tooltip placement
> let expectedY = (scale.top + 10);
> tooltipEl.style.top = expectedY + 'px';
> 
> if (flipTipSide) {
> tooltipEl.style.left = flippedTipExpectedX + 'px';
> } else {
> tooltipEl.style.left = expectedX + 'px';
> }
> // See if we have more items in the item table than items used this pass
> let tooltipBodyEl = tooltipEl.querySelector('div #'+ tooltipId + '_body');
> let tooltipItemTable = tooltipBodyEl.querySelector('#'+ tooltipId + '_item_table');
> let tooltipTableBody = tooltipItemTable.querySelector('tbody');
> 
> // If we have more items than we touched, remove the extras
> if (tooltipItemTable.rows.length - 1 > tooltipItemCount) {
> while (tooltipItemTable.rows.length > tooltipItemCount) {
> tooltipTableBody.lastChild.remove()
> }
> }
> // Add title element to top most tooltip
> let titleEl = tooltipEl.querySelector('#tt_'+element+'_title')
> if (element === topScaleId) {
> if (!titleEl) {
> titleEl = document.createElement('span');
> titleEl.id = 'tt_'+element+'_title';
> titleEl.classList.add('psc-trendsurfer\/Tooltip\/title');// Your custom style here
> titleEl.appendChild(document.createTextNode(''));
> tooltipEl.insertBefore(titleEl, tooltipEl.childNodes[0]);
> }
> titleEl.textContent = tooltip.title[0];
> 
> } else if (titleEl) {
> tooltipEl.removeChild(titleEl);
> }
> });
> }
> 
> ```

Another™ Edit:  
So tick label placement handling is terrible. You have to leave them visible, but make the text transparent, and then add a custom plugin to iterate through the list of generated ticks and draw them where you want in the `afterDraw` event call.

But hey, at least tick labels are centered on ticks and I don't have varying amounts of my chart cut off the right side.

> **Corrected Tick Labels**
>
> ![centeredTicks](https://us1.discourse-cdn.com/inductiveautomation/original/3X/0/0/001720e7a7007dc08f267dcf010291698d956bbf.png)

> **afterDraw callback**
>
> ```javascript
> (context) => {
> let ctx = context.ctx;
> let myAxis = context.scales.x;
> myAxis.ticks.forEach((tick, ndx) => {
> let xPos = myAxis.getPixelForTick(ndx);
> let yPos = myAxis.top + 20;
> 
> // draw tick
> ctx.save();
> ctx.textBaseline = 'middle';
> ctx.textAlign ='center';
> ctx.fillStyle = '#666';
> ctx.fillText(tick.label, xPos, yPos);
> ctx.restore();
> });
> }
> 
> ```

I also got annoyed with the ticks so I started and finished my partial polling logic so I don't fetch the whole span if the user has panned over a small amount or if the user has zoomed into an area and the granularity did not change.

[Previous page](https://forum.inductiveautomation.com/t/bug-perspective-power-chart-bug-embr-chart-as-a-replacement/102084.md?page=3)
