browser resize event + handling chart sizes

3,759 views
Skip to first unread message

Davis Ford

unread,
Jul 11, 2014, 10:18:17 PM7/11/14
to dc-js-us...@googlegroups.com
Hi, I searched the list, and found this Q & A with respect to resize events: https://groups.google.com/d/msg/dc-js-user-group/U7YVh6aVwNg/AOMscghZetoJ

The question was an old one, and I'm just wondering if the answer is still the only way to do this?

I'm trying to make everything responsive -- sometimes on a resize, chart sizes can get stuck at the old height/width, and a filter will usually fix it (which I suspect issues a redrawAll())

Another issue, I have, is that I'm using AngularJS, and while the data is loading...which takes a second or two, I don't want to show a bunch of blank panels waiting for the charts to render, so I can hide it with angular ng-show https://docs.angularjs.org/api/ng/directive/ngShow which will hide the DOM elements until the data is available.  When I do that, however, I end up with something like this:

vs. not using ng-show

b/c at the time renderAll is called, it can't correctly guess the size of the parent container.

What's the best solution for fixing dynamic resize issues?  Is the only option to hook into the resize event and issue a dc.redrawAll() ?


Matt Traynham

unread,
Jul 14, 2014, 11:30:18 AM7/14/14
to dc-js-us...@googlegroups.com
That's the way I did it.  If a user or the browser is issuing a resize of the chart, I first hide the chart and then after a delay using an angular $timeout, add the chart back.

If you don't want to do it that way, jquery can help you guess the size properly by getting the offsetWidth/offsetHeight of an element.  Under the covers, jquery does a trick to get those values; it set's the div's display value from none to block, get's the height/width, and then set's the display back to none (without the user even noticing).  You can do it similarly without jquery, only using angular, in a similar fashion.

After injecting $window and $document, use this function on an angular element:

function offset(element) {
   
var display = element[0].style.display;
   
if(display === 'none') {
      element
[0].style.display = 'block';
   
}
   
var boundingClientRect = element[0].getBoundingClientRect();
   element
[0].style.display = display;
   
return {
      width
: boundingClientRect.width || element.prop('offsetWidth'),
      height
: boundingClientRect.height || element.prop('offsetheight'),
      top
: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
      left
: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
   
}
}


Gordon Woodhull

unread,
Jul 14, 2014, 12:19:08 PM7/14/14
to Davis Ford, dc-js-user-group
forgot to cc the group ---

Hi Davis,

We have this issue:

Looks like the requester found something that partly works. I would love it if someone could contribute a PR; otherwise I'll take a look for the release after the (soon!) 2.0.

Cheers
Gordon

Davis Ford

unread,
Jul 14, 2014, 12:24:54 PM7/14/14
to dc-js-us...@googlegroups.com, davi...@gmail.com
Thanks Gordon / Matt -- I'm going to try out these solutions today and I'll post what I figure out -- if it is something that could be easily carved out into a PR into the library, I'll see if I can send one up.

Davis Ford

unread,
Jul 18, 2014, 4:40:39 PM7/18/14
to dc-js-us...@googlegroups.com
Matt, are you hiding and showing the <svg> element or it's parent container?

I'm trying something similar, but just hiding and showing either of those even after a delay on a resize event does nothing.  It simply hides/shows it.

I tried something like this:

angular.module('foo.bar').directive('chartResize', ['$window', '$timeout', function ($window, $timeout) {
  return {
    restrict: 'A',
    link: function (scope, elem, attrs) {
       function toggleDisplay() {
         $timeout(function () { elem.css('display', 'none').css('display', 'block'); }, 300);
       }
       // debounce resize event
       var lazyToggleDisplay = _.debounce(toggleDisplay, 300);
       $window.onresize = lazyToggleDisplay;
   }
 }
}]);

Then add the directive on the chart or chart container:

<div chart-resize>
   <svg>
</div>

or 

<div chart-resize>
  <svg chart-resize>
</div>

Matt Traynham

unread,
Jul 18, 2014, 10:52:53 PM7/18/14
to dc-js-us...@googlegroups.com
Actually, I don't hide/show either of those.  My parent container fills 100% of it's parent and thus the dc chart will do the same on render (with the defaults).  I watch for resizes on the parent container and call resetSvg() on the dc chart, which effectively empties the svg.  When the resize has stopped, I delay a render.  My guess is the same thing could be done with a hide/show, but you can't hide the parent container during a resize.  

Regardless, all the charts will eventually call resetSvg() when you call render() on them (which is necessary to reflect new height/widths).

Davis Ford

unread,
Jul 18, 2014, 11:22:13 PM7/18/14
to Matt Traynham, dc-js-us...@googlegroups.com
Ok, I have a solution that seems to work.  My chart heights are fixed, it is only the width that needs to resize, so this isn't universally applicable.

angular.directive('svgResize', ['$window', function ($window) {
  return {
    restrict: 'A',
    scope: { chart: '=' }, // need reference to the dc chart 

    link: function (scope, elem, attrs) {
      function resize() {
        d3.select(attrs.selector + ' svg').attr('width', '100%');
        scope.chart.redraw();
      }
     // using lodash debounce
     $window.addEventListener('resize', _.debounce(resize, 300));
  }
}]);

Then in the html...

<div id='mychart'>
  <svg svg-resize chart='mychart' selector='mychart'>
</div>

I need to bind to the dc chart reference, so I can call redraw.  The selector attribute is still a bit weird...since I am passing in the actual elem (wrapped in jQLite) to the link function, but I tried all the following:

elem.attr('width', '100%'); // using jQLite .attr() to try to set the width
elem[0].setAttribute('width', '100%'); // using vanilla dom api to try to set the width
d3.select(elem[0]).attr('width', '100%'); // using d3.select and pass the reference

...and none of them affect the width attribute on the svg element, so I just did it with d3 and used a css selector that I passed in through the directive's attributes.  I know this is more of an angular issue, but I'm trying to resolve why I can't get the simpler solution to work setting the width attribute directly on the element itself when I have a reference to it.  There's something bizarre about the elem passed in via the linking function.

If your charts just need to be responsive via width, this is a general solution that seems to work, but you'll definitely want to use something to debounce the resize event.


Matt Traynham

unread,
Jul 18, 2014, 11:40:25 PM7/18/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
I might have been a bit misleading in the first post, I think I was a bit unaware of what you were trying to accomplish.

From personal experience, it might be better to not touch the width/height of the SVG, nor touch the width/height functions of the dc chart since it handles this exact case for you.  You just have to call redraw() to get the SVG updated to fill the parent.

According to these lines, it should fill the parent 100% as long as you don't directly set the chart.height and chart.width functions, they should fill on redraw:

So:
angular.directive('svgResize', ['$window', function ($window) {
  return {
    restrict: 'A',
    scope: { chart: '=' }, // need reference to the dc chart 

    link: function (scope, elem, attrs) {
      function resize() {
        scope.chart.redraw();
      }
     // using lodash debounce
     $window.addEventListener('resize', _.debounce(resize, 300));
  }
}]);

Now the parent container 'div id=myChart', how is that being resized?  Is there some css width property you have set, maybe to auto?

Davis Ford

unread,
Jul 19, 2014, 12:19:47 AM7/19/14
to Matt Traynham, dc-js-us...@googlegroups.com
Doesn't seem to work for me if I don't force the width to 100%.

For example, here's a line chart (how it is configured).

        charts.salesLine
          .height(160)
          .margins({top: 20, left: 50, right: 20, bottom: 20})
          .dimension(dim.day)
          .group(grp.discountRollup, 'Discount')
          .renderArea(true)
          .x(d3.time.scale().domain([beginDay, endDay]))
          .xUnits(d3.time.days)
          .valueAccessor(function (d) {
            return d.value.discount_total;
          })
          .elasticX(true)
          .elasticY(true)
          .brushOn(true)
          .legend(dc.legend())
          .yAxis().tickFormat(dollarLabel);

If I remove the line where I force the width to 100% with d3, it no longer fills the parent container.  Parent is nested in bootstrap .row > .col-lg-8 > .row > .col-lg-4 so widths are calculated via bootstrap grid -- I don't set them anywhere.

Screenshot:

Inline image 1 

Matt Traynham

unread,
Jul 19, 2014, 11:03:00 PM7/19/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Hey Davis,

Two questions.

Is the lodash debounce getting triggered?  I know it's silly, but I'm just curious.  Also, sometimes the resizes on specific charts don't necessarily redraw them correctly...  For instance, I know the PieChart caches it's radius, and thus you have to null it out on preRender.


        chart = new dc.pieChart(this.getChartId(), this.getChartGroup())
           
.legend(dc.legend());
        chart
.on('preRender', function (chart) {
            chart
.radius(null);
       
});

Also, is it possible for you to create a jsfiddle?  A simple bootstrap grid and dc chart example without angular.  Also, the lodash debouce would be helpful as well.

Davis Ford

unread,
Jul 19, 2014, 11:13:33 PM7/19/14
to Matt Traynham, dc-js-us...@googlegroups.com
Hey Matt -- yes, it's definitely getting triggered (had console.log in there, as well as debugged it).  I'm not resizing pie charts, b/c I set their width/height to a constant.  It is mainly rowchart or linechart.

I could probably create a jsfiddle, but it may be faster if I just shoot you an email with a url where you can see the app in its dev glory.  Don't want to post it on the public group, quite yet.

Regards,
Davis

Matt Traynham

unread,
Jul 19, 2014, 11:15:11 PM7/19/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Sure thing, happy to help :)

Matt Traynham

unread,
Jul 20, 2014, 12:59:02 AM7/20/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
I created a jsFiddle with a working row chart.

Here's some things to point out:

            <div id="chart1" class="dc-chart" style="float: inherit !important;"/></div>

dc.js sets float:left, but that seems to screw up the fill 100% with bootstrap.

Now that the parent takes up 100%, the _debounce handler I added calls

function resize() {
    chart3.root().select('svg')
        .attr('height', chart3.height())
        .attr('width', chart3.width());
    chart3.redraw();
}

Lastly, setting elasticX on the chart to 'true'.

Davis Ford

unread,
Jul 20, 2014, 1:08:08 AM7/20/14
to Matt Traynham, dc-js-us...@googlegroups.com
Cool -- yea I also nixed the float: left, as it was problematic.  Might make a decent PR to re-think that one, so we don't all have to add that css override.

Thanks for making this Matt -- it is very helpful.  Going to digest it tomorrow morning when the brain is fresh, but it looks like there are a couple places I can optimize my solution.

Best regards,
Davis

Matt Traynham

unread,
Jul 20, 2014, 1:11:43 AM7/20/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Similarly a line chart:

Yet it looks like some of that is getting clipped when you resize, so that is a random issue.  

My two cents, you may just want to reset the chart SVG on resize start, so it doesn't extend past the lines of your bootstrap grid.  And then debounce just renders instead of redraws.

Like this:

You can do the same for the row:

Phew.  Sorry for the any confusion I caused :(


Davis Ford

unread,
Jul 20, 2014, 1:13:05 AM7/20/14
to Matt Traynham, dc-js-us...@googlegroups.com
No confusion -- really appreciate you digging into it.  Hopefully it helps someone else on the list as well :)

Matt Traynham

unread,
Jul 20, 2014, 1:14:17 AM7/20/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Not a problem.  Cool app though, looking forward to seeing the final product if we get a chance!

Gordon Woodhull

unread,
Jul 20, 2014, 1:31:36 PM7/20/14
to Matt Traynham, dc-js-us...@googlegroups.com
Following this eagerly. Thanks Matt and everyone for helping with the support, and thanks Davis for discussing this instead of silently hacking around the problem. 

I see that the float:left comes from this issue from way back:

I agree that this may not always be desirable. In general dc should probably not try to guess how the client wants to do layout. float:left is great for notebook-style apps like the one I'm building, but not great for dashboards.

We can consider such breaking changes on the way from 2.0 to 3.0. We only need to document them. Right now I'm just trying to get everything working. =)

Matt Traynham

unread,
Jul 20, 2014, 3:43:47 PM7/20/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Hey Gordon,

I think it would be good to open a bug/enhancement regarding correct resizing during a redraw of all charts (not just a render).  Some charts already work, like the rowChart (but elasticX has to be true).  Line charts and possibly some others, just don't seem to redraw properly at all.

It might be a pretty big effort to get them all fixed, but it would definitely be worth it.  I know Davis and myself are not the only ones using dynamic sizes for chart containers.

Gordon Woodhull

unread,
Jul 20, 2014, 3:51:41 PM7/20/14
to Matt Traynham, dc-js-us...@googlegroups.com
Yes I agree redrawing is key, along with getting rid of the float.

I think @wssbck's issue covers the problem well, though I don't know if the people discussing that have put as much effort into it as you guys have:

https://github.com/dc-js/dc.js/issues/544

Honestly I haven't even read the ticket thoroughly – I have been consumed with more prosaic things. But I think aiming for 2.1 is realistic. A PR would be great.. The hard part is testing...

Davis Ford

unread,
Jul 26, 2014, 4:03:35 PM7/26/14
to dc-js-us...@googlegroups.com, skit...@gmail.com
Hey Gordon, the full source for my directive looks like this: https://gist.github.com/davisford/cc7815000a78029b08dc 

The problem I am encountering is one where the resize function is being called too early -- before the chart has had a chance to initialize itself with data.  You'll see I'm calling the chart.data() function here, but as the page it loading -- perhaps before the data is fully loaded and initalized, this blows up with the following stack trace:

TypeError: Cannot read property 'all' of undefined at Object.<anonymous> (http://localhost:3000/dc.js/dc.js:3066:54) at Object._dc.dc.baseMixin._chart.data (http://localhost:3000/dc.js/dc.js:790:45) at resize (http://localhost:3000/components/warehouse/svg-resize-directive.js:32:27)


In dc.js code:

    _chart.data = function(d) {
        if (!arguments.length) return _data.call(_chart,_group);
        _data = d3.functor(d);
        _chart.expireCache();
        return _chart;
    };

_chart.data(function(group) {
        if (_cap == Infinity) {
            return _chart._computeOrderedGroups(group.all());
        } else {
            var topRows = group.top(_cap); // ordered by crossfilter group order (default value)
            topRows = _chart._computeOrderedGroups(topRows); // re-order using ordering (default key)
            if (_othersGrouper) return _othersGrouper(topRows);
            return topRows;
        }
    });

The problem I'm seeing is that the group is undefined at this point.  Everything that hangs off dc chart is pretty much a function.  Is there a safe way for me to test that the chart is initialized and has data?  It would help if the data() function would test for an undefined group property and return undefined, maybe?

Regards,
Davis

Gordon Woodhull

unread,
Jul 29, 2014, 12:21:42 PM7/29/14
to Davis Ford, dc.js user group, skit...@gmail.com
Can't you just
1. set a property on the window once you've loaded the data
2. not handle any resize events unless this property is set
?

Because after all it is your code that is initializing dc, right?

Davis Ford

unread,
Jul 29, 2014, 12:31:42 PM7/29/14
to Gordon Woodhull, dc.js user group, skit...@gmail.com
Yes, I can find a way to work around it, but it would be helpful to be able to call the .data() function and not have it blow up with an error

It is a minor inconvenience for me -- as the way the directive works, it isn't really aware at all of the data loading.  That is done via a controller + service, and they have no knowledge of each other.  I either have to make them aware of each other now, or do it through events, or I added this hack of a workaround, which seems to work:

function resize() {
          //if it is too early, dc.js will blow up if you call data()
          if (scope.chart.group() && scope.chart.data().length > 0) {

This is just me lobbying for a safe .data( ) function despite the lifecycle state dc.js is in

I've worked around it for now, but it is a hack, and I'll come back around to it to find a better solution.
Reply all
Reply to author
Forward
0 new messages