Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide out-of-the-box HTML tooltip capability #7195

Open
s4m0r4m4 opened this issue Mar 12, 2020 · 12 comments
Open

Provide out-of-the-box HTML tooltip capability #7195

s4m0r4m4 opened this issue Mar 12, 2020 · 12 comments

Comments

@s4m0r4m4
Copy link

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
@benmccann
Copy link
Contributor

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

@s4m0r4m4
Copy link
Author

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.

@benmccann
Copy link
Contributor

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

@etimberg
Copy link
Member

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.

@benmccann
Copy link
Contributor

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.

@s4m0r4m4
Copy link
Author

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!

@etimberg
Copy link
Member

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.

@Zuzze
Copy link

Zuzze commented Oct 11, 2020

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.

@etimberg etimberg changed the title Tooltips get cropped if they go beyond canvas Provide default HTML tooltip capability Oct 17, 2020
@etimberg etimberg changed the title Provide default HTML tooltip capability Provide out-of-the-box HTML tooltip capability Oct 17, 2020
@allanwsilva
Copy link

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)

@gg4u
Copy link

gg4u commented May 13, 2022

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 ?

@adesh-thetaonelab
Copy link

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

@apetkovBP
Copy link

apetkovBP commented Feb 4, 2023

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';
                    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants