Chart.js icon indicating copy to clipboard operation
Chart.js copied to clipboard

Provide out-of-the-box HTML tooltip capability

Open s4m0r4m4 opened this issue 4 years ago • 10 comments

This issue was created after discussion on https://github.com/chartjs/Chart.js/issues/622#issuecomment-598267163. Thanks for your help with this!

Expected Behavior

Tooltips should not get cut off/cropped.

Current Behavior

The tooltip is rendered within the canvas element, which causes it to be cutoff if it extends beyond the canvas element. For instance, I have a horizontal bar chart with a single bar (used for status display), and the tooltip has multiple entries that are cropped out of view.

Possible Solution

I've heard suggestions of rendering the tooltip outside of the canvas so its visible regardless of canvas sizing, but I'm not in a good position to say whether that's the best path forward.

Steps to Reproduce (for bugs)

Here is my reproducible example: https://plnkr.co/edit/QKIivEiOLyanvis0?open=lib%2Fapp.ts&deferRun=1

Hover your cursor over the bar to see the tooltip get cutoff.

Environment

  • Chart.js version: 2.9.3
  • Browser name and version: Chrome 80.0.3987.116

s4m0r4m4 avatar Mar 12 '20 17:03 s4m0r4m4

I've heard suggestions of rendering the tooltip outside of the canvas so its visible regardless of canvas sizing

You can do that today. See https://www.chartjs.org/docs/latest/configuration/tooltip.html#external-custom-tooltips

benmccann avatar Mar 12 '20 23:03 benmccann

Thank you for the link, I appreciate that there is a workaround for now and I will look into implementing it for my app.

On a more long-term discussion, this feels like the custom tooltip workaround you linked should be the default behavior of chart.js. That workaround involves a significant chunk of code for each chart just to render a tooltip outside the canvas (not to mention directly messing with the DOM, which is frowned upon in an Angular app). Are there reasons to avoid that behavior as default? Another potential solution would be to specify the behavior using an option in the ChartOptions legend dictionary to specify the desired behavior.

s4m0r4m4 avatar Mar 13 '20 13:03 s4m0r4m4

It's an interesting thought. It'd probably be much better performance too since redrawing the canvas whenever the mouse is moved is quite expensive

benmccann avatar Mar 14 '20 02:03 benmccann

In terms of integration with other frameworks, having it outside the library is better. I think it should be possible for the tooltip to be defined by the framework and then integrated. If we had to ship angular, react, etc bindings it’d be a lot of bloat.

Perhaps this is a plugin opportunity since I’m v3 the tooltip has been converted to a plugin.

etimberg avatar Mar 14 '20 02:03 etimberg

Couldn't we have a default implementation that's just pure DOM though? I'm not sure why we'd necessarily need to integrate with other frameworks.

benmccann avatar Mar 14 '20 02:03 benmccann

Yeah I don't mean to imply that Chart.js should support integrations for the major frameworks - there area couple of libraries out there that wrap chart.js for that purpose (ng2-chart, etc.). My meaning was more along the lines of "it'd be nice to not have to write custom tooltip code that directly manipulates the DOM when I'm working in a framework intended to do that for me". Angular in particular recommends not directly manipulating the DOM since it could conflict with the framework DOM update.

A default implementation that is pure DOM would certainly be acceptable from my perspective. I can't comment on v3 stuff since I'm not familiar with it. Thanks for your feedback!

s4m0r4m4 avatar Mar 14 '20 13:03 s4m0r4m4

That's fair, we can look at what would be needed for a DOM version shipped with the library or as a plugin.

I looked at plnkr above and I think an HTML tooltip is going to be the only workable solution. Given there are 10 items in the tooltip, I don't think the canvas is tall enough to even render it.

etimberg avatar Mar 14 '20 13:03 etimberg

If you are looking for a quick fix, I have noticed you can avoid tooltip cutoff by adjusting the layout, title or legend in chart options, and this way expand the canvas area so that you can fit tooltips inside the canvas as a whole without needing to add custom tooltips.

Option 1) layout adds padding to the overall chart element

In chart options root level:

 layout: {
            padding: {
                bottom: 70, // use whatever fits you
                top: 10 // use whatever fits you
            },
  },

Option 2) If you need a title or instruction text, you could add this inside canvas using title. This expands the canvas on top/bottom/left/right depending on your title position and therefore gives more space for tooltips to expand, you might not even need layout padding after this.

In chart options root level:

title: {
            display: true,
            text: "My Chart Title"
            fontSize: 15
            position: "bottom"
},

Option 3) If you need legend, you can use that to expand your canvas area and this way give more space for tooltips

In chart options root level:

legend: {
       display: true,
       position: "bottom"
},

Note that adjusting the layout might change your actual chart size. You can fix this by adjusting the height in your chart wrapper

In template:

 <div class="chart-container">
      <canvas #stackedBarCanvas> </canvas>
 </div>

And in (s)css:

.chart-container {
  height: 300px;
}

Here is a fix for the reproducible example mentioned earlier: https://plnkr.co/edit/2B0EwadOmgA3O1Bz?open=lib%2Fapp.ts&deferRun=1&preview

Other minor adjustments are adjusting the tooltip font size and font family or reducing the tooltip text/lines with custom settings, more about tooltip customization in the documentation https://www.chartjs.org/docs/latest/configuration/tooltip.html.

Zuzze avatar Oct 11 '20 09:10 Zuzze

maybe just adding a simple property like offcanvas: true would do the trick? (that would be like invoking the custom function but using the tooltip element already defined)

allanwsilva avatar Nov 03 '20 01:11 allanwsilva

Hi all, glad to see many are thinking about external tooltips.

My preference, for UX point of view, would be to have a tooltip defined withing a certain space of the html (e.g. a div box under or aside the chart) that will change content on mouse over (desktop) or finger tip (mobile) on the data point.

So that I could load arbitrary length information relate to that data point.

Could you maybe offer an example for reference ?

gg4u avatar May 13 '22 22:05 gg4u

This issue was created after discussion on #622 (comment). Thanks for your help with this!

Expected Behavior

Tooltips should not get cut off/cropped.

Current Behavior

The tooltip is rendered within the canvas element, which causes it to be cutoff if it extends beyond the canvas element. For instance, I have a horizontal bar chart with a single bar (used for status display), and the tooltip has multiple entries that are cropped out of view.

Possible Solution

I've heard suggestions of rendering the tooltip outside of the canvas so its visible regardless of canvas sizing, but I'm not in a good position to say whether that's the best path forward.

Steps to Reproduce (for bugs)

Here is my reproducible example: https://plnkr.co/edit/QKIivEiOLyanvis0?open=lib%2Fapp.ts&deferRun=1

Hover your cursor over the bar to see the tooltip get cutoff.

Environment

  • Chart.js version: 2.9.3
  • Browser name and version: Chrome 80.0.3987.116

I am using chartjs 2.9.3 and currently cant find any article online ! can you share your solution @s4m0r4m4

adesh-thetaonelab avatar Jan 11 '23 20:01 adesh-thetaonelab

Here is a external tooltip that looks like almost exactly like the one from the canvas and also have some basic calculations so it is not outside screen boundaries


external: function(context) {
                        // Tooltip Element
                        let tooltipEl = document.getElementById('chartjs-tooltip');
                        let tooltipContentEl = document.getElementById('chartjs-tooltip-content');
    
                        // Create element on first render
                        if (!tooltipEl) {
                            tooltipEl = document.createElement('div');
                            tooltipEl.id = 'chartjs-tooltip';
                            document.body.appendChild(tooltipEl);
    
                            // Create tooltip content element
                            tooltipContentEl = document.createElement('div');
                            tooltipContentEl.id = 'chartjs-tooltip-content';
                            tooltipEl.appendChild(tooltipContentEl);
                        }
    
                        // Hide if no tooltip
                        const tooltipModel = context.tooltip;
                        if (tooltipModel.opacity === 0) {
                            tooltipEl.style.opacity = '0';
                            return;
                        }
    
                        // Set caret Position
                        tooltipEl.classList.remove('above', 'below', 'no-transform');
                        if (tooltipModel.yAlign) {
                            tooltipEl.classList.add(tooltipModel.yAlign);
                        } else {
                            tooltipEl.classList.add('no-transform');
                        }
    
                        function getBody(bodyItem) {
                            return bodyItem.lines;
                        }
    
                        // Set Text
                        if (tooltipModel.body) {
                            const bodyLines = tooltipModel.body.map(getBody);
    
                            let innerHtml = '';
                            bodyLines.forEach(function(body, i) {
                                const colors = tooltipModel.labelColors[i];
                                let style = 'background: #495057';
                                style += '; color:' + '#ffffff';
                                style += '; border-color:' + '#000000';
                                style += '; border-width: 2px';
                                style += '; padding: 0.5rem';
                                style += '; border-radius: 5px';
                                style += '; word-wrap: break-word';
                                const colorSquare = '<span class="mr-2" style="border: 2px solid #ffffff; display: inline-block; width: 12px; height: 12px; background-color: ' + colors.backgroundColor +'"></span>';
                                const span = '<div style="' + style + '">' + colorSquare + body + '</div>';
                                innerHtml += span;
                            });

                            tooltipContentEl.innerHTML = innerHtml;
                        }
                        const position = context.chart.canvas.getBoundingClientRect();
                        const font = tooltipModel.options.bodyFont;
                        const bodyFont = !font || font.size || font.family ? null :
                        (font.style ? font.style + ' ' : '')
                        + (font.weight ? font.weight + ' ' : '')
                        + font.size + 'px '
                        + font.family;
                        
                        let left = position.left + window.pageXOffset + tooltipModel.caretX;
                        // Display, position, and set styles for font
                        tooltipEl.style.opacity = '1';
                        tooltipEl.style.position = 'absolute';
                        tooltipEl.style.left = left + 'px';
                        tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px';
                        tooltipEl.style.font = bodyFont;
                        tooltipEl.style.padding = tooltipModel.padding + 'px ' + tooltipModel.padding + 'px';
                        tooltipEl.style.pointerEvents = 'none';
                        
                        //adjust to right position if no space in left
                        if (left + DomHandler.getOuterWidth(tooltipEl) >= DomHandler.getViewport()?.width) {
                            left -= DomHandler.getOuterWidth(tooltipEl);
                        }
                        tooltipEl.style.left = left + 'px';

                        // Check if there's enough space above the chart
                        let top = position.top + window.pageYOffset + tooltipModel.caretY;
                        if (top - DomHandler.getOuterHeight(tooltipEl) < 0) {
                        top = position.top + window.pageYOffset + tooltipModel.caretY + DomHandler.getOuterHeight(tooltipEl);
                        }

                        // Check if there's enough space below the chart
                        if (top + DomHandler.getOuterHeight(tooltipEl) >= DomHandler.getViewport().height) {
                        top = position.top + window.pageYOffset + tooltipModel.caretY - DomHandler.getOuterHeight(tooltipEl);
                        }

                        tooltipEl.style.top = top + 'px';
                    },

apetkovBP avatar Feb 04 '23 14:02 apetkovBP