amCharts doesn't display chart for initially-hidden divs

Andrzej Doyle picture Andrzej Doyle · Apr 4, 2012 · Viewed 28.1k times · Source

I am using amCharts (which uses Raphaël behind the scenes) to render some charts as SVG; and have noticed that if the SVG is rendered in an initially-invisible div, the browser does not immediately render the image when the div becomes visible. If I modify the display however, e.g. by resizing the browser or Ctrl-mousewheel zooming, the SVG image is then rendered as expected when the page is redrawn.

The exact method of div visibility switching is via Bootstrap's tabbed navbar.

I admit to not being very experienced with SVG - is this an issue with the browsers' rendering, or amCharts' SVG markup, or am I required to explicitly call some sort of repaint method when I can tell the visibility of an SVG has changed?

Here's a jsFiddle which illustrates the problem; if you switch to Section 2 (in Chrome, Firefox) the chart isn't visible initially. Resizing the display causes it to appear.

Answer

Andrzej Doyle picture Andrzej Doyle · Apr 4, 2012

I've found the reason for both the initial behaviour and the workaround - and it's all amCharts specific (nothing to do with SVG per se) so I'm rephrasing the title accordingly.

What happens is that when amCharts creates the SVG, it needs to (or at least, decides to) define the width and height in absolute terms. These are based on the size of the target div, obtained via the offsetWidth and offsetHeight properties.

The inactive tab has the display: none property set, and as a result this part of the DOM is not even rendered, so returns zero for both size properties. This ultimately leads to amCharts creating a 0x0 SVG chart when chart.write is called for the hidden div.

Resizing fixes things because each chart registers a listener to the onresize window event, which calls the chart's handleResize method. This forces a recalculation of the width and height based on the div's new (current) dimensions.


So in conclusion I think there are two alternative ways to handle this:

  • Call chart.write for a chart when and only when its tab becomes visible.
  • Call each chart's handleResize method when the tabs change.

(The first option avoids the initial hit of rendering an invisible chart, but then does a full redraw every time the tabs are changed. The latter takes a hit up-front but is likely quicker thereafter. For bonus marks, the best solution would be to render each chart exactly once between each resize, the first time it becomes visible, but that's a lot more complex as it would involve interfering with the default event listeners, amongst other things.)

Update: There's further complications with rendering an invisible chart; in particular, I found issues with the height calculations not taking into account the space required by the domain axis and so stretching the chart out of its div. This wasn't fixed by calling handleResize - calling measureMargins beforehand looked like it should work but didn't. (There's probably another method one could call after this to make it work such as resetMargins but at this point it started to feel very flaky...)

As such I don't think it's practical to render a chart for the first time on a non-visible div, so I went with some combination of the bullets above. I listen for when a chart's tab becomes visible for the first time and then call chart.write for the appropriate chart object - and whenever the tabs change, all previously-rendered charts are told to handle the resize.