Migrating from ipyelk 2.x to 3.0#
This page is for you if a notebook or a library of yours uses ipyelk 2.x. It tells you
how to change that code for ipyelk 3.0. Each section describes one breaking change. A
breaking change is a change after which some 2.x code fails or behaves differently.
Each section tells you:
what changed
whose code the change affects
the 2.x code and the 3.0 code
the error or the symptom that the 2.x code shows on 3.0
the pull request that made the change
The Changelog lists every change, including the changes that do not break 2.x code.
Find the changes that affect you#
Do these steps before you change your code:
Search your code for the removed names in the table in The errors for removed names. The table lists the names that raise
DeprecatedAPIErrororDeprecatedImportError.Run your notebooks and your tests with
ipyelk3.0.If you see
DeprecatedAPIErrororDeprecatedImportError, read the message. The message names the replacement.If you see a
DeprecationWarning, read the message. The message names the replacement. The table in The warnings for deprecated names lists the deprecated names.Read the sections about changes that do not raise an error:
The errors for removed names#
A removed name is an attribute, a constructor keyword or a module-level name that 3.0
deleted. 3.0 has no alias and no deprecation warning for a removed name. Instead,
ipyelk raises an error when your code uses a removed name. The message of the error
tells you why the name was removed and what to use instead. Every 3.x release keeps
these errors.
ipyelk raises one of two errors:
ipyelk.exceptions.DeprecatedAPIErrorfor a removed attribute or constructor keyword. It is a subclass ofAttributeError. Thushasattr()returnsFalsefor a removed attribute, andgetattr(obj, name, default)returns the default.ipyelk.exceptions.DeprecatedImportErrorfor a removed module or module-level name. It is a subclass ofImportError. Thusfrom ipyelk.tools import Zoomshows the message, not only “cannot import name”.
The two errors have the same base class, ipyelk.exceptions.DeprecatedAPI. One
except DeprecatedAPI clause catches both errors.
# 2.x
from ipyelk.tools import Painter
Painter(cssClasses="highlight")
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Painter.cssClasses was removed in ipyelk 3.0: it was an unfinished placeholder that painted nothing. Use painter.paint(ids, *css_classes) / painter.styles (id -> classes applied in the view only). No alias: this error is raised throughout 3.x.
# 3.0
from ipyelk.exceptions import DeprecatedAPI
from ipyelk.tools import Hover
print(hasattr(Hover(), "ids"))
try:
from ipyelk.tools import Zoom
except DeprecatedAPI as error:
print(type(error).__name__)
This table lists the removed names that raise DeprecatedAPIError or
DeprecatedImportError. Some sections of this page describe other removed names, which
raise no error or a different error. The last column of the table links to the section
that tells you what to use instead.
Removed name |
Error |
Section |
|---|---|---|
|
|
|
|
|
|
|
|
|
assigning |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The warnings for deprecated names#
A deprecated name is a name that worked in 2.x and still works in 3.x. When your code
uses a deprecated name, ipyelk shows a DeprecationWarning. The message names the
replacement. ipyelk 4.0 removes Registry, MarkIndex.context and
ipyelk.elements.layout_options.model.strip_none.
Deprecated name |
Warning |
Section |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Tools#
A tool is an object of the class ipyelk.tools.Tool or of one of its subclasses. Tools
control a diagram: Selection, Hover, FitTool and ToggleCollapsedTool are tools.
Tool invocation and lifecycle names#
In 2.x, some tool names had more than one role. In 3.0, each name has one role. The sections below describe each name. The changes are in #157.
The examples in these sections use this tool:
# both versions: a tool that prints when it runs
from ipyelk.tools import Tool
class Hello(Tool):
async def run(self):
print("hello")
tool = Hello()
Request a run with trigger()#
A run is one execution of the run() method of a tool. Tool.trigger() requests a run.
A call to trigger() is a request. trigger() cancels a run that did not finish,
schedules run(), and returns the asyncio.Task. Tool.handler is removed.
This change affects code that calls tool.handler() or passes tool.handler as a
callback. The change is in #157.
# 2.x
tool.handler()
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Tool.handler was removed in ipyelk 3.0. It was three different things (the execution method, ToolButton's click callback trait, and SetTool's trait observer). Call tool.trigger() to request execution; pass ToolButton(on_click=callback) for a button callback (called as callback(tool)).
# 3.0
import ipywidgets as W
button = W.Button(description="Hello")
button.on_click(tool.trigger) # trigger() ignores the button argument
await tool.trigger()
trigger() raises NotImplementedError for a tool that has no run(). In 2.x, the run
failed later, inside the task. Selection, Hover and ToolButton have no run().
# 3.0
from ipyelk.tools import Selection
try:
Selection().trigger()
except NotImplementedError as error:
print(error)
Register on_start and on_done callbacks#
Tool.on_start(callback) replaces Tool.on_run(callback). Tool.on_done(callback) is
now a method that registers a callback. In 2.x, on_done was a trait that held one
callback.
An
on_startcallback fires when the run starts. In 2.x, anon_runcallback fired after a successful run, in spite of its name.An
on_donecallback fires only after a successful run. It does not fire when the run fails, when a newer request cancels it, or whentrigger()rejects the request.A tool can have more than one
on_startcallback and more than oneon_donecallback. TheDiagramthat owns a tool registers itsrefresh()method as anon_donecallback.remove=Trueremoves a callback.Each callback receives the tool as its argument.
This change affects code that registers on_run callbacks or assigns on_done. If a
2.x on_run callback must see the result of the run, register it with on_done in 3.0.
The change is in #157.
# 2.x
tool.on_run(lambda tool: print("ran"))
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Tool.on_run was removed in ipyelk 3.0. Despite its name, on_run callbacks ran AFTER a successful run, not when the run started, so the name was ambiguous. Register tool.on_start(callback) for the actual start of the work, or tool.on_done(callback) for success only (both are called as callback(tool); on_done never fires on failure, cancellation, or a rejected trigger). No forwarding: this error is raised throughout 3.x.
# 2.x
tool.on_done = lambda: print("done")
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Tool.on_done is a registration method in ipyelk 3.0, not an assignable callable: a single slot let one assignment silently replace another listener's callback (the Diagram registers its own refresh on on_done). Register tool.on_done(callback) (called as callback(tool); remove=True unregisters). This error is raised throughout 3.x.
# 3.0
tool.on_start(lambda tool: print("started"))
tool.on_done(lambda tool: print("done"))
await tool.trigger()
In 2.x, a tool could call self.on_done() to refresh its diagram, because the Diagram
assigned its refresh() method to on_done. A tool that did its work outside run()
used this call. In 3.0, the call raises TypeError, because on_done registers a
callback. This change affects subclasses of Tool that call self.on_done().
# 2.x: a tool that did its work outside run() and refreshed the diagram
from ipyelk import from_element
from ipyelk.elements import Node
from ipyelk.pipes import flows as F
class Hide(Tool):
def hide(self):
self.tee.inlet.flow = self.reports # change the elements here
self.on_done()
hide = Hide(reports=(F.Node.hidden,))
from_element(Node(id="root")).register_tool(hide)
hide.hide()
The 2.x code raises this error on 3.0:
TypeError: Tool.on_done() missing 1 required positional argument: 'callback'
In 3.0, call self.trigger() instead. After a successful run, the tool records its
change reports and calls its on_done callbacks, which include Diagram.refresh(). If
the work stays outside run(), give the tool a run() that does nothing.
# 3.0: trigger() records the change reports and refreshes the diagram
from ipyelk import from_element
from ipyelk.elements import Node
from ipyelk.pipes import flows as F
class Hide(Tool):
def hide(self):
# change the elements here
return self.trigger()
async def run(self):
pass # the work is in hide()
hide = Hide(reports=(F.Node.hidden,))
from_element(Node(id="root")).register_tool(hide)
await hide.hide()
Callbacks receive the object that calls them#
Every callback receives the object that calls it. Toolbar calls on_close(toolbar)
when you click its close button. In 2.x, Toolbar called on_close() with no argument.
Diagram.refresh(sender=None) ignores its argument, so you can register it as an
on_done callback. In 2.x, the argument was change.
This change affects code that sets Toolbar.on_close to a function with no parameters.
The change is in #157.
# both versions
from ipyelk.tools import Toolbar
toolbar = Toolbar()
# 2.x
toolbar.on_close = lambda: print("closed")
toolbar.close_btn.click()
On 3.0, the callback does not run. In a notebook, the click shows this error:
TypeError: <lambda>() takes 0 positional arguments but 1 was given
# 3.0
toolbar.on_close = lambda toolbar: print("closed")
toolbar.close_btn.click()
SetTool observes active with a private method#
SetTool moves its CSS classes when active changes. In 2.x, the observer that did
this was the public method SetTool.handler. In 3.0, the observer is the private method
_update_active. It is not a way to request a run.
This change affects code that calls SetTool.handler directly. The change is in #157.
# both versions
from ipyelk.elements import Node
from ipyelk.tools import SetTool
node = Node(id="n1")
set_tool = SetTool()
# 2.x
set_tool.handler(None)
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Tool.handler was removed in ipyelk 3.0. It was three different things (the execution method, ToolButton's click callback trait, and SetTool's trait observer). Call tool.trigger() to request execution; pass ToolButton(on_click=callback) for a button callback (called as callback(tool)).
# 3.0: set active, and the observer moves the classes
set_tool.active = (node,)
print(node.properties.cssClasses)
Disable a tool with disabled#
Tool.disabled replaces the disable trait. In 2.x, no code read disable, so it
disabled nothing. In 3.0, disabled stops the tool:
trigger()raisesRuntimeError. Nothing is scheduled, and no callback fires.A click on a
ToolButtondoes not run its callback. In a notebook, the click shows theRuntimeError.Every control under
tool.uithat has adisabledtrait is disabled. Boxes and progress bars have nodisabledtrait.
This change affects code that sets tool.disable. The change is in #157.
# both versions: a tool that prints when it runs
from ipyelk.tools import Tool
class Hello(Tool):
async def run(self):
print("hello")
tool = Hello()
# 2.x
tool.disable = True
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Tool.disable was removed in ipyelk 3.0: nothing read it, so it never disabled anything. Use tool.disabled (the ipywidgets name), which rejects trigger() and clicks and disables the controls under tool.ui.
# 3.0
tool.disabled = True
try:
tool.trigger()
except RuntimeError as error:
print(error)
Bind a tool after you make it#
A dependency of a tool is a trait that the tool must have before it can run. A tool that
has a dependency with the value None is an unbound tool. In 3.0, ipyelk supports
unbound tools. You can make a tool first and bind it later, with
Diagram.register_tool() or an assignment.
These dependencies are None until you bind them:
PipelineProgressBar.pipeToggleCollapsedTool.selectionSetTool.selection
In 2.x, a read of one of these traits before you set it raised TraitError. In 3.0, an
unbound tool disables its controls. Tool.missing_dependencies() names the dependencies
that are None, and trigger() raises RuntimeError that names them.
This change affects code that expects TraitError when it reads one of these traits.
The change is in #157.
# 2.x
from ipyelk.tools import ToggleCollapsedTool
print(ToggleCollapsedTool().selection)
In 2.x, the code raised TraitError. On 3.0, it prints:
None
# 3.0
from ipyelk import Diagram
from ipyelk.tools import ToggleCollapsedTool
tool = ToggleCollapsedTool()
print(tool.missing_dependencies())
try:
tool.trigger()
except RuntimeError as error:
print(error)
Diagram().register_tool(tool)
print(tool.missing_dependencies())
Tools add their change reports to the pending flow#
A pipe is an ipyelk.pipes.Pipe, one stage in the processing of a diagram. A tool sends
its changes to the pipe in tool.tee. A change report is a string in Tool.reports
that names a kind of change, for example "node.properties.hidden". The pending flow is
the tuple pipe.inlet.flow. It holds the change reports that the next call to
Diagram.refresh() processes.
In 2.x, a tool replaced the pending flow with its own change reports. Thus, two tools
that ran in the same turn of the event loop lost the change reports of the first tool.
In 3.0, a tool adds its change reports to the pending flow when its run finishes. The
new method Tool.record_reports() keeps the order and drops duplicates.
ToggleCollapsedTool.run() no longer sets the pending flow.
This change affects subclasses of Tool that set self.tee.inlet.flow. The 2.x code
does not raise an error. If it replaces the pending flow, it still drops the change
reports of other tools. The change is in #157.
# both versions: a pipe with a change report from another tool
from ipyelk.pipes import Pipe
from ipyelk.pipes import flows as F
from ipyelk.tools import Tool
pipe = Pipe()
pipe.inlet.flow = (F.Layout,)
# 2.x: the tool replaced the pending flow
class Hide(Tool):
async def run(self):
self.tee.inlet.flow = self.reports
await Hide(tee=pipe, reports=(F.Node.hidden,)).trigger()
print(pipe.inlet.flow)
On 3.0, the 2.x code prints a pending flow without the layout change report of the
other tool:
('node.properties.hidden',)
# 3.0: let the base class record the change reports when the run finishes
class Hide(Tool):
async def run(self):
pass # change the elements here
await Hide(tee=pipe, reports=(F.Node.hidden,)).trigger()
print(pipe.inlet.flow)
If a tool must record its change reports before its run finishes, call
self.record_reports().
Resolve a selection that has unknown ids#
Selection.elements() gets the elements of the selected ids. By default, it raises
NotFoundError for an id that is not in the index of the diagram. The new keyword
elements(strict=False) skips unknown ids. The new method missing_ids() returns them.
Both keep the order of ids.
ipyelk does not remove unknown ids from Selection.ids. Thus, you can select ids
before the first layout. Selection.get_index() now makes the index when the index has
no elements. In 2.x, get_index() never made it.
This change affects code that catches NotFoundError to skip unknown ids. The change is
in #157.
# both versions: a diagram with a selection of one known id and one unknown id
from ipyelk import from_element
from ipyelk.elements import Node
diagram = from_element(Node(id="root", children=[Node(id="n1")]))
selection = diagram.view.selection
selection.ids = ("n1", "missing")
# 2.x
list(selection.elements())
The 2.x code still raises NotFoundError on 3.0, and the message names the unknown id:
NotFoundError: Element with id:missing not in index
# 3.0
print([element.id for element in selection.elements(strict=False)])
print(selection.missing_ids())
Compare Selection.ids to a tuple#
Selection.ids is a tuple, in 2.x and in 3.0. 3.0 documents it as a tuple. A comparison
of ids to a list is always False. The 01_Linking example now compares ids to a
tuple.
This change affects code that compares Selection.ids to a list. The change is in
#157.
# both versions
from ipyelk.tools import Selection
selection = Selection(ids=("n1",))
# 2.x
print(selection.ids == ["n1"])
The 2.x code prints:
False
# 3.0
print(selection.ids == ("n1",))
Type-check Toolbar.order() as tool widgets#
Toolbar.order() returns a dict[int, list[DOMWidget]], and Toolbar.tool_order()
returns a list[DOMWidget]. These are the ui widgets of the tools, not the tools. The
methods returned these widgets in 2.x too, but their 2.x type annotations named Tool.
A tool without a ui is not in the result.
This change affects only code that a type checker checks against the 2.x annotations. The code runs as before. The change is in #157.
Read the hovered element from Hover.hovered_id#
Hover.hovered_id replaces Hover.ids. Its type is str | None, and its default is
None. In spite of its name, Hover.ids held only one id, as a string. The value has
the same type, but the trait has a new name and can be None. ipyelk has no alias for
ids. Selection.ids does not change and is still a tuple.
This change affects code that reads or observes ids on a Hover tool, for example
diagram.view.hover. The change is in #157.
# both versions
from ipyelk.tools import Hover
hover = Hover()
# 2.x
print(hover.ids)
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Hover.ids was removed in ipyelk 3.0: despite the plural name it only ever held one id (a str), and it never cleared on pointer leave. Use hover.hovered_id (str | None; None once the pointer leaves). No alias: this error is raised throughout 3.x.
# 3.0
print(hover.hovered_id) # the id under the pointer, or None
hover.hovered_id = "n1" # highlight an element from the kernel
hover.hovered_id = None # remove the highlight
Expect None when the pointer leaves an element#
When the pointer leaves the elements of the diagram, hovered_id becomes None. In
2.x, ids kept the last hovered id.
This change affects observers of the hovered id that expect a string. The 2.x code does
not raise an error when you rename ids to hovered_id. But an observer that uses
string methods fails when the pointer leaves. The change is in #157.
# both versions
from ipyelk.tools import Hover
hover = Hover()
# 2.x, with ids renamed to hovered_id: the observer expects a string
hover.observe(lambda change: print(change.new.upper()), "hovered_id")
hover.hovered_id = "n1"
hover.hovered_id = None # what the browser writes when the pointer leaves
The 2.x code prints N1 and then raises this error on 3.0:
AttributeError: 'NoneType' object has no attribute 'upper'
# 3.0
def show(change):
print("nothing" if change.new is None else change.new.upper())
hover.observe(show, "hovered_id")
hover.hovered_id = "n1"
hover.hovered_id = None
Paint elements with Viewer.painter#
A viewer is the Viewer widget of a diagram, diagram.view. A view is one display of a
viewer in the browser. Viewer.painter is an ipyelk.tools.Painter. In 3.0, it adds
CSS classes to the elements in each view of the viewer. The painted classes are
temporary. They do not change properties.cssClasses or the JSON that ipyelk sends to
the Eclipse Layout Kernel (ELK), the library that lays out the diagram.
paint(ids, *css_classes)adds classes to the elements.idsis one id, one element, or a sequence of ids and elements.unpaint(ids, *css_classes)removes classes. With no classes, it removes the ids.clear()removes every class that the painter added. It never removes a class that is inproperties.cssClasses.stylesis the synceddict[str, tuple[str, ...]]of ids to painted classes.painted_ids,elements(strict=True)andmissing_ids()work like the methods ofSelection.
The 2.x placeholders cssClasses, marks and name are removed. They painted nothing.
This change affects code that sets cssClasses, marks or name on a Painter. The
change is in #157.
# 2.x
from ipyelk.tools import Painter
Painter(marks=["n1"])
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Painter.marks was removed in ipyelk 3.0: it was an unfinished placeholder that painted nothing. Use painter.paint(ids_or_elements, *css_classes); painter.painted_ids lists what is painted. No alias: this error is raised throughout 3.x.
# 3.0
from ipyelk import Diagram
painter = Diagram().view.painter
painter.paint(["n1", "n2"], "highlight")
painter.unpaint("n1", "highlight")
print(painter.styles)
painter.clear()
Diagrams and viewers#
These sections describe the Diagram and its viewer, diagram.view. A viewer can have
more than one view.
Override fit() and center() with the full signatures#
Viewer.fit() and Viewer.center() have the same signatures as the methods of
SprottyViewer. Sprotty is the library that draws the diagram in the browser.
fit(model_ids=None, animate=None, max_zoom=None, padding=None)center(model_ids=None, animate=None, retain_zoom=None)
model_ids is one id or a sequence of ids. A str is one id, never a sequence of
characters. A list[str] still works. On the generic Viewer, the two methods do
nothing. In 2.x, the Fit and Center buttons of the generic Viewer raised TypeError.
This change affects subclasses of Viewer that override fit() or center() with the
2.x signature, which had no parameters. The change is in #157.
# 2.x
from ipyelk.diagram import Viewer
class MyViewer(Viewer):
def fit(self):
print("fit")
MyViewer().fit("n1")
The 2.x code raises this error on 3.0:
TypeError: MyViewer.fit() takes 1 positional argument but 2 were given
# 3.0
from ipyelk.diagram import Viewer
class MyViewer(Viewer):
def fit(self, model_ids=None, animate=None, max_zoom=None, padding=None):
print("fit", model_ids)
view = MyViewer()
view.fit("n1")
view.fit_tool.ui.click()
Add a ControlOverlay before you use it#
Viewer.control_overlay is None by default. In 2.x, every viewer had a
ControlOverlay. To show widgets over the diagram, assign a ControlOverlay first. The
browser shows no overlay when control_overlay is None or has no children. In 2.x,
every selection made an empty overlay node in the page. The browser draws the overlay
again when its children change.
This change affects code that sets view.control_overlay.children without an assignment
of control_overlay first. The change is in #157.
# both versions
import ipywidgets as W
from ipyelk import Diagram
view = Diagram().view
button = W.Button(description="Hello")
# 2.x
view.control_overlay.children = [button]
The 2.x code raises this error on 3.0:
AttributeError: 'NoneType' object has no attribute 'children'
# 3.0
from ipyelk.tools import ControlOverlay
view.control_overlay = ControlOverlay()
view.control_overlay.children = [button]
view.control_overlay.children = [] # shows nothing
Read the camera from Viewer.viewport#
The camera of a view is the part of the diagram that the view shows. Viewer.viewport
is an ipyelk.tools.Viewport. When the camera of a view moves, the browser writes the
new camera to Viewer.viewport. This write is a camera report. Thus, Viewport holds
the camera of the view that sent the last camera report. Viewport replaces the Zoom
and Pan tools and the Viewer.viewed trait. No code wrote to these 2.x names.
Viewport has five traits, and the browser writes them together in one camera report:
view_idis the id of the view that sent the camera report.originis the position of the top-left corner of the view, in diagram coordinates.zoomis the number of CSS pixels for each diagram unit.canvas_sizeis the size of the view, in CSS pixels.viewed_idsis the ids of the elements that the view shows, in diagram order. Edges are not inviewed_ids.
Each trait is None until the first camera report. When an observer of one trait runs,
the other traits already hold the values of the same camera report. Kernel code cannot
set the traits. Each view keeps its own camera, and each camera report names its view.
This change affects code that reads view.zoom, view.pan or view.viewed. The change
is in #157.
# both versions
from ipyelk import Diagram
view = Diagram().view
# 2.x
print(view.zoom.zoom)
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Viewer.zoom was removed in ipyelk 3.0: the Zoom tool was a placeholder that nothing ever wrote. Read viewer.viewport.zoom (the browser's latest report, None until one arrives) and move a view with viewer.set_viewport(zoom=...). No alias: this error is raised throughout 3.x.
# 3.0
viewport = view.viewport
print(viewport.zoom, viewport.origin, viewport.viewed_ids) # None before the first camera report
viewport.observe(lambda change: print(viewport.view_id), "origin")
Move the camera with set_viewport()#
SprottyViewer.set_viewport(*, origin=None, zoom=None, animate=True, view_id=None)
moves the camera. It is a command, not a trait. origin=None and zoom=None keep the
current values of the view. view_id=None moves every view of the viewer. After the
move, the browser sends a camera report to Viewer.viewport.
This change affects code that sets the traits of the 2.x Zoom or Pan tools to move
the camera. These traits did not move it. The change is in #157.
# both versions
from ipyelk import Diagram
view = Diagram().view
# 2.x
view.pan.origin = (0, 0)
The 2.x code raises this error on 3.0:
DeprecatedAPIError: Viewer.pan was removed in ipyelk 3.0: the Pan tool was a placeholder that nothing ever wrote. Read viewer.viewport.origin and viewer.viewport.canvas_size (the browser's latest report, None until one arrives) and move a view with viewer.set_viewport(origin=(x, y)). No alias: this error is raised throughout 3.x.
# 3.0
view.set_viewport(origin=(0, 0), zoom=1.5) # every view, animated
view.set_viewport(zoom=2, animate=False, view_id=view.viewport.view_id)
Give each diagram its own tools and toolbar#
Passing another open diagram’s tools or toolbar, assigning them later, or
registering one of its tools now raises TraitError before any binding changes. In 2.x,
this redirected tools to the second diagram and closing either diagram could close the
other’s tools. Create fresh tools and a fresh toolbar for each independent diagram. To
show the same diagram in two places, display the same Diagram twice.
A custom tool can move between diagrams: first remove it from the old diagram’s tools,
then call new_diagram.register_tool(tool). The viewer’s own selection, hover,
viewport, painter, fit and center tools stay with that viewer even when removed from
Diagram.tools; use a fresh tool for a different viewer.
Make a new diagram or tool after you close one#
A closed Diagram, Pipe or tool raises ipyelk.exceptions.ClosedError when you use
it. A Pipeline is a pipe that runs a list of other pipes, its stages. A closed widget
has no connection to the browser, so a call to it cannot reach the browser. In 2.x, the
call did nothing, and no error showed.
On a closed
Diagram,refresh(),register_tool()and an assignment tosource,pipe,view,tools,toolbar,symbolsorstyleraiseClosedError.On a closed
Pipe,schedule_run()raisesClosedError. On a closedPipeline,run()and an assignment topipesraise it too.A refused
pipesassignment keeps the stages. In 2.x, it removed them.A diagram whose pipe is closed raises
ClosedErroronrefresh().When a pipeline closes while it processes a diagram, it stops before the next stage. The task that
refresh()returned is then cancelled, soawait diagram.refresh()raisesasyncio.CancelledError.When a run fails just before its pipe closes, the task keeps the exception. The closed pipe does not call its
on_errorcallback. It logs a warning that names the exception.A diagram closes its tools when it closes. On a closed tool,
trigger()raisesClosedError. In a notebook, a click on a closedToolButtonshows the error.A second
close()still does nothing.
ClosedError is a subclass of traitlets.TraitError and of RuntimeError. Thus, an
except TraitError clause or an except RuntimeError clause catches it. The new
properties Diagram.closed and Pipe.closed are True after a call to close().
This change affects code that uses a diagram, a pipe or a tool after close(). The
change is in #202.
# both versions
from ipyelk import Diagram, from_element
from ipyelk.elements import Node
diagram = from_element(Node(id="root"))
diagram.close()
# 2.x
diagram.refresh()
The 2.x code raises this error on 3.0:
ClosedError: Diagram is closed and cannot refresh; build a new Diagram
# 3.0: make a new diagram from the source, which stays open
if diagram.closed:
diagram = Diagram(source=diagram.source)
diagram.refresh()
The ClosedError of a closed tool names the class of the tool, as the ClosedError of
a closed diagram does. The new property Tool.closed is True after a call to
close(). The change is in #226.
# 2.x
diagram.view.fit_tool.ui.click()
In a notebook, the click shows this error on 3.0:
ClosedError: FitTool is closed and cannot run; build a new FitTool
# 3.0: make a new diagram, which makes new tools
fit = diagram.view.fit_tool
if fit.closed:
diagram = Diagram(source=diagram.source)
fit = diagram.view.fit_tool
fit.ui.click()
Set layout, children, inlet and outlet before you close#
Four more assignments raise ClosedError after close():
On a closed
Diagram, an assignment toDiagram.layoutorDiagram.children.On a closed
Pipeline, an assignment toPipeline.inletorPipeline.outlet.
In 2.x, these assignments did not raise. A closed widget has no connection to the
browser, so the browser did not show the change. The message of the ClosedError is the
same as for the other assignments in the section above.
This change affects code that sets one of these four traits after close(). The change
is in #198.
# both versions
import ipywidgets as W
from ipyelk import Diagram, from_element
from ipyelk.elements import Node
diagram = from_element(Node(id="root"))
diagram.close()
# 2.x
diagram.layout = W.Layout(height="400px")
The 2.x code raises this error on 3.0:
ClosedError: Diagram is closed and cannot set layout; build a new Diagram
# 3.0: make a new diagram with the layout
diagram = Diagram(source=diagram.source, layout=W.Layout(height="400px"))
Give each open diagram its own source#
A source is the MarkElementWidget that holds the elements of a diagram,
Diagram.source. In 3.0, a source feeds one open diagram. Diagram(source=...) and an
assignment to Diagram.source raise traitlets.TraitError when another open Diagram
has that source.
In 2.x, two diagrams could share a source, but they did not work together:
Each run of a diagram took the pending changes from the source. Thus, a recorded change did not reach both diagrams.
Each layout wrote its positions and sizes onto the same elements. Thus, the last layout won, also in the other diagram.
A closed diagram releases its source. A diagram also releases its old source when it
gets a new one. To show one diagram in two places, display the same Diagram twice.
MarkElementWidget.diagram is the open diagram of a source, or None. You cannot set
it. The outlet of a pipe stage, such as first.pipe.outlet, is not a source. This rule
does not apply to it.
This change affects code that gives the source of an open diagram to a second diagram. The change is in #178.
# both versions
from ipyelk import Diagram, from_element
from ipyelk.elements import Node
first = from_element(Node(id="root"))
# 2.x
second = Diagram(source=first.source)
The 2.x code raises this error on 3.0:
TraitError: MarkElementWidget feeds another open diagram; close that diagram with source.diagram.close() or give this one its own MarkElementWidget. To show one diagram in two places, display the same Diagram twice
# 3.0: give the second diagram its own source
second = from_element(Node(id="root"))
# 3.0: or close the first diagram, and give its source to the second diagram
first.close()
second = Diagram(source=first.source)
Close the last diagram before you run its cell again#
In a notebook, the source and the diagram are often in different cells. If you run the
cell of the diagram again, Diagram(source=source) raises the error above. The diagram
of the last run is still open, because ipywidgets keeps every open widget. This is also
true when no name refers to that diagram. Use source.diagram to close it.
# both versions
from ipyelk.pipes import MarkElementWidget
source = MarkElementWidget(value=Node(id="root")) # cell 1
diagram = Diagram(source=source) # cell 2, first run
# 2.x
diagram = Diagram(source=source) # cell 2, second run
The 2.x code raises this error on 3.0:
TraitError: MarkElementWidget feeds another open diagram; close that diagram with source.diagram.close() or give this one its own MarkElementWidget. To show one diagram in two places, display the same Diagram twice
# 3.0: close the diagram of the last run, then make the new diagram
if source.diagram is not None:
source.diagram.close()
diagram = Diagram(source=source) # cell 2
Give the viewer to the diagram when you make it#
In 3.0, Diagram.view is fixed after construction. Give the viewer to the diagram with
Diagram(view=...). An assignment of a different viewer raises traitlets.TraitError.
An assignment of the same viewer does nothing.
The tools of a diagram stay bound to the viewer that the diagram started with. In 2.x,
an assignment of a new viewer moved the output of the pipe to the new viewer. But the
selection, the painter, the Fit and Center buttons and ToggleCollapsedTool still acted
on the old viewer, and the old viewer stayed open.
This change affects code that assigns Diagram.view. The change is in #208.
# both versions
from ipyelk import Diagram, from_element
from ipyelk.diagram import SprottyViewer
from ipyelk.elements import Node
diagram = from_element(Node(id="root"))
# 2.x
diagram.view = SprottyViewer()
The 2.x code raises this error on 3.0:
TraitError: Diagram.view cannot be replaced, because the tools are bound to it; close this Diagram, then build a new Diagram(source=..., view=...)
# 3.0: close the diagram, and give its source and the new viewer to a new diagram
diagram.close()
diagram = Diagram(source=diagram.source, view=SprottyViewer())
diagram.close() also closes diagram.view, so give the new diagram a new viewer.
Diagram(view=...) raises traitlets.TraitError when the viewer is closed.
A viewer is the view of one open diagram, too. Diagram(view=...) raises
traitlets.TraitError when the viewer is the view of another open diagram. In 2.x, both
diagrams set the source of the viewer, and the last diagram won. When one diagram
closed, it also closed the viewer of the other diagram.
# 2.x
second = Diagram(view=diagram.view)
The 2.x code raises this error on 3.0:
TraitError: SprottyViewer belongs to another open diagram; give this one its own SprottyViewer. To show one diagram in two places, display the same Diagram twice
# 3.0: give the second diagram its own viewer
second = Diagram(view=SprottyViewer())
Call sync_value() for browser code that reads a mark#
A MarkElementWidget syncs its value with the browser only when a browser stage reads
or writes the value. These marks sync their value:
the inlet of a
SyncedInletPipethe outlet of a
SyncedOutletPipe, such asElkJSandBrowserTextSizerthe
sourceof a viewer
These objects call MarkElementWidget.sync_value() when they take the mark. Every other
mark keeps its value in the kernel, for example Diagram.source. A browser write to a
value that does not sync has no effect. The flow and index of a mark never sync,
because no browser code reads them.
This change affects browser code that reads the value of another mark. Call
sync_value() on that mark first. The change is in #161.
# both versions
from ipyelk.elements import Node
from ipyelk.pipes import MarkElementWidget
mark = MarkElementWidget(value=Node(id="root"))
# 2.x: this printed True
print("value" in mark.get_state())
# 3.0: sync the value of a mark that your browser code reads
mark.sync_value()
print("value" in mark.get_state())
Use a MarkIndex as a kernel object, not as a widget#
In 3.0, a MarkIndex is a plain traitlets.HasTraits, not a widget. The MarkIndex
lives only in the kernel, so it opens no comm, and MarkElementWidget.index is never
sent to the browser. A MarkElementWidget is a Widget, not a DOMWidget, because no
view displays it. Thus, a MarkElementWidget has no layout, and its browser model is
a WidgetModel. One Diagram() opens 52 widgets in 3.0. In 2.1.1, it opened 78
widgets.
Every stage of a pipe shares the MarkIndex of the source, as in 2.x. The pipe builds
the MarkIndex when a stage first reads it. When you replace the tree of the source,
set source.index.elements = None. The next run then builds the index again for the new
tree. This works as in 2.x.
This change affects these uses:
code that uses a
MarkIndexas a widget:.comm,close(),display()orwidget_serializationbrowser code that reads
mark.get('index')code that reads
layoutof aMarkElementWidget
MarkIndex.comm, MarkIndex.close, MarkElementWidget.layout and
MarkElementWidget(layout=...) raise DeprecatedAPIError throughout 3.x. The change is
in #237.
# both versions
from ipyelk.elements import Node
from ipyelk.pipes import MarkElementWidget
mark = MarkElementWidget(value=Node(id="root"))
# 2.x: the index was a widget, so code closed it with the mark
mark.index.close()
The 2.x code raises this error on 3.0:
DeprecatedAPIError: MarkIndex is not a widget in ipyelk 3.0: it lives only in the kernel, so it has no comm and nothing to close. Its elements and document work as in 2.x. No alias: this error is raised throughout 3.x.
# 2.x: a mark had a layout, which no view used
mark.layout
The 2.x code raises this error on 3.0:
DeprecatedAPIError: MarkElementWidget is not a DOMWidget in ipyelk 3.0: no view displays it, so it has no layout. No alias: this error is raised throughout 3.x.
# 3.0: drop the elements to rebuild the index, and close the mark only
mark.value = Node(id="new-root")
mark.index.elements = None
mark.build_index()
print(mark.index.document["new-root"] is mark.value)
mark.close()
Elements and layout options#
An option id is the full id of a layout option of the Eclipse Layout Kernel (ELK), for
example org.eclipse.elk.spacing.edgeEdge. An option class is a class in
ipyelk.elements.layout_options that writes one option id, for example EdgeSpacing.
The layoutOptions dictionary of an element maps option ids to values.
Option classes use the option ids that ELK knows#
Some 2.x option classes wrote option ids that ELK does not know. ELK ignored these
options and showed no error. In 3.0, these option classes write the correct option ids.
Thus, these options now change the layout. A unit test checks each option class against
the options of the bundled elkjs, the JavaScript version of ELK.
Option class |
2.x option id |
3.0 option id |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the same id, with a tab character at the start |
|
Other option classes change too:
ConsiderModelOrderapplies only to parents and has the new valuePREFER_NODES.PortAlignmentEast,PortAlignmentWest,PortAlignmentNorthandPortAlignmentSouthare subclasses ofPortAlignment. Thus, they have a value and a control, and they apply to nodes.Recordnodes setEdgeNodeSpacing(spacing=0), which now has an effect.
This change affects code that uses these option classes, and code that writes the 2.x
option ids in layoutOptions. The 2.x code does not raise an error. But the layout of a
diagram that uses these option classes can change. ELK still ignores a 2.x option id in
layoutOptions. The change is in #192.
# 2.x
from ipyelk.elements import layout_options as opt
print(opt.EdgeSpacing().identifier)
On 3.0, the option class writes a different option id:
org.eclipse.elk.spacing.edgeEdge
The new function ipyelk.schema.unknown_layout_options(keys) finds the keys that ELK
does not know. Use it to find 2.x option ids in your layoutOptions.
# 3.0
from ipyelk.elements import Node
from ipyelk.schema import unknown_layout_options
node = Node(
layoutOptions={
"org.eclipse.elk.edge.edgeEdge": "5", # the 2.x id
"org.eclipse.elk.spacing.edgeEdge": "5",
}
)
print(unknown_layout_options(node.layoutOptions))
Set the label-port spacing in each direction#
LabelPortHorizontalSpacing and LabelPortVerticalSpacing replace LabelPortSpacing.
ELK has no spacing.labelPort option, so ELK ignored LabelPortSpacing. In 3.0,
LabelPortSpacing sets the horizontal spacing and shows a DeprecationWarning.
This change affects code that uses LabelPortSpacing. The change is in #192.
# 2.x
from ipyelk.elements.layout_options.port_options import LabelPortSpacing
LabelPortSpacing(spacing=3)
The 2.x code shows this warning on 3.0:
DeprecationWarning: LabelPortSpacing is deprecated: ELK has no `spacing.labelPort` option. Use LabelPortHorizontalSpacing or LabelPortVerticalSpacing.
# 3.0
from ipyelk.elements.layout_options.port_options import (
LabelPortHorizontalSpacing,
LabelPortVerticalSpacing,
)
LabelPortHorizontalSpacing(spacing=3)
LabelPortVerticalSpacing(spacing=3)
Select a layout algorithm other than Draw2D#
The choices of LayoutAlgorithm no longer include Draw2D
(org.eclipse.elk.conn.gmf.layouter.Draw2D). elkjs does not have this algorithm, so a
layout that used it failed. An import of Draw2DLayout from
ipyelk.elements.layout_options or from ipyelk.elements.layout_options.layout raises
DeprecatedImportError in every 3.x release.
This change affects code that imports Draw2DLayout or selects the Draw2D algorithm.
The change is in #192.
# 2.x
from ipyelk.elements.layout_options import Draw2DLayout
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.elements.layout_options.layout.Draw2DLayout was removed in ipyelk 3.0: elkjs has no Draw2D algorithm (org.eclipse.elk.conn.gmf.layouter.Draw2D), so a layout that selected it failed. Select another Algorithm, for example ELKLayered, ELKBox, ELKRadial or ELKRectanglePacking from ipyelk.elements.layout_options.layout: LayoutAlgorithm(value=ELKLayered.identifier). No alias: this error is raised throughout 3.x.
An import from the module ipyelk.elements.layout_options.layout fails too:
# 2.x
from ipyelk.elements.layout_options.layout import Draw2DLayout
The 2.x code raises the same error on 3.0. The message starts with this text:
DeprecatedImportError: ipyelk.elements.layout_options.layout.Draw2DLayout was removed in ipyelk 3.0
# 3.0
from ipyelk.elements.layout_options.layout import ELKLayered, LayoutAlgorithm
LayoutAlgorithm(value=ELKLayered.identifier)
Give layoutOptions string keys and values#
The type of layoutOptions on elements is LayoutOptions, from ipyelk.elements. It
is dict[str, str], as in the ELK JSON schema. When ipyelk validates an element, it
changes the values:
A
boolbecomes"true"or"false". A NumPy boolean, for examplenp.bool_(True), does too.An integer becomes a string:
20becomes"20". A NumPy integer, for examplenp.int64(20), does too.A
floatthat has no fractional part and a magnitude less than2**53loses its fraction:2.0becomes"2". ELK ignores an integer option with the value"2.0".Another
floatbecomes itsrepr():2.5becomes"2.5", and1e16becomes"1e+16".Another number, for example
np.float64(12.5), aDecimalor aFraction, becomes a plainfloatfirst. Thus,np.float64(12.5)becomes"12.5".
Validation raises ValidationError for nan, an infinity, None, a value of another
type, or a key that is not a string. 2.x accepted these values. When ipyelk serializes
an element for ELK, it changes a bool or a number to a string too. Thus, a value that
you set in place, for example node.layoutOptions[key] = 20, goes to ELK as "20". A
None that you set in place makes the serialization raise a ValueError that names the
key. With exclude_none=True, the serialization removes that key instead, and ELK uses
the default of the option. OptionsWidget.value accepts only string keys and values.
This change affects code that puts None, nan or an object other than a string, a
number or a bool in layoutOptions. Code that puts 2.0 there also changes: ELK now
applies the option. The change is in #192.
# 2.x
from ipyelk.elements import Node
Node(layoutOptions={"org.eclipse.elk.direction": None})
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Node
layoutOptions.`org.eclipse.elk.direction`
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
# 2.x: a None that you set in place
from ipyelk.elements import Node
node = Node(layoutOptions={})
node.layoutOptions["org.eclipse.elk.direction"] = None
node.model_dump()
On 3.0, the serialization raises PydanticSerializationError, a subclass of
ValueError, with this message:
PydanticSerializationError: Error calling function `serialize_layout_options`: ValueError: layoutOptions['org.eclipse.elk.direction'] is None: ELK cannot read it. Delete the key to unset the option.
# 3.0: leave out the key, and use a number or a string
from ipyelk.elements import Node
node = Node(layoutOptions={"org.eclipse.elk.partitioning.partition": 2.0})
print(node.layoutOptions)
An unknown key on an element raises#
An unknown key is a keyword argument or a dictionary key that a class has no field for.
In 3.0, these classes raise ValidationError for an unknown key when you make an
object:
Node,Port,Label,Edge,EdgeSectionand their subclassesthe properties classes, for example
NodePropertiesthe shape classes, for example
Rect
In 2.x, these classes ignored an unknown key. Thus a typo such as Node(hieght=5) did
nothing, and the node kept no height. A shape also rejects a field of another shape
class. For example, Rect(start="arrow") raises, because start is a field of
EdgeShape. A Point now ignores an unknown key. 2.x raised TypeError for one.
Assignment does not change. In both versions, node.hieght = 5 raises ValidationError
for an element or its properties, and a shape raises ValueError. layoutOptions
accepts any option id, as in 2.x.
ELK JSON can have keys that the models do not have, for example the $H key that elkjs
adds. Thus convert_elkjson, from_elkjson, the value that comes back from the
frontend and the networkx loader ignore those keys. To load a dictionary of your own
in the same way, use ipyelk.elements.from_wire(Node, data). In 3.0,
Node.model_validate(data) raises for an unknown key.
This change affects code that passes a misspelled key or a key of another class. It also
affects code that loads ELK JSON with Node.model_validate. The change is in #233.
# 2.x
from ipyelk.elements import Node
Node(id="a", hieght=5)
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Node
hieght
Extra inputs are not permitted [type=extra_forbidden, input_value=5, input_type=int]
# 3.0
from ipyelk.elements import Node
node = Node(id="a", height=5)
print(node.height)
# 2.x: ELK JSON with a key that the models do not have
from ipyelk.elements import Node
Node.model_validate({"id": "root", "$H": 123})
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Node
$H
Extra inputs are not permitted [type=extra_forbidden, input_value=123, input_type=int]
# 3.0: from_wire ignores the keys that the models do not have
from ipyelk.elements import Node, from_wire
root = from_wire(Node, {"id": "root", "$H": 123})
print(root.id)
JSON loads a shape as the class of its type string#
The type string of a shape is its type field, for example node:path. The type string
selects the renderer in the frontend. In 3.0, ELK JSON loads each shape as the class of
its type string. ELK JSON loads through convert_elkjson, from_elkjson and the value
that comes back from the frontend after each layout. Thus a Path, a Comment or a
Widget keeps its class and its fields. 2.x loaded every shape as the shape class of
its element, for example NodeShape.
This change affects code that reads the shapes of a loaded tree. It also affects code that loads JSON with incomplete shapes, and code that defines shape classes. The change is in #233.
A shape must have the fields of its class#
The class of the type string validates the shape. If the JSON does not have a field that
the class requires, loading raises ValidationError. 2.x loaded the shape as a
NodeShape. For example, a node:path shape without use raises.
# 2.x: a node:path shape without use
from ipyelk.elements import convert_elkjson
shape = {"type": "node:path"}
convert_elkjson({"id": "root", "children": [{"id": "a", "properties": {"shape": shape}}]})
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Node
children.0.properties.shape.use
Field required [type=missing, input_value={'type': 'node:path'}, input_type=dict]
# 3.0: give each shape the fields that its class requires
from ipyelk.elements import convert_elkjson
shape = {"type": "node:path", "use": "M 0,0 L 10,10"}
root = convert_elkjson({"id": "root", "children": [{"id": "a", "properties": {"shape": shape}}]})
print(type(root.children[0].properties.shape).__name__)
A Circle loads as an Ellipse#
Circle and Ellipse have the same type string, node:round. The JSON does not hold
radius, rx or ry. Thus a node:round shape loads as an Ellipse with the width
and height of the JSON. The renderer draws both shapes from the width and the height,
so the diagram does not change. isinstance(shape, Circle) is False after a load, in
both versions.
# both versions: the JSON of a node with a Circle shape
from ipyelk.elements import Node, NodeProperties, convert_elkjson, shapes
from ipyelk.elements.serialization import to_json
circle = NodeProperties(shape=shapes.Circle(radius=7))
data = to_json(Node(id="root", children=[Node(id="a", properties=circle)]), None)
# 2.x: this printed NodeShape 14.0
shape = convert_elkjson(data).children[0].properties.shape
print(type(shape).__name__, shape.width)
The 2.x code shows this on 3.0:
Ellipse 14.0
# 3.0: read the size from width and height
shape = convert_elkjson(data).children[0].properties.shape
print(shape.width / 2)
A widget shape needs an open widget#
A node:widget shape loads as a Widget with the widget that has the model_id in
use. If no open widget has that model_id, the shape loads as a NodeShape with the
same JSON, and ipyelk shows a UserWarning. This occurs when the widget closed, or
when the JSON comes from another kernel. Only ELK JSON loads in this way. A
node:widget dictionary in your own code with no open widget, for example
NodeProperties(shape={"type": "node:widget", "use": "abc123"}), raises
ValidationError, because the Widget has no widget.
# 2.x: JSON with the model_id of a widget that is not open
from ipyelk.elements import convert_elkjson
shape = {"type": "node:widget", "use": "abc123"}
root = convert_elkjson({"id": "root", "children": [{"id": "a", "properties": {"shape": shape}}]})
print(type(root.children[0].properties.shape).__name__)
The 2.x code shows this warning on 3.0, and loads a NodeShape as 2.x did:
UserWarning: No open widget has the model_id 'abc123' of a 'node:widget' shape. The shape loads as a NodeShape with the same JSON.
NodeShape
# 3.0: load the JSON while its widget is open
from ipywidgets import IntSlider
from ipyelk.elements import Node, NodeProperties, convert_elkjson, shapes
from ipyelk.elements.serialization import to_json
slider = IntSlider()
properties = NodeProperties(shape=shapes.Widget(widget=slider))
data = to_json(Node(id="root", children=[Node(id="a", properties=properties)]), None)
print(convert_elkjson(data).children[0].properties.shape.widget is slider)
A second class for a type string replaces the first#
If a shape class of your own sets a new type string, the class adds itself to
BaseShape.json_classes when Python defines it. Then JSON loads that type string as
your class. 2.x had no json_classes. If your class sets a type string that another
class has, your class replaces the other class. Then ipyelk shows a UserWarning. For
example, a class Oval with the type string node:round shows this warning:
UserWarning: __main__.Oval replaces ipyelk.elements.shapes.Ellipse as the class that loads the shape type 'node:round' from JSON. Give it another type, or set `loads_from_json = False` on it.
To keep the other class, give your class another type string, or set
loads_from_json = False on your class. If you define the same class again, for example
when you run a notebook cell again, ipyelk shows no warning.
Give each shape a payload of its type#
The payload of a shape is the data in its use field, for example the path data of a
Path. In 2.x, use was a string on every shape, and the frontend guessed what to do
with a bad value. In 3.0, the type string of a shape selects the type of its payload, in
Python, in TypeScript and in the JSON schema elkschema.json. Python validates the
payload when you make a shape, when you assign use and when JSON loads. An invalid
payload raises ValidationError at the use field, except on a Widget, as the next
sections tell. The Shapes page lists the payload types: SymbolId, PathData,
Markup, ImageUrl and NotchSize.
This change affects code that reads a Comment notch as a string, and code that gives a
shape a payload that the frontend cannot draw. The change is in #148.
Read the comment notch as a number#
Comment.use is the size of the notch in pixels, as a number of the type NotchSize.
2.x changed a number to a string. A string of a number, for example the "20" in the
JSON that 2.x saved, still loads as the number. The JSON of 3.0 holds a number. The JSON
schema elkschema.json describes the JSON of 3.0, so ipyelk.schema.validate_elk_json
rejects the string of 2.x. To check JSON that 2.x saved, load it with convert_elkjson
and validate the JSON that ipyelk.elements.serialization.to_json writes.
# 2.x: this printed '20'
from ipyelk.elements import shapes
print(repr(shapes.Comment(use=20).use))
The 2.x code shows this on 3.0:
20.0
# 3.0
from ipyelk.elements import shapes
print(shapes.Comment(use="20").use == 20)
A notch of 0 draws no notch, so the node is a rectangle. 2.x drew a notch of 15 pixels
for 0. A notch that is not a number, is negative, nan or an infinity raises
ValidationError. 2.x drew a notch of 15 pixels for a string that was not a number. The
renderer limits the notch to the width and the height of the node.
# 2.x
from ipyelk.elements import shapes
shapes.Comment(use="big")
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Comment
use
Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='big', input_type=str]
# 3.0: give the size in pixels
from ipyelk.elements import shapes
print(shapes.Comment(use=30).use)
Give a payload that the renderer can draw#
These payloads raise ValidationError in 3.0. The 2.x frontend drew nothing for them.
An empty symbol identifier in
Use,Icon,PortShapeorLabelShape. For a port or a label with no symbol, leave outuse.An empty
ImageURL.Pathdata that does not start withMormand a number, or that has characters other than path commands, numbers, commas and ASCII whitespace.A
useonRect,Circle,EllipseorDiamond. These shapes have no payload.A
useon aWidgetthat is not themodel_idof its widget. ipyelk writes themodel_idof the widget touse, so leave outuse. 2.x dropped the value. The error is at the shape, not atuse. ipyelk checksusewhen you make the shape and when you assignuse, not when you assignwidget, and not when the widget is closed.
An assignment raises the same error as a construction, and the shape keeps its old
value. 2.x validated an assignment only on Comment.
# 2.x
from ipyelk.elements import shapes
shapes.Path(use="hello")
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Path
use
String should match pattern '^[ \t\n\r\f]*[Mm][ \t\n\r\f]*[0-9+\-.][ \t\n\r\fMmZzLlHhVvCcSsQqTtAa0-9eE+\-.,]*$' [type=string_pattern_mismatch, input_value='hello', input_type=str]
# 3.0
from ipyelk.elements import shapes
path = shapes.Path(use="M 0,0 L 20,20")
path.use = "M 0,0 L 30,30"
print(path.use)
# 2.x
from ipyelk.elements import shapes
shapes.Rect(use="ignored")
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for Rect
use
Input should be None [type=none_required, input_value='ignored', input_type=str]
# 3.0
from ipyelk.elements import shapes
print(shapes.Rect().use)
NodeShape, PortShape and LabelShape keep a string use for a shape class of your
own. To validate the payload of your class, subclass one of them, set a new type string
and give use a type.
With the type string of a built-in class, a NodeShape, a LabelShape or an
ElementShape validates use with the type of that class. For example,
NodeShape(type="node:comment") takes a notch in pixels, as Comment does. A type
string whose class requires use, such as node:path, requires it here too. A
node:widget shape takes any string or None. 2.x took any string for every type
string, and the JSON did not pass validate_elk_json. The change is in #244.
# 2.x
from ipyelk.elements import shapes
shapes.NodeShape(type="node:comment", use="big")
The 2.x code raises this error on 3.0:
ValidationError: 1 validation error for NodeShape
use
Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='big', input_type=str]
# 3.0: give the payload of the type string, or use its class
from ipyelk.elements import shapes
print(shapes.NodeShape(type="node:comment", use=30).use)
print(shapes.Comment(use=30).use)
Expect NotFoundError or NotUniqueError from ElementIndex.root()#
A root is a node that has no parent. ElementIndex.root() raises
ipyelk.exceptions.NotFoundError when the index has no root. It raises NotUniqueError
when the index has more than one root, and the message names the roots. check_ids(),
check_edges() and MarkIndex.root() call root(), so they raise these errors too.
In 2.x, an index with no root failed an assert with the wrong message, “Multiple
roots”. An index with more than one root used the first root and showed no error. Thus,
the checks did not look at the other trees.
This change affects code that catches AssertionError from these methods, or that
indexes more than one tree. The change is in #206.
# both versions: an index with two roots
from ipyelk.elements import ElementIndex, Node
index = ElementIndex.from_els(Node(id="a"), Node(id="b"))
# 2.x
print(index.root().id)
The 2.x code raises this error on 3.0:
NotUniqueError: The index has 2 roots: a, b. 2 nodes have no parent link. A child put in `children` after its parent is made, as with `children.append()`, has no parent link: add it with `add_child()`, or index one tree at a time.
# 3.0: index one tree
from ipyelk.elements import ElementIndex, Node
index = ElementIndex.from_els(Node(id="root", children=[Node(id="a"), Node(id="b")]))
print(index.root().id)
Add a child with add_child(), not children.append()#
The parent link of a node is the reference from the node to its parent. root() reads
the parent link: a node without one is a root. Some ways to add a node to a tree set the
parent link, and others do not:
Way to add the child |
Sets the parent link |
|---|---|
|
Yes |
|
Yes |
|
Yes |
|
Yes |
|
No |
|
No |
Ports follow the same rules: Node(ports=[p]), n.add_port(p) and n.name = p set the
parent link of the port p. A change to n.ports after n is made does not set it.
root() looks only at nodes, so a port, a label or an edge is never a root. An edge
that add_edge() makes has no parent link, because the list edges of its node holds
it.
In 2.x, the same ways set the parent link. But root() used the first node without a
parent link and showed no error. Thus, a tree that children.append() built had one
root for 2.x: its top node. In 3.0, each child that children.append() added is one
more root. On the first layout of a diagram, ValidationPipe calls root(). Thus, the
first layout fails with NotUniqueError, and the diagram shows no graph.
This change affects code that builds a tree with children.append() or any other change
to children or ports after the node is made. To fix the code, add each child with
add_child(), or give the children to Node(children=[...]). ipyelk does not set the
missing parent links for you.
# 2.x: the first layout of a diagram runs this pipe
from ipyelk.elements import Node
from ipyelk.pipes import ValidationPipe
root = Node(id="root")
root.children.append(Node(id="a"))
pipe = ValidationPipe()
pipe.inlet.value = root
pipe.inlet.build_index()
await pipe.run()
The 2.x code raises this error on 3.0:
NotUniqueError: The index has 2 roots: root, a. 2 nodes have no parent link. A child put in `children` after its parent is made, as with `children.append()`, has no parent link: add it with `add_child()`, or index one tree at a time.
# 3.0: add the child with add_child()
from ipyelk.elements import Node
from ipyelk.pipes import ValidationPipe
root = Node(id="root")
child = root.add_child(Node(id="a"))
pipe = ValidationPipe()
pipe.inlet.value = root
pipe.inlet.build_index()
await pipe.run()
print(child.get_parent().id)
Do not use the data classes of layout_options.model#
In 2.x, the module ipyelk.elements.layout_options.model had ELK JSON data classes with
from_dict and to_dict converters. No code in ipyelk used them. In 3.0, the module
has only five tag classes: ElkGraphElement, ElkNode, ElkPort, ElkEdge and
ElkLabel. A tag class names the kind of element that a layout option applies to, in
LayoutOptionWidget.applies_to. A tag class has no fields and no methods.
An import of a removed name raises DeprecatedImportError. The table in
The errors for removed names lists the names. Nothing replaces them. The function
strip_none moved to ipyelk.util.strip_none. Its old import shows a
DeprecationWarning and still works. To make a graph, use the classes Node, Port,
Edge and Label of ipyelk.elements. To load ELK JSON, use
ipyelk.elements.convert_elkjson or ipyelk.elements.from_wire.
3.0 also removes ElkLabel.__eq__ and ElkLabel.__hash__. They served a cache of text
sizes that is gone.
This change affects code that imports a removed name or strip_none, or that makes an
object of a tag class. The changes are in #206 and #214.
# 2.x
from ipyelk.elements.layout_options.model import ElkPoint
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.elements.layout_options.model.ElkPoint was removed in ipyelk 3.0: it was a generated ELK JSON data class that nothing in ipyelk used, and nothing replaces it. Build graphs from ipyelk.elements.Node, Port, Edge and Label, and load ELK JSON with ipyelk.elements.convert_elkjson or from_wire. No alias: this error is raised throughout 3.x.
# 2.x
from ipyelk.elements.layout_options.model import ElkLabel
label = ElkLabel(id="l1", text="hello")
The 2.x code raises this error on 3.0:
TypeError: ElkLabel is a tag class with no fields: it names the kind of element that a layout option applies to. To make an element, use ipyelk.elements.Label.
# 3.0: make the label from the class of ipyelk.elements
from ipyelk.elements import Label
label = Label(id="l1", text="hello")
print(label == Label(id="l1", text="hello"))
# 2.x
from ipyelk.elements.layout_options.model import strip_none
The 2.x code shows this warning on 3.0:
DeprecationWarning: ipyelk.elements.layout_options.model.strip_none is deprecated: import strip_none from ipyelk.util. ipyelk 4.0 removes this alias.
# 3.0
from ipyelk.util import strip_none
print(strip_none({"id": "a", "labels": [], "width": None}))
Do not use GraphWrappingStrategy.horizontal#
3.0 removes the trait GraphWrappingStrategy.horizontal. No code read it, so it changed
nothing. On 3.0, GraphWrappingStrategy(horizontal=...) ignores the keyword and shows
no error. A later read of horizontal raises AttributeError. An assignment to
horizontal makes a plain Python attribute and shows no error.
traitlets issues a DeprecationWarning for the ignored keyword. A notebook shows a
DeprecationWarning only when the code of the notebook issues it. Thus, a notebook does
not show this warning. By default, a pytest run shows it in its summary of warnings.
This change affects code that sets or reads GraphWrappingStrategy.horizontal. No other
trait replaces it. The change is in #206.
# 2.x
from ipyelk.elements.layout_options.wrapping_options import GraphWrappingStrategy
strategy = GraphWrappingStrategy(horizontal="center")
print(strategy.horizontal)
The 2.x code raises this error on 3.0:
AttributeError: 'GraphWrappingStrategy' object has no attribute 'horizontal'
# 3.0: leave out horizontal
from ipyelk.elements.layout_options.wrapping_options import GraphWrappingStrategy
GraphWrappingStrategy(value="OFF")
Expect a label to have the size that you assign#
In 3.0, the layout uses the width and the height that you assign to a Label.
Unless the LabelShape of a label sets both width and height, the browser measures
the text of the label. In 2.x, the measured size then replaced the size that you
assigned. In 3.0, the layout uses the measured size only for a dimension that has no
other value.
For each dimension of a label, the layout uses the first of these values:
the
widthorheightthat you assigned to theLabelthe
widthorheightof itsLabelShapethe size that the browser measured
This change affects code that assigns a width or a height to a Label. It also
affects code that sets only one of the two on a LabelShape. The 2.x code does not
raise an error. But the layout of the label can change. The change is in #223.
# 2.x
from ipyelk.elements import Label, LabelProperties
from ipyelk.elements.shapes import LabelShape
label = Label(text="A long label", width=40)
other = Label(
text="A long label", properties=LabelProperties(shape=LabelShape(width=40))
)
On 3.0, the layout makes both labels 40 pixels wide. On 2.x, both labels had the width of their text.
# 3.0: leave out the width to lay out the label at the width of its text
from ipyelk.elements import Label
label = Label(text="A long label")
In 3.0, a shape also keeps its class and its fields through a layout. In 2.x, the shape
came back from the browser as its base class, for example a Circle as a NodeShape.
The element also kept its laid-out size, so a later change to the size of its shape had
no effect. In 3.0, a change to the size of a shape changes the size of the element at
the next layout. 2.x code that assigns None to width and height after it changes
the shape still works. The Shapes page tells you how to request that
layout.
Find elements by id with a Document#
A Document (ipyelk.elements.Document) owns the ids of an element tree. It replaces
the Registry context manager, which 3.0 deprecates. Document(root) gives each
element without an id the id that the element serializes with. This id is the wire_id
of the element. Thus, the id that the browser shows is the id that the element keeps.
A Document finds an element in three ways:
document[id]returns the element. It raisesNotFoundErrorfor an unknown id.document.resolve(id)returns aResolved(element, via_slack), orNonefor an unknown id.document.lookup(ids, strict=True)returns the elements ofids. Withstrict=False, it skips the unknown ids.document.missing(ids)lists them.
No ipyelk code uses a Registry in 3.0. A Registry holds an element weakly, so it
does not keep the element alive. It holds a key that cannot be weakly referenced, such
as a str, as in 2.x.
This change affects code that makes a Registry or enters one with with. Registry
works through 3.x, and ipyelk 4.0 removes it. The change is in #160.
# 2.x
from ipyelk.elements import ElementIndex, Node, Registry
nodes = [Node(id="n1"), Node()]
with Registry():
el_map = ElementIndex.from_els(*nodes)
node = el_map.elements["n1"]
The 2.x code shows this warning on 3.0:
DeprecationWarning: ipyelk.elements.Registry is deprecated and will be removed in ipyelk 4.0: a Document (ipyelk.elements.Document) owns the ids of an element tree, without a context manager. Document(root) gives each element without an id the id that it serializes with, and document.resolve(id) finds the element; to reuse a fragment, copy it with Node.clone(prefix). Registry keeps working through 3.x.
# 3.0
from ipyelk.elements import Document, Node
nodes = [Node(id="n1"), Node()]
doc = Document(*nodes)
node = doc["n1"]
print(doc.resolve("typo"), doc.missing(["n1", "typo"]))
Read the ids of a pipe from MarkIndex.document#
MarkIndex.document replaces MarkIndex.context, which 3.0 deprecates. Every stage and
tool of a pipe shares the MarkIndex of the pipe. Its document is the Document of
the pipe, and MarkIndex.elements is the id map of that document. MarkIndex.context
holds a Registry that ipyelk does not read.
MarkIndex.elements and MarkIndex.context are properties in 3.0, not traits.
MarkIndex(elements=...) and an assignment to elements work as in 2.x. But
observe(callback, "elements") never calls the callback.
This change affects code that reads or assigns MarkIndex.context, and code that
observes MarkIndex.elements. MarkIndex.context works through 3.x, and ipyelk 4.0
removes it. The change is in #160.
# both versions
from ipyelk.elements import Node
from ipyelk.pipes import MarkElementWidget
mark = MarkElementWidget(value=Node(id="root", children=[Node()]))
mark.build_index()
element = mark.value.children[0]
# 2.x
with mark.index.context:
el_id = element.get_id()
The 2.x code shows this warning on 3.0:
DeprecationWarning: MarkIndex.context is deprecated and will be removed in ipyelk 4.0: MarkIndex.document (an ipyelk.elements.Document) owns the ids, and nothing in ipyelk reads the Registry. Use index.document.resolve(id).
# 3.0: the id that the element serializes with is the id that it keeps
el_id = element.wire_id()
print(mark.index.document[el_id] is element)
Index an element that has no id#
ElementIndex.from_els, VisIndex.from_els and HierarchicalIndex.from_els index an
element without an id by its wire_id. In 2.x, they raised ValueError outside a
Registry. 3.0 adds Painter.paint, which also stores the wire_id of an element that
has no id. The id of the element stays None until a Document writes it.
This change affects code that expects the ValueError. The change is in #160.
# both versions
from ipyelk.elements import ElementIndex, Node
node = Node()
# 2.x: this raised a ValueError, "none is not an allowed value"
index = ElementIndex.from_els(node)
# 3.0
index = ElementIndex.from_els(node)
print(index.elements[node.wire_id()] is node, node.id)
Give each element one parent#
An element under a second parent raises ValueError when you attach it. The message
names both parents. While the second parent is made, the error is a
pydantic.ValidationError, which is a ValueError too. This applies to labels, child
nodes and ports. An element that its old parent no longer holds attaches without an
error.
In 2.x, a shared label failed later, in ValidationPipe, with Nonunique Element Ids.
A shared child node or port failed an assert (owned by different node). A label that
you append to labels in place is not checked when you attach it. ValidationPipe
still reports its id. Compartment(bullet_shape=...) gives each line its own bullet
label. In 2.x, it shared one label, which failed validation.
To use one fragment in several places, copy it with Node.clone(prefix). The copy of an
element with the id X has the id "<prefix>.X".
This change affects code that puts one element under two parents. The change is in #160.
# 2.x
from ipyelk.elements import Label, Node
label = Label(text="x")
a = Node(id="A", labels=[label])
b = Node(id="B", labels=[label])
The 2.x code raises this error on 3.0:
Value error, Label without an id (text 'x') is already under Node 'A', so it cannot also be under Node 'B'. An element has one parent: detach it first, or use a copy (Node.clone(prefix) for a fragment, model_copy() for a label).
# 3.0: give each parent its own label, and copy a fragment with clone
from ipyelk.elements import Label, Node
label = Label(text="x")
a = Node(id="A", labels=[label])
b = Node(id="B", labels=[label.model_copy()])
gate = Node(id="gate", children=[Node(id="core")])
root = Node(id="root", children=[gate.clone("g1"), gate.clone("g2")])
print([child.children[0].id for child in root.children])
Imports#
Do not import Zoom or Pan#
An import of ipyelk.tools.Zoom or ipyelk.tools.Pan raises DeprecatedImportError.
Read the camera from Viewer.viewport tells you what to use instead. The change is in #157.
This change affects code that imports Zoom or Pan from ipyelk.tools.
# 2.x
from ipyelk.tools import Zoom
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.tools.Zoom was removed in ipyelk 3.0: it was a placeholder that nothing ever wrote. The browser reports the camera of the most recently reporting view as viewer.viewport (ipyelk.tools.Viewport: view_id, origin, zoom, canvas_size, viewed_ids); move a view with viewer.set_viewport(zoom=...). No alias: this error is raised throughout 3.x.
# 3.0
from ipyelk.tools import Viewport
print(Viewport().zoom)
Import ControlOverlay from ipyelk.tools.control_overlay#
The module ipyelk.tools.contol_overlay had a spelling mistake. Its new name is
ipyelk.tools.control_overlay. An import of the old module raises
DeprecatedImportError and names the new module. The old module exports no names. The
change is in #157.
This change affects code that imports from ipyelk.tools.contol_overlay.
# 2.x
from ipyelk.tools.contol_overlay import ControlOverlay
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.tools.contol_overlay (misspelled) was removed in ipyelk 3.0; import ControlOverlay from ipyelk.tools.control_overlay (or ipyelk.tools). No alias: this error is raised throughout 3.x.
# 3.0
from ipyelk.tools import ControlOverlay
from ipyelk.tools.control_overlay import ControlOverlay as SameClass
print(ControlOverlay is SameClass)
Do not import ipyelk.pipes.mappings#
3.0 removes the module ipyelk.pipes.mappings, because no code used it. The module had
its own classes Edge and Port, which are not the classes of ipyelk.elements. It
also had the type aliases NodeMap, EdgeMap and PortMap. 3.0 has no replacement for
these names. An import of the module or of one of its names raises
DeprecatedImportError.
This change affects code that imports ipyelk.pipes.mappings. To make a graph, use the
classes Node, Edge and Port from ipyelk.elements. The change is in #206.
# 2.x
from ipyelk.pipes.mappings import NodeMap
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.pipes.mappings was removed in ipyelk 3.0: nothing in ipyelk used its Edge and Port dataclasses or its NodeMap, EdgeMap and PortMap aliases, and nothing replaces them. Build graphs from ipyelk.elements.Node, Edge and Port. No alias: this error is raised throughout 3.x.
# 3.0: make the graph from the classes of ipyelk.elements
from ipyelk.elements import Node, Port
a = Node(id="a", ports=[Port(id="a.out")])
b = Node(id="b")
root = Node(id="root", children=[a, b])
root.add_edge(source=a.ports[0], target=b)
print(len(root.edges))
Do not import Mark or MarkFactory#
3.0 removes ipyelk.elements.Mark, ipyelk.elements.MarkFactory and the module
ipyelk.elements.mark_factory. They served the networkx transformer that ipyelk 2.0
removed, and no code used them after that. An import of the module or of one of the
names raises DeprecatedImportError.
This change affects code that imports these names. To use one fragment in several
places, copy it with Node.clone(prefix). To load a networkx graph, use
ipyelk.from_nx or ipyelk.loaders.NXLoader. The change is in #160.
# 2.x
from ipyelk.elements import MarkFactory
The 2.x code raises this error on 3.0:
DeprecatedImportError: ipyelk.elements.MarkFactory was removed in ipyelk 3.0: Mark and MarkFactory served the networkx transformer that ipyelk 2.0 removed (#90), and nothing used them since. To use one fragment in several places, copy it with Node.clone(prefix); to load a networkx graph, use ipyelk.from_nx or ipyelk.loaders.NXLoader. No alias: this error is raised throughout 3.x.
# 3.0: copy a fragment with clone
from ipyelk.elements import Node
gate = Node(id="gate", children=[Node(id="core")])
print(gate.clone("g1").children[0].id)
Do not import NodeExpandTool in TypeScript#
The browser part of ipyelk is the npm package @jupyrdf/jupyter-elk. 3.0 removes its
TypeScript class NodeExpandTool and its ExpandAction. The wheel handler of
NodeExpandTool returned before it did anything, and no code sent ExpandAction. 3.0
also removes the empty change:interaction listener, because no kernel class has an
interaction trait.
This change affects only TypeScript code that imports NodeExpandTool or
ExpandAction. Python code does not change. The examples in this section are not run.
The change is in #206.
// 2.x
import { NodeExpandTool } from '@jupyrdf/jupyter-elk/lib/tools';
import { ExpandAction } from '@jupyrdf/jupyter-elk/lib/tools/expand';
On 3.0, lib/tools does not export NodeExpandTool, and the module lib/tools/expand
is gone. Thus, the 2.x imports fail.
// 3.0: delete the import, because there is no replacement