Mpld3: Browser/HTML specify figure size rather than figsize * dpi

Created on 6 Feb 2014  Â·  31Comments  Â·  Source: mpld3/mpld3

Really enjoying the package, many thanks for sharing.

This is a suggested improvement, but it's not quite pythonic. But then again neither is d3js :)

It's great that the figure's set size is reflected on screen, but I think it would be "more responsive" to have an option to set the canvas size to the bounding div, and then letting that div size be determine by the browser width/whatever else the developer wants.

Is it worth me mucking about in _js.py and _object.py to get this going?

Most helpful comment

@cliffckerr I can definitely add this to draw_figure, perhaps as an argument that takes an Object and sets all the attributes specified in that object on the svg, so you could pass in something like {viewBox: ..., width: '90vw', height: 'auto'}.

I did want to ask you and the others in this thread if you think this should be added anywhere else, to support e.g. fig_to_html(). If so, maybe we should think of a solution to that. Otherwise, let me know and I can make the changes to draw_figure() straight away.

All 31 comments

I'm not quite sure what you're asking... We get the figure size in inches from the matplotlib figure, and also get the dpi from the matplotlib figure. How are you suggesting we change this? Thanks

Rather than use the absolute figure size set in Python, I mean something like this:

<div id='parent' class='figsizeinfo'>
//mpld3 generated code
</div>

Where figsizeinfo is a CSS specification of size, and the mpld3 fills the div. (in my experience this is how flotjs works)

Hmm... this question might betray my inexperience with web stuff, but I thought that SVG (which mpld3 uses) is entirely pixel-based, and that can't be easily changed. Does flotjs use SVG, or some other specification?

SVG (Scalable Vector Format) graphics are vector based, not pixel based.

Here’s an an example of an SVG image generated with vanilla matplotlib that scales with window size:

http://bleepshow.pithy.io/SVG_Example

(clearly riffed on your example)

d3js, and all canvas drawing figures, are vector as far as I understand, so they should be able to do this.

The trick, I imagine, is to set

figwidth = $(“#parent”).width //(jquery based, but you get the point )

rather than

figwidth = figsize[0] * dpi;

Yes, I know that SVG is vector-based, but unless I'm mistaken, the size of the canvas, as well as the size of markers and their position on the screen are measured in points. It seems like it would be extremely complicated to write SVG code that will scale correctly for an arbitrary figure with an arbitrary window size.

No, it's pretty easy.

Here's hack I applied to your fig_to_d3 code:

print mpld3.fig_to_d3(fig).replace("\n","\r").replace("var figwidth = 8.0 * 80","var figwidth = $(window).width()*.9;").replace("var figheight = 6.0 * 80","var figheight = $(window).height()*.9;")

which comes up as :

http://bleepshow.pithy.io/mpld3_window_scale_test

Now, the labels are in the wrong place, but again it's "just" a matter of reference their position to the div and not absolutely to the figure. But the rendering of the figure works well as far as I can tell.

(note that this doesn't resize unless you refresh, but with a little glue that's also straight forward)

I'm currently jerry-rigging a solution based on this for myself, I'm happy to try and polish it into something decent for a pull-request.

Responsive size of mpld3 figure will be really usefull. If you add this with a button (in the toolbar) to be able to open the current figure to a new window and entire figure will be easly resizable.

+1

@hadim I'd want to keep it "pythonic", so something like

mpld3.fig_to_d3(fig,size="adaptive")

would be a good trigger

I agree !

I understand that making the figure a different size is easy. But overall I'm still not convinced it's a good idea. Once the figure size is modified, it will involve hacking all the line widths, marker loations, marker sizes, font sizes, etc. which will add a whole lot of complication to the code without much benefit. Also, the correct placement of the legend, axis labels, and other properties depends on the plt.draw() command, which makes assumptions based on the internal figure size and dpi settings, and I honestly have no idea how that could easily be re-adjusted after the fact.

The philosophy of this mpld3 is to produce simple outputs that reflect as closely as possible what's in the matplotlib figures. If you want to change the size of the output figure, the preferred method would be to adjust the matplotlib figure size. Anything else seems like it will quickly become a major headache to implement, test, and maintain.

What about a plugin ?

plugins.connect(fig, plugins.ResponsiveSize())

A plugin would probably be a good way to do it. But again, how do you deal with adapting the font size, line widths, marker sizes, legend locations, and axes text locations? Because of all those issues, without some extensive infrastructure to support resizing, I think such a plugin would have a very limited range of usefulness.

@jakevdp I disagree: the only thing that suffered in my 3 second hack above were the label locations, and again, it's a matter of writing better javascript. The DOM allows for absolute positioning and relative position without much work.

The way the code is implemented now, everything is hard coded into the figure. With a bit of work, widths/heights, line thickness, and font size can be linearly scaled against the div size. The trickiest part would be the aspect ratio, and that can be locked. Once the aspect ratio is locked, linearly scaling everything is quite straightforward

@hadim that is a good a idea.

I don't know d3.js so I don't have any idea. But because we are working on svg file, I really don't imagine we can't easly do it...

Some links to start thinking about the best way to make it:

+1 for @dansteingart. Why not set all SVG internal length and position be in relative coordinates and then before the output use a viewBox to resize the whole figure and so match with figsize matplotlib parameter (or be responsive in other case).

@dansteingart - I agree that with enough JS hacks, you could make it work. But regardless, I am not in support of fundamentally modifying this package to include that feature.

Why? Well, one piece is that it goes against the aforementioned philosophy of the package, which is that plot elements, figure sizes, etc. should be defined through the matplotlib interface.

Second is that this is not a "must-have" feature, and making the code base significantly more complicated for such a non-must-have feature will hurt the package in the long run. For some background on why this might be, see @ellisonbg's talk from last year at SciPy.

If you'd like to work on a plugin to add this sort of behavior, you're welcome to. If it works well, it may even be worth including it in the plugins module. But I am firmly against major modifications to the basic figure layout for new features which run counter to the philosophy of the package, no matter how cool they may be.

I'm just getting caught up on this, and is sounds to me like some sort of plugin can accomplish some sort of interesting responsive figure resizing behavior. @dansteingart started this thread off with an idea for a feature, and a question about if it was worth get started implementing the feature in a certain way. In answer to the question, I would suggest that you start with a plugin and see how far that can go without changing _objects.py or _js.py. But if you're up for it, I'd like to take a step back and talk about what problem we are trying to solve:

It's great that the figure's set size is reflected on screen, but I think it would be "more responsive" to have an option to set the canvas size to the bounding div, and then letting that div size be determine by the browser width/whatever else the developer wants.

I'd like some clarification about the use case where this is going to come up. In my regular workflow, I use ipython with a notebook server running in the cloud, with my web browser maximized. So it could come up for me if I switch from my work display to my laptop display and have less screen width. But I'm sure there are other settings that others are thinking of. What else?

@jakevdp Fair enough, and I agree that there shouldn’t be anything changed _at all_ on the python side. However, I think it’s all fair game on the JS side, and if the point of the experiment is to marry the best aspects of matplotlib and d3js, I think it’s worth while to pursue.

The way I see it, once you call fig_to_d3, you’re out of python land, and making the figure responsive = writing better JS.

As for utility of responsive pages, for iPython output alone, I agree with you, but python is more than iPython notebook, and I think, ultimately, using mpl3d to create size responsive web pages will be more useful than you imply here.

Per your logic, if I wanted to zoom in on a detail in keeping with matplotlib, I should just change xlim/ylim.

Again thanks for the great package, I’ll see what I can’t do with a plugin and get back to you.

@aflaxman

I'd like some clarification about the use case where this is going to come up. In my regular workflow, I use ipython with a notebook server running in the cloud, with my web browser maximized. So it could come up for me if I switch from my work display to my laptop display and have less screen width. But I'm sure there are other settings that others are thinking of. What else?

see http://pithy.io

The I wrote pithy to allow me/my group/my classes to write scripts that then turn into web apps. Having the figure fit the window makes life much nicer. MPL3d gives me a much nicer tools to do interactive plots (I was hacking flot around), and if the interactive plots are size responsive, it's all win.

Thanks @dansteingart. Keep in mind that even though the JS is hidden from the Python user, it is still code and it still needs to be maintained. My experience over the years with open source development is that folks often come along who are excited about implementing new features, and once those features are merged, their maintenance and upkeep fall on the shoulders of the few devs who have invested in the package in the long term. I'm already extremely over-committed to the point where I'm forced to ignore bug reports on well-used packages I created just a few years ago. I really don't want mpld3 to get to that point.

Thanks for understanding, and I look forward to seeing what you can put together in a plugin.

@jakevdp that point is well taken, and I think the plugin architecture works well to that end. If it breaks it'll be my fault, not yours (that is, unless you change fig_to_d3 :-p )

The rewrite has landed, and this might be _slightly_ easier to implement as a plugin, at least for special situations. I didn't write it with this sort of dynamic situation in mind, though, so there may be some pitfalls. Let me know if you make any progress on this.

Hi, thanks for the great project. Has there been any progress on this issue? Has the plugin / feature been developed? Specifically, I am referring to "option to set the canvas size to the bounding div." Thanks.

I think this is a worthy feature, but for reference after plotting you can use d3 to make the figure autoscale (here with the window height)

fetch("d3/" + figure + ".json")
    .then((response) => response.json())
    .then((data) => mpld3.draw_figure(figure, data))
    .then(() => {
        d3.select("#" + figure)
            .selectAll(".mpld3-figure")
            .attr("width", "100%")
            .attr("height", "70vh")
            .attr("viewBox", "0 0 1200 800")
            .attr("preserveAspectRatio", "xMinYMin meet");
    });

Here I'll second @hadim and @carlinmack to use viewBox. It can be very simple actually. For instance, if you currently have <svg width="1200" height="800">, try instead <svg viewBox="0 0 1200 800">. That's it. The viewBox defines the coordinate system inside of the svg, and the width and height parameters become optional and define the external dimensions like any other element. You could also have width=100% or set the value via css directly (the width and height as css have precedence over attributes).

I used that successfully with a recent chrome version. Cross-browser dependency should be investigated for the details, but I think the bottom line is clear.

PS: there is a background article at https://css-tricks.com/scale-svg but I find it confusing when the method I outlined above just works (at least in modern browsers) and is simple conceptually: viewBox = "0 0 ${width} ${height}" for the inner code, width=... and height=... as svg attribute or css parameter for the re-scaling, if necessary.

Yeah well, that's exactly what @carlinmack suggested. I was confused by "use d3 to make the figure autoscale", since it is pure HTML / CSS, but ok, manipulating the DOM is what d3 does. The "preserveAspectRatio" as indicated by @carlinmack is the default value I believe (at least that's what is explained in the css-tricks.com link). Why would you indicate height="70vh" if the aspect ratio is preserved anyway ? Is that some kind of upper bound for very narrow and tall figures ?

correct, I didn't want portrait figures being higher than the viewport :)

Except that it makes the figure bigger than necessary, not appropriate when a tight fit with surrounding content is needed. Using max-height instead works for me.

Here is how I end up loading mpld3 figures in the browser:

      let figureID = "id_12345" ; 
      let dataURL = figureID + ".json" ; 

      d3.json(dataURL).then( function(data) {
        mpld3.draw_figure(figureID, data);
        return data;
      }).then( function(data) {
        d3.select("#" + figureID)
          .selectAll(".mpld3-figure")
          .attr("width", null)   // remove "width" and "height" attributes as set by mpld3
          .attr("height", null)  
          .attr("viewBox", `0 0 ${data.width} ${data.height}`)
      });

Thanks for the discussion @carlinmack @perrette ! Coincidentally we just encountered this too, @dkong-idm's solution was:

    const resizeChart = (viewBoxSettings) => {
        let svg = document.getElementsByClassName("mpld3-figure");

        if (svg && svg.length > 0) {
            svg[0].setAttribute("viewBox",viewBoxSettings );
            svg[0].setAttribute("width", "90vw");
            svg[0].setAttribute("height", "auto");
        }
    }

@vladh , would it be a lot of work to add this auto-scaling functionality to mpld3, either as an optional arg to draw_figure or else as a standalone function?

@cliffckerr I can definitely add this to draw_figure, perhaps as an argument that takes an Object and sets all the attributes specified in that object on the svg, so you could pass in something like {viewBox: ..., width: '90vw', height: 'auto'}.

I did want to ask you and the others in this thread if you think this should be added anywhere else, to support e.g. fig_to_html(). If so, maybe we should think of a solution to that. Otherwise, let me know and I can make the changes to draw_figure() straight away.

@vladh Yes, it would also be very useful in fig_to_html().

Was this page helpful?
0 / 5 - 0 ratings