Skip to content

Commit

Permalink
SVG: work around Notebook issues.
Browse files Browse the repository at this point in the history
Work around ipython/ipython#1866 in
Jupyter Notebook by prepending a unique slug to the id fields within
the SVG.

This also requires updating any filter, marker-start, and marker-end nodes
which have a 'url(#id)' reference to refer to the beslugged version.

If ever you see an issue where the SVG on the Model tab is fine but the
rest are broken in some way, suspect a cross-SVG ID referencing issue.
The Model tab is the first one rendered and will "win" any such
conflict.

JupyterLab renders SVGs within an iframe and is not impacted. Only
Notebook (and Voila) really need to have the IDs uniquified. As it
doesn't hurt JupyterLab to do this, we do so unconditionally.
  • Loading branch information
DentonGentry committed Aug 22, 2019
1 parent 680b6b3 commit 59caaa8
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 22 deletions.
35 changes: 17 additions & 18 deletions ui/charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,15 @@ def __init__(self, mutable=True, is_jupyterlab=True):
pd.set_option('display.max_columns', 200)
pd.set_option('display.max_rows', 200)
self.is_jupyterlab = is_jupyterlab
if not self.is_jupyterlab:
self.is_jupyternb = not is_jupyterlab
if self.is_jupyternb:
alt.renderers.enable('notebook')
self.vega_widget = importlib.import_module('vega.widget')
qgrid.on(names=['cell_edited', 'row_added', 'row_removed'], handler=vma_qgrid_modified)
all_solutions = pd.read_csv(os.path.join('data', 'overview', 'solutions.csv'),
index_col=False, skipinitialspace=True, header=0,
skip_blank_lines=True, comment='#')
soln_results = pd.read_csv(os.path.join('data', 'overview', 'soln_results.csv'),
index_col=False, skipinitialspace=True, header=0,
skip_blank_lines=True, comment='#')
all_solutions = pd.read_csv(os.path.join('data', 'overview', 'solutions.csv'), header=0,
index_col=False, skipinitialspace=True, skip_blank_lines=True, comment='#')
soln_results = pd.read_csv(os.path.join('data', 'overview', 'soln_results.csv'), header=0,
index_col=False, skipinitialspace=True, skip_blank_lines=True, comment='#')
all_solutions = all_solutions.merge(soln_results, on='Solution', how='left')
sectors = all_solutions.pivot_table(index='Sector', aggfunc=sum)
all_solutions['SectorCO2eq'] = all_solutions.apply(
Expand Down Expand Up @@ -1075,7 +1074,7 @@ def get_model_tab(self, solutions):
modelmap = ipywidgets.Output()
with modelmap:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(model=c)))
data=ui.modelmap.get_model_overview_svg(model=c, prefix='model')))
editor = self.get_scenario_editor_for_solution(soln_mod=c)
divider = ipywidgets.HTML(value='<br/><hr/><br/>')
children.append(ipywidgets.VBox(children=[modelmap, divider, editor]))
Expand Down Expand Up @@ -1225,7 +1224,7 @@ def get_first_cost_tab(self, solutions):
fc_model = ipywidgets.Output()
with fc_model:
IPython.display.display(IPython.display.SVG(data=ui.modelmap.get_model_overview_svg(
model=sys.modules[s.__module__], highlights=['fc'], width=350)))
model=sys.modules[s.__module__], highlights=['fc'], width=350, prefix='fc')))
fc_chart = ipywidgets.Output()
with fc_chart:
melted_df = df.reset_index().melt('Year', value_name='cost', var_name='column')
Expand Down Expand Up @@ -1269,8 +1268,8 @@ def get_operating_cost_tab(self, solutions):
oc_model = ipywidgets.Output()
with oc_model:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(
model=sys.modules[s.__module__], highlights=['oc'], width=350)))
data=ui.modelmap.get_model_overview_svg(model=sys.modules[s.__module__],
highlights=['oc'], width=350, prefix='oc')))
oc_chart = ipywidgets.Output()
with oc_chart:
melted_df = df.reset_index().melt('Year', value_name='cost', var_name='column')
Expand Down Expand Up @@ -1313,7 +1312,7 @@ def get_adoption_data_tab(self, solutions):
ad_model = ipywidgets.Output()
with ad_model:
IPython.display.display(IPython.display.SVG(data=ui.modelmap.get_model_overview_svg(
model=sys.modules[s.__module__],
model=sys.modules[s.__module__], prefix='ad',
highlights=['ad', 'capds', 'caref', 'sc', 'ht'], width=350)))

ad_chart = ipywidgets.Output()
Expand Down Expand Up @@ -1443,7 +1442,7 @@ def get_tam_data_tab(self, solutions):
with tm_model:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(model=sys.modules[s.__module__],
highlights=['tm'], width=250)))
highlights=['tm'], width=250, prefix='tm')))

tm_geo_pds = ipywidgets.Output()
with tm_geo_pds:
Expand Down Expand Up @@ -1540,7 +1539,7 @@ def get_emissions_tab(self, solutions):
with c2_model:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(model=sys.modules[s.__module__],
highlights=['c2'], width=350)))
highlights=['c2'], width=350, prefix='em')))

# FaIR results
CFTb = s.c2.FaIR_CFT_baseline()
Expand Down Expand Up @@ -1630,8 +1629,8 @@ def get_aez_data_tab(self, solutions):
ae_model = ipywidgets.Output()
with ae_model:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(
model=sys.modules[s.__module__], highlights=['ae'], width=350)))
data=ui.modelmap.get_model_overview_svg(model=sys.modules[s.__module__],
highlights=['ae'], width=350, prefix='aez')))
children.append(ipywidgets.HBox([ae_table, ae_model]))

if children:
Expand Down Expand Up @@ -1661,8 +1660,8 @@ def get_dez_data_tab(self, solutions):
de_model = ipywidgets.Output()
with de_model:
IPython.display.display(IPython.display.SVG(
data=ui.modelmap.get_model_overview_svg(
model=sys.modules[s.__module__], highlights=['de'], width=350)))
data=ui.modelmap.get_model_overview_svg(model=sys.modules[s.__module__],
highlights=['de'], width=350, prefix='dez')))
children.append(ipywidgets.HBox([de_table, de_model]))

if children:
Expand Down
34 changes: 30 additions & 4 deletions ui/modelmap.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Manipulates SVG of the model operation as an overview for the researcher."""

import os.path
import random
import string
import xml.etree.ElementTree as ET
from model import advanced_controls

def get_model_overview_svg(model, highlights=None, width=None):
def get_model_overview_svg(model, highlights=None, width=None, prefix=None):
"""Return an SVG containing only the modules used in this solution."""
is_land = is_ocean = False
has_default_pds_ad = has_custom_pds_ad = has_s_curve_pds_ad = False
Expand Down Expand Up @@ -48,9 +50,13 @@ def get_model_overview_svg(model, highlights=None, width=None):
if width is not None:
resize(tree, width)

if prefix is not None:
randomize_ids(tree, prefix)

# Jupyter Notebook display(SVG()) does not tolerate explicit namespaces on the SVG tags.
# Jupyterlab does, but we support use of the Notebook for https://github.com/QuantStack/voila
# Remove the ns0: namespace prefixes and emit a default namespace.
# We do this for Lab as well because it doesn't hurt and looks cleaner as XML.
tree.getroot().attrib['xmlns'] = 'http://www.w3.org/2000/svg'
s = ET.tostring(tree.getroot(), encoding='utf8', method='xml')
return s.replace(b'ns0:', b'')
Expand All @@ -72,18 +78,20 @@ def delete_module(tree, name):
for child in list(edge):
edge.remove(child)


def node_color_fill(tree, name):
"""Color one node in the tree.
Note that a list of modules is passed in, some of which may not exist in
this specific solution because the code passing in the highlights is generic.
It is not an error for name to not exist.
Note that a list of modules is passed in, some of which may not exist in
this specific solution because the code passing in the highlights is generic.
It is not an error for name to not exist.
"""
node = tree.find(r'.//{http://www.w3.org/2000/svg}g[@id="' + name + '"]')
if node is not None and len(node) >= 1:
rect = node[1]
rect.attrib['fill'] = '#3D9970'


def resize(tree, width):
"""Adjust the viewPort to fit a new width."""
svg = tree.getroot()
Expand All @@ -93,3 +101,21 @@ def resize(tree, width):
new_height = old_height * ratio
svg.attrib['width'] = str(width)
svg.attrib['height'] = str(new_height)


def randomize_ids(tree, prefix):
"""Attach random text to id fields.
Work around https://github.com/ipython/ipython/issues/1866 in Jupyter Notebook
by prepending a random slug to the id fields within the SVG.
"""
for elem in tree.getroot().iter():
if 'id' in elem.attrib:
orig = elem.attrib['id']
elem.attrib['id'] = prefix + '_' + orig
for tag in ['filter', 'marker-end', 'marker-start']:
if tag not in elem.attrib:
continue
orig = elem.attrib[tag]
if 'url(#' in orig:
elem.attrib[tag] = orig.replace('url(#', f'url(#{prefix}_')
7 changes: 7 additions & 0 deletions ui/tests/test_modelmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ def test_ocean():
node = tree.find(r'.//{http://www.w3.org/2000/svg}g[@id="sc"]')
assert node is not None
assert len(node) == 0

def test_randomize_ids():
xml = ui.modelmap.get_model_overview_svg(model=solarpvutil, prefix='test')
tree = ET.fromstring(xml)
# The node id should have been randomized now, not "sc"
node = tree.find(r'.//{http://www.w3.org/2000/svg}g[@id="sc"]')
assert node is None

0 comments on commit 59caaa8

Please sign in to comment.