Use a widget from inside another widget
See original GitHub issueI hope this is the right place to ask a question like this; if not please point where I should do it.
This question is similar to what I am trying to do:
However in my case, the “widget container” I want to use as a holder for other objects is an <svg>
element that has other elements inside. This is a container type that I don’t think I have seen existing yet in the widgets library.
The ultimate goal is to have a view that holds references to all the sub-views, which are all children of the parent <svg>
element. Then one would be able to do many different interesting things, like respond to subview clicks and trigger behaviors. Example: a polygon sub-view- which might be a <path>
element- could toggle a class="selected"
attribute of that element (to change color based on the CSS or something like that). Or, it might add the polygon’s index to a list of “selected” polygons, which current list of selected things is reflected in the model.
I did some Backbone tutorials this weekend and feel barely knowledgeable enough to generally understand how one might write a widget that contains references to other “sub-widgets” (In particular, the sub-widgets will have Unicode
fields, and these fields are just the HTML tags for different SVG elements). In pure Backbone, I think you might use a Collection for this. But I am assuming it will be quite a bit different using ipywidgets.
I have investigated the Box
source code and also the BoxView
typescript code. However since I have never learned Typescript I am having trouble understanding how to correctly create my own container widget with a children
field, with a list of user defined Geometry
objects in it, and especially how to be able to access those “sub-views” (and sub models) from the “parent” view, and respond to change in their state, etc…
Is there a place to look at by example to learn how I might learn to use such a container in the ipywidgets world, using regular javascript? If not, has anyone written a general HTML type container that just has a list of HTML code stored as Unicode strings, perhaps?
More details on what I am doing in case it isn’t clear:
The shapely
library has very nice Jupyter interaction. You can view polygons this way:
import shapely.geometry as geo
p = geo.Polygon([ [0,0], [1,0], [1,1] ])
p
We can also get the svg
code for any shapely object:
In: p.svg()
Out: '<path fill-rule="evenodd" fill="#66cc99" stroke="#555555" stroke-width="2.0" opacity="0.6" d="M 0.0,0.0 L 1.0,0.0 L 1.0,1.0 L 0.0,0.0 z" />'
I have written this widget, which stores the SVG as a Unicode
field for any arbitrary shapely-like geometry (other than having events, I think the javascript side should have behavior very similar to the HTML widget, except I will want the tagName
and className
fields to specify a svg group tag: <g class="geoselector" >
– see javascript code at bottom):
import ipywidgets as widgets
from traitlets import Unicode, List, Instance
class Geometry(widgets.DOMWidget):
"""shapely.geometry object(s) represented as a group of svg element(s)."""
# TODO: add to_json, from_json methods for the comms system..??
_view_name = Unicode('GeometryView').tag(sync=True)
_view_module = Unicode('geoselector').tag(sync=True)
_model_name = Unicode('GeometryModel').tag(sync=True)
_model_module = Unicode('geoselector').tag(sync=True)
value = Unicode().tag(sync=True)
def __init__(self, geometry):
self.value = geometry.svg()
When the javascript part is added, this view seems to work fine by itself** (when enclosed in a <svg>
parent tag), and I can easily add a stylesheet and to give it the default, hover, and clicked, colors we want, etc etc.
Looking at the Box
code, I am thinking my SVG container should look something like this:
class SVGContainer(widgets.DOMWidget):
"""An SVG space to put SVG objects inside."""
_view_name = Unicode('SVGContainerView').tag(sync=True)
_view_module = Unicode('geoselector').tag(sync=True)
_model_name = Unicode('SVGContainerModel').tag(sync=True)
_model_module = Unicode('geoselector').tag(sync=True)
# unsure what to do with the **widget_serialization arguments to .tag()...
# is this required if Geometry has a to_json and from_json method?
children = List(trait=Instance(Geometry), help="List of children svg geometries").tag(
sync=True) # list of ALL the <g> children of the <svg>
selected = List().tag() # list of currently selected <g> indexes
def __init__(self, geometries):
self.children = [g.svg() for g in geometries]
self.selected = []
What I need to figure out is how to correct/add the missing pieces above, and to write the javascript side, so that I can access my sub-views and sub-models.
Here’s a start at the javascript part:
require.undef('geoselector');
define('geoselector', ["@jupyter-widgets/base"], function(widgets) {
var GeometryModel = widgets.DOMWidgetModel.extend({
// do stuff with the model here
});
var GeometryView = widgets.DOMWidgetView.extend({
tagName: 'g', // specify tag as <g> rather than <div>; does this break something?
className: 'geoselector', // specify class for all the <g> tags; does this break something?
initialize: function() {
GeometryView.__super__.initialize.apply(this, arguments);
this.model.on('change:value', this.render, this);
},
events: {
'click': 'onClick',
},
onClick: function() {
this.el.classList.toggle('selected') // switch between selected and not selected when clicked
},
render: function() {
GeometryView.__super__.render.apply(this, arguments);
this.el.innerHTML = this.model.get('value');
},
});
var SVGContainerModel = widgets.DOMWidgetModel.extend({
toggleListIndex: function(index) {
// add or remove the index from this.selected here
}
// do more stuff with the model here
});
var SVGContainerView = widgets.DOMWidgetView.extend({
tagName: 'svg',
initialize: function() {
SVGContainerView.__super__.initialize.apply(this, arguments);
this.model.on('change:selected', this.renderSubviews, this);
// how to add a listener to the clicks of each sub-view...?
this.model.on('click:children', this.subviewClicked, this);
},
render: function() {
SVGContainerView.__super__.render.apply(this, arguments);
// nothing need be done here-- rendering handled in sub-views
},
renderSubviews: function() {
// how to trigger re-rendering of all the subviews?
},
subviewClicked: function(index) {
// add/remove selected index to the model list of current indexes
this.model.toggleListIndex(index)
},
});
return {
GeometryView : GeometryView,
GeometryModel : GeometryModel,
SVGContainerView : SVGContainerView,
SVGContainerModel : SVGContainerModel,
};
});
** actually I seem to be getting this error right now; trying to figure it out: AttributeError: 'Geometry' object has no attribute '_model_id'
– i think this is related to not yet adding to_json
and from_json
methods?
Issue Analytics
- State:
- Created 4 years ago
- Comments:7 (7 by maintainers)
Top GitHub Comments
Ah, if you override
__init__
in widgets, you should call the super class’__init__
forwarding the kwargs.You will also want to add
**widgets_serialization
to youchildren
trait.@SylvainCorlay
Can I ask you to look at the toy notebook below? It is giving me javascript errors when
this.create_child_view
is called on a sub model object in the parent viewinitialize
method:https://nbviewer.jupyter.org/urls/dl.dropbox.com/s/j7m1fuotr3tqae1/broken_subwidget_example.ipynb