Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions bqplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
"""

from traitlets import (Unicode, Instance, List, Dict, CFloat, Bool, Enum,
Float, Int)
from ipywidgets import DOMWidget, register, Color, widget_serialization
Float, Int, TraitError, default, validate)
from ipywidgets import DOMWidget, register, Color, widget_serialization, Layout

from .scales import Scale, LinearScale
from .interacts import Interaction
Expand Down Expand Up @@ -77,20 +77,33 @@ class Figure(DOMWidget):

Layout Attributes

min_width: CFloat (default: 800.0)
minimum width of the figure including the figure margins
min_height: CFloat (default: 600.0)
minimum height of the figure including the figure margins
preserve_aspect: bool (default: False)
Determines whether the aspect ratio for the figure specified by
min_width and min_height is preserved during resizing. This does not
guarantee that the data coordinates will have any specific aspect
ratio.
fig_margin: dict (default: {top=60, bottom=60, left=60, right=60})
Dictionary containing the top, bottom, left and right margins. The user
is responsible for making sure that the width and height are greater
than the sum of the margins.

min_aspect_ratio: float
minimum width / height ratio of the figure
max_aspect_ratio: float
maximum width / height ratio of the figure

Note
----

The aspect ratios stand for width / height ratios.

- If the available space is within bounds in terms of min and max aspect
ratio, we use the entire available space.
- If the available space is too oblong horizontally, we use the client
height and the width that corresponds max_aspect_ratio (maximize width
under the constraints).
- If the available space is too oblong vertically, we use the client width
and the height that corresponds to min_aspect_ratio (maximize height
under the constraint).
This corresponds to maximizing the area under the constraints.

Default min and max aspect ratio are both equal to 16 / 9.

"""
title = Unicode().tag(sync=True, display_name='Title')
axes = List(Instance(Axis)).tag(sync=True, **widget_serialization)
Expand All @@ -99,11 +112,24 @@ class Figure(DOMWidget):
**widget_serialization)
scale_x = Instance(Scale).tag(sync=True, **widget_serialization)
scale_y = Instance(Scale).tag(sync=True, **widget_serialization)

@default('scale_x')
def _default_scale_x(self):
return LinearScale(min=0, max=1)

@default('scale_y')
def _default_scale_y(self):
return LinearScale(min=0, max=1)

fig_color = Color(None, allow_none=True).tag(sync=True)

min_width = CFloat(800.0).tag(sync=True)
min_height = CFloat(500.0).tag(sync=True)
preserve_aspect = Bool().tag(sync=True, display_name='Preserve aspect ratio')
layout = Instance(Layout, kw={
'flex': '1',
'align_self': 'stretch',
'min_height': '350px'
}, allow_none=True).tag(sync=True, **widget_serialization)
min_aspect_ratio = Float(16.0 / 9.0).tag(sync=True)
max_aspect_ratio = Float(16.0 / 9.0).tag(sync=True)

fig_margin = Dict(dict(top=60, bottom=60, left=60, right=60)).tag(sync=True)
padding_x = Float(0.0, min=0.0, max=1.0).tag(sync=True)
Expand All @@ -113,11 +139,14 @@ class Figure(DOMWidget):
default_value='top-right').tag(sync=True, display_name='Legend position')
animation_duration = Int().tag(sync=True, display_name='Animation duration')

def _scale_x_default(self):
return LinearScale(min=0, max=1)

def _scale_y_default(self):
return LinearScale(min=0, max=1)
@validate('min_aspect_ratio', 'max_aspect_ratio')
def _validate_aspect_ratio(self, proposal):
value = proposal['value']
if proposal['trait'].name == 'min_aspect_ratio' and value > self.max_aspect_ratio:
raise TraitError('setting min_aspect_ratio > max_aspect_ratio')
if proposal['trait'].name == 'max_aspect_ratio' and value < self.min_aspect_ratio:
raise TraitError('setting max_aspect_ratio < min_aspect_ratio')
return value

_view_name = Unicode('Figure').tag(sync=True)
_model_name = Unicode('FigureModel').tag(sync=True)
Expand Down
1 change: 1 addition & 0 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"bootstrap": "^3.3.5",
"d3": "^3.5.16",
"element-resize-detector": "^1.1.0",
"jquery": "^2.1.4",
"jupyter-js-widgets": "^1.0.0",
"topojson": "^1.6.24",
Expand Down
126 changes: 68 additions & 58 deletions js/src/Figure.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
* limitations under the License.
*/

var resizeDetector = require("element-resize-detector")({
strategy: "scroll" //<- For ultra performance.
});


define(["jupyter-js-widgets", "d3", "underscore"],
function(widgets, d3, _) {
"use strict";
Expand All @@ -27,8 +32,6 @@ define(["jupyter-js-widgets", "d3", "underscore"],
},

render : function() {
this.width = this.model.get("min_width");
this.height = this.model.get("min_height");
this.id = widgets.uuid();

// Dictionary which contains the mapping for each of the marks id
Expand All @@ -53,17 +56,12 @@ define(["jupyter-js-widgets", "d3", "underscore"],
"ms-user-select": "none",
"moz-user-select": "none",
"khtml-user-select": "none",
"webkit-user-select": "none"});
"webkit-user-select": "none"
});

this.$el.css({"flex-grow": "1",
"flex-shrink": "1",
"align-self": "stretch",
"min-width": this.width,
"min-height": this.height});
this.margin = this.model.get("fig_margin");

this.svg = d3.select(this.el)
.attr("viewBox", "0 0 " + this.width + " " + this.height);
this.svg = d3.select(this.el);
this.update_plotarea_dimensions();
// this.fig is the top <g> element to be impacted by a rescaling / change of margins
this.fig = this.svg.append("g")
Expand All @@ -80,6 +78,7 @@ define(["jupyter-js-widgets", "d3", "underscore"],
.style("pointer-events", "all");

this.change_color();
this.change_layout();

this.fig_axes = this.fig.append("g");
this.fig_marks = this.fig.append("g");
Expand Down Expand Up @@ -135,8 +134,7 @@ define(["jupyter-js-widgets", "d3", "underscore"],
that.axis_views = new widgets.ViewList(that.add_axis, null, that);
that.axis_views.update(that.model.get("axes"));

// TODO: move to the model
that.model.on_some_change(["fig_margin", "min_width", "min_height", "preserve_aspect"], that.update_layout, that);
that.model.on_some_change(["fig_margin", "min_aspect_ratio", "max_aspect_ratio"], that.relayout, that);
that.model.on_some_change(["padding_x", "padding_y"], function() {
this.figure_padding_x = this.model.get("padding_x");
this.figure_padding_y = this.model.get("padding_y");
Expand All @@ -160,19 +158,11 @@ define(["jupyter-js-widgets", "d3", "underscore"],
this.set_interaction(value);
}, that);

// that.id is added to namespace the event to this particular
// view. This is required because we are adding the event on
// the output cell and hence we need to unbind it when this
// view is being removed. To identify the event listener
// corresponding to this view, we need the id.
$(that.options.cell).on("output_area_resize."+that.id, function() {
that.update_layout();
});
resizeDetector.listenTo(that.el, _.bind(that.relayout, that));

that.displayed.then(function() {
that.el.parentNode.appendChild(that.tooltip_div.node());
that.create_listeners();
that.update_layout();
});
});
},
Expand All @@ -192,12 +182,18 @@ define(["jupyter-js-widgets", "d3", "underscore"],

create_listeners: function() {
this.listenTo(this.model, "change:fig_color", this.change_color, this);
this.listenTo(this.model, "change:layout", this.change_layout, this);
},

change_color: function() {
this.bg.style("fill", this.model.get("fig_color"));
},

change_layout: function() {
this.stopListening(this.model.previous("layout"));
this.listenTo(this.model.get("layout"), "change", this.relayout, this);
},

remove: function() {
this.svg.remove();
$(this.options.cell).off("output_area_resize."+this.id);
Expand Down Expand Up @@ -408,59 +404,73 @@ define(["jupyter-js-widgets", "d3", "underscore"],
});
that.y_padding_arr[scale_id] = max;
});

// This is for the figure to relayout everything to account for the
// updated margins.
this.trigger("margin_updated");

},

update_plotarea_dimensions: function() {
this.plotarea_width = this.width - this.margin.left - this.margin.right;
this.plotarea_height = this.height - this.margin.top - this.margin.bottom;
},

update_layout: function() {
// First, reset the natural width by resetting the viewbox, then measure the flex size, then redraw to the flex dimensions
this.svg.attr("width", null);
this.svg.attr("viewBox", "0 0 " + this.model.get("min_width") +
" " + this.model.get("min_height"));
var rect = this.el.getBoundingClientRect();
this.width = rect.width > 0 ? rect.width : this.model.get("min_width");
this.height = rect.height > 0 ? rect.height : this.model.get("min_height");
var preserve_aspect = this.model.get("preserve_aspect");
if (preserve_aspect === true) {
var aspect_ratio = this.model.get("min_width") / this.model.get("min_height");
if (this.width/this.height > aspect_ratio) {
this.width = this.height * aspect_ratio;
} else {
this.height = this.width / aspect_ratio;
relayout: function() {
this.svg.attr("viewBox", "0 0 1 1");
var that = this;
window.requestAnimationFrame(function () {
var ratio = that.el.clientWidth / that.el.clientHeight;
var max_ratio = that.model.get("max_aspect_ratio");
var min_ratio = that.model.get("min_aspect_ratio");

if (ratio <= max_ratio && ratio >= min_ratio) {
// If the available width and height are within bounds in terms
// of aspect ration, use all the space available.
that.width = that.el.clientWidth;
that.height = that.el.clientHeight;
} else if (ratio > max_ratio) {
// The available space is too oblong horizontally.
// Use all vertical space and compute width based on maximum
// aspect ratio.
that.height = that.el.clientHeight;
that.width = that.height * max_ratio;
} else { // ratio < min_ratio
// The available space is too oblong vertically.
// Use all horizontal space and compute height based on minimum
// aspect ratio.
that.width = that.el.clientWidth;
that.height = that.width / min_ratio;
}
}
this.svg.attr("viewBox", "0 0 " + this.width + " " + this.height);
this.svg.attr("width", this.width);

// update ranges
this.margin = this.model.get("fig_margin");
this.update_plotarea_dimensions();
that.svg.attr("viewBox", "0 0 " + that.width +
" " + that.height);
window.requestAnimationFrame(function () {
// update ranges
that.margin = that.model.get("fig_margin");
that.update_plotarea_dimensions();

this.scale_x.set_range([0, this.plotarea_width]);
this.scale_y.set_range([this.plotarea_height, 0]);
// transform figure
this.fig.attr("transform", "translate(" + this.margin.left + "," +
this.margin.top + ")");
this.title.attr({x: (0.5 * (this.plotarea_width)),
y: -(this.margin.top / 2.0),
dy: "1em"});
that.scale_x.set_range([0, that.plotarea_width]);
that.scale_y.set_range([that.plotarea_height, 0]);
// transform figure
that.fig.attr("transform", "translate(" + that.margin.left + "," +
that.margin.top + ")");
that.title.attr({
x: (0.5 * (that.plotarea_width)),
y: -(that.margin.top / 2.0),
dy: "1em"
});

this.bg
.attr("width", this.plotarea_width)
.attr("height", this.plotarea_height);
that.bg
.attr("width", that.plotarea_width)
.attr("height", that.plotarea_height);

this.clip_path.attr("width", this.plotarea_width)
.attr("height", this.plotarea_height);
that.clip_path.attr("width", that.plotarea_width)
.attr("height", that.plotarea_height);

this.trigger("margin_updated");
this.update_legend();
that.trigger("margin_updated");
that.update_legend();
});
});
},

update_legend: function() {
Expand Down
7 changes: 3 additions & 4 deletions js/src/FigureModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ define(["jupyter-js-widgets", "./BaseModel", "underscore"], function(widgets, Ba
scale_x: undefined,
scale_y: undefined,
fig_color: null,

min_width: 800.0,
min_height: 500.0,
preserve_aspect: false,
layout: undefined,
min_aspect_ratio: 16 / 9,
max_aspect_ratio: 16 / 9,

fig_margin: {
top: 60,
Expand Down