Changelog#
3.0.0 (unreleased)#
Breaking changes#
Tools no longer replace the pipe’s pending flow when they run.
Toolmerges itsreportsintotee.inlet.flow(order preserved, duplicates dropped) through the newTool.record_reports(), so two tools triggered in the same turn – or a tool triggered while the initial("new",)flow is still unconsumed – keep every writer’s pending reports.ToggleCollapsedTool.runno longer assigns the flow itself (#156).Viewer.fit()/Viewer.center()now carry the fullSprottyViewersignatures (model_ids: str | Sequence[str] | None, plus the animate/zoom/padding keywords) and are documented no-ops on the generic viewer.model_idsaccepts one id or any sequence of ids (astris one id, never its characters);list[str]still works. The baseViewer’s Fit/Center buttons no longer raiseTypeErrorwhen clicked (#156).Tool invocation and lifecycle names each have one role (#156):
Tool.trigger()requests execution (cancels a running request, schedulesrun(), returns theasyncio.Task);Tool.handleris removed.ToolButton.on_clickreplaces thehandlertrait; it is called ason_click(tool).SetTool’sactiveobserver is the private_update_active; it is not an execution entry point.Tool.on_start(callback)replaceson_run. It fires when the work actually starts (2.xon_runfired after a successful run, despite its name).Tool.on_done(callback)is a registration method (several listeners; the owningDiagramregisters its refresh there), no longer an assignable single-slot trait. Callbacks receive the tool and fire only afterrun()completes successfully and its change reports are recorded – never on failure, cancellation, or a request rejected bytrigger().Reports are recorded when the work finishes (successfully or not), so partial changes from failed or cancelled work still reach the next refresh; a request superseded by a newer
trigger()never overwrites the newer request’s state.trigger()on a tool without arun()(state-only tools such asSelection, callback tools such asToolButton) raisesNotImplementedErrorbefore anything is scheduled; nothing is faked.Every callback receives the emitting object:
Toolbar.on_closeis typedCallableand called ason_close(toolbar).Diagram.refresh(sender=None)ignores its argument (it waschange).
Tool.disabledreplaces the inert, synceddisabletrait (nothing ever read it). It is an execution guard:trigger()andToolButtonclicks are rejected withRuntimeErrorwhile the tool is disabled (nothing is scheduled, no callback fires), and every control undertool.uithat has adisabledtrait follows it – buttons and the buttons insideSetTool’s box, but not boxes or progress bars, which have no such trait (#156).Unbound tools are a supported state:
PipelineProgressBar.pipe,ToggleCollapsedTool.selectionandSetTool.selectionareNoneuntil bound (they used to raiseTraitErroron read), so a tool can be constructed first and bound later (Diagram.register_tool, assignment, or the progress bar’s firstupdate). While a dependency listed in the tool’s_dependenciesisNone,Tool.missing_dependencies()names it, the tool’s controls are disabled, andtrigger()raisesRuntimeErrornaming the missing dependency (#156).Selection.elements()stays strict by default (an unknown id raisesNotFoundErrornaming it) and gains explicit tolerant resolution:elements(strict=False)skips unknown ids andmissing_ids()reports them, both inidsorder. Requested ids are never filtered on assignment, so selecting before the first layout keeps working.get_index()now builds the index when the pipe’sMarkIndexhas no elements yet (it only checked for a missingMarkIndex, which always exists) (#156).Toolbar.order()andToolbar.tool_order()are annotated as returning tool UIs (dict[int, list[DOMWidget]]/list[DOMWidget]), which is what they always returned, and document that tools without auiare omitted.Selection.idsis documented as the tuple it is; the01_Linkingexample compares it to a tuple (its list comparison was always true), and the12/13examples drop commented-out references to the long-gonetoolbar.commands(#156).Hover.idsis replaced byHover.hovered_id: str | None(defaultNone). No alias exists: code that reads or observesidson the hover tool must switch tohovered_id. Despite its name,idsonly ever held one id (a string, never a tuple), so the value shape is unchanged; only the trait name and theNonestate are new.Selection.idsis unchanged and still a tuple (#155).Leaving an element now clears the hover:
hovered_idbecomesNonewhen the pointer leaves the diagram’s elements (it previously kept the last hovered id forever). Observers that only expect strings must handleNone(#155).Viewer.control_overlayis opt-in and defaults toNone; assign aControlOverlay(e.g.view.control_overlay = ControlOverlay()) before setting itschildren. The browser renders no overlay container forNoneor for an overlay withoutchildren(previously every selection rendered an empty<id>_entropynode), and re-renders when the overlay’schildrenchange (#156).ipyelk.tools.contol_overlay(sic) is renamed toipyelk.tools.control_overlaywithout an alias (#156).Viewer.viewport(anipyelk.tools.Viewport) replaces the never-writtenZoomandPantools and theViewer.viewedtrait with real viewport synchronization (#156). The browser reports the camera of the most recently reporting view as one snapshot –view_id,origin,zoom,canvas_size,viewed_ids– so an observer of any field sees all five updated together. It is not a global viewport: several views of one diagram stay independent and each report names its view. The traits are read-only in the kernel (the browser is the only writer) andNoneuntil the first report.viewed_idslists the model elements whose bounds touch the visible rectangle, in model order; edges, renderer artifacts and the slack ports/edges that stand in for hidden elements are never listed.SprottyViewer.set_viewport(*, origin=None, zoom=None, animate=True, view_id=None)moves the camera (a command, likefit/center, never a trait):Nonekeeps the view’s current origin/zoom,view_id=Nonemoves every connected view (#156).Viewer.painter(anipyelk.tools.Painter) is live: temporary, view-only styling (#156).painter.paint(ids, *css_classes)adds classes to the rendered elements in every connected view without touchingproperties.cssClasses– a re-layout re-applies them, the ELK/model JSON never changes, and exported SVG shows them.unpaint(ids, *css_classes)removes classes (with none given, drops the ids),clear()removes everything the painter applied and never a class the model set,painted_ids,elements(strict=True)andmissing_ids()mirrorSelection. Targets are ids (astris one id) or elements (stored asget_id()); the syncedstylestrait isdict[str, tuple[str, ...]]. The unfinished 2.x placeholderscssClasses,marksandnameare removed.Removed names are hard errors for the whole 3.x line, not aliases or warnings:
Tool.handler,ToolButton.handler,Tool.on_run,Tool.disable,Hover.ids,Viewer.zoom,Viewer.pan,Viewer.viewed,ipyelk.tools.Zoom,ipyelk.tools.Pan,Painter.cssClasses,Painter.marks,Painter.name, and assigningTool.on_startorTool.on_doneraiseipyelk.exceptions.DeprecatedAPIError(anAttributeError, sohasattr,getattr(..., default)and introspection keep working) on read, assignment, and as constructor keywords, with the reason and the replacement in the message (mechanism:ipyelk.exceptions.RemovedAPI/RegistrationMethod/check_removed). The removed module-level names raiseipyelk.exceptions.DeprecatedImportErrorinstead, which is anImportErrorand not anAttributeError, becausefrom ipyelk.tools import Zoomwould otherwise lose the message to a bare “cannot import name”. Importing the misspelledipyelk.tools.contol_overlaymodule raises it too, naming the new path; it re-exports nothing. Both flavors share theipyelk.exceptions.DeprecatedAPIbase, so oneexceptcatches either.
Migration#
# 2.x: a Tool subclass that hand-rolled the merge to avoid clobbering the flow
inlet.flow = tuple(dict.fromkeys((*inlet.flow, *self.reports)))
# 3.0: the base class does this; drop the hand-rolled merge or call
self.record_reports()
# 2.x # 3.0
button.on_click(tool.handler) button.on_click(tool.trigger)
await tool.handler() await tool.trigger()
FitTool(handler=lambda: view.fit()) FitTool(on_click=lambda tool: view.fit())
tool.on_run(callback) tool.on_start(callback) # when work starts
tool.on_done(callback) # success only
tool.on_done = callback tool.on_done(callback) # + remove=True
toolbar.on_close = lambda: ... toolbar.on_close = lambda toolbar: ...
tool.disable = True tool.disabled = True # now actually disables
# a Tool subclass that did its work synchronously and refreshed by hand:
inlet.flow = (...); self.on_done() self.trigger() # run() may be a no-op
# 2.x
hover.ids # last hovered id, never reset
# 3.0
hover.hovered_id # id under the pointer, or None
hover.hovered_id = "n1" # highlight from the kernel
hover.hovered_id = None # clear the highlight
# 2.x: every viewer allocated an overlay, rendered even when empty
view.control_overlay.children = [button]
# 3.0: opt in first
from ipyelk.tools import ControlOverlay # was ipyelk.tools.contol_overlay
view.control_overlay = ControlOverlay()
view.control_overlay.children = [button]
view.control_overlay.children = [] # renders nothing
# 2.x: placeholders nothing ever wrote
view.zoom.zoom, view.pan.origin, view.viewed
# 3.0: the browser's latest report (None until a view reports), one snapshot
view.viewport.zoom, view.viewport.origin, view.viewport.viewed_ids
view.viewport.view_id # which view reported
view.viewport.observe(callback, "origin") # every field is already updated
# move a view (a command, not state)
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)
# 2.x: Painter(cssClasses=..., marks=..., name=...) painted nothing
# 3.0: view-only classes per id, applied in every view, never in the model
view.painter.paint(["n1", "n2"], "highlight") # or elements
view.painter.unpaint("n1", "highlight") # drop a class; unpaint("n1") drops the id
view.painter.clear()
view.painter.styles # {"n2": ("highlight",)}
The removed names (handler, on_run, disable, Hover.ids, assigning on_start or
on_done, the contol_overlay module, Viewer.zoom/pan/viewed,
ipyelk.tools.Zoom/Pan, Painter.cssClasses/marks/name) raise
DeprecatedAPIError – or DeprecatedImportError for the module-level names –
throughout 3.x with the replacement in the message.
@jupyrdf/jupyter-elk 3.0.0#
Write
hovered_idback on pointer leave, but only when the departed element is still the hovered one, so a stale leave cannot erase a newer enter; never dispatchnullto sprotty as an element id; re-wiring the hover tool releases the previous tool’s listener (#155).Skip the control overlay when the viewer’s
control_overlayisnullor has nochildren, and re-render when itschildrenchange (#156).Report the viewport: every
ELKViewerViewowns a stableview_idand, 100 ms after the lastSetViewport/Center/FitToScreen/InitializeCanvasBoundsaction (plus sprotty’s animation time for an animated move) or re-layout, writesview_id,origin,zoom,canvas_sizeandviewed_idsto the kernel’sviewporttool in one state update. A gather superseded by a newer one is dropped, a snapshot the kernel could not tell from the last write is skipped, and a detached view never reports. Theviewportcustom message appliesSprottyViewer.set_viewportto the addressed view (or every view) (#156).Apply the kernel
painter.stylesat transform time: the ELK -> sprotty transform merges the painted classes after each element’s model classes (no duplicates, symbols excluded), so every render – including re-layouts – carries them, and achange:stylesre-renders the same layout through sprotty’s model update, which keeps the selection and the camera.UpdateModelCommand2now also carrieshoverFeedbackover to the updated element, so a re-render under a resting pointer no longer drops the mouseover (#156).
2.1.3 (unreleased)#
Fixed#
Hiding a root-level node with visible edges no longer crashes elkjs:
iter_visiblereported every sibling after a hidden one as hidden and projected a hidden node’s slack port onto its preceding sibling (for a hidden first child of the root, onto the root itself, which elkjs rejects). Hidden compound children now project onto the compound, and an edge whose projection would touch the root is dropped from the wire while itsEdgestays in the index (#161, #169)Never serialize an id as
null: an id-less element mints a wire id once and keeps it, the index adopts it, and an id theRegistrymints first becomes the wire id, so edges no longer go on the wire assources: [null]and the id first seen is the id for the object’s life. Port ids compose from the parent’s id in and out of aRegistry(was<uuid>.<uuid>underNode(id="N")), a copied element mints its own wire id instead of sharing the original’s, andLabel.wrapno longer copies a generated id onto every line (#160, #169)Add nested label spacing regardless of the sublabel width:
size_nested_labelparsedls.width or 0 + spacingasls.width or (0 + spacing)(#161, #169)Fix the first-frame culling bug while culling stays off:
validCanvasBoundswas the inverse of its comment, so nothing was culled on a sized canvas and almost everything on the0x0canvas before the firstInitializeCanvasBoundsAction; the guard now skips culling while the canvas size is unknown, behind an explicitCULLING_ENABLEDconstant (turning it on is #170). The SVG exporter sizes itsviewBoxfrom the model’s content extent instead of the live group’s bounding rect, which under-sized by the layout’s left margin and made the export viewport-dependent (#161, #169)Stop echoing browser-written state back to the frontends: the selection, the hover and the laid-out graph were re-sent to every attached frontend as soon as the browser wrote them, and the graph was then sent a second time as a re-serialized copy, so every layout arrived back twice and the diagram re-rendered on each. Known limitation, documented on the traits: a second frontend attached to the same kernel no longer learns browser-made selections from that echo (#164, #161)
Keep a refresh that was requested while a run was already in flight: a style change, or a tool that failed after changing the model, recorded itself by overwriting the pipeline’s pending flow, and a completing run cleared that flow by overwriting it in turn — so the request was lost and the change never redrawn. Runs now take the pending flow when they start and put it back if they fail or are cancelled (#164, #160)
Stop throwing away browser work a refresh has already started: a burst of
refresh()calls cancelled the in-flight run and started another, so the browser went on laying out graphs whose results were discarded, and a cancelled run’s answer could be consumed by its successor. Requests are now counted rather than cancelled and one runner serves them on the trailing edge (ten calls in one tick cost one layout), and each roundtrip carries a generation counter, so a re-sent request is recognized as the same work instead of queueing a duplicate layout in the browser and an answer for an abandoned generation is ignored.Pipe.cancel()is now the only thing that stops a run, andpipe._taskis the pipe’s current runner rather than “the run this call just scheduled” (#164, #161)Stop swapping the browser’s laid-out copy into the pipeline’s input: it carried nothing that persisting the result does not already merge into the user’s own elements, but the next run rebuilt the element index from that copy, so a selection resolved to a copy and a hidden node resolved to the slack port sharing its id (#164, #161)
Make edge-owner validation linear in the hierarchy instead of quadratic in it: validating 1000 nodes with 3000 root-owned edges fell from 14.4 s to 0.010 s (500 nodes / 1500 edges, 3.5 s to 0.005 s; 100 / 300, 0.146 s to 0.001 s), and validation no longer re-reports on its outlet when it changed nothing (#164, #161)
Settle a browser roundtrip on the event loop that awaits it: with
ipykernel7 and a frontend that uses kernel subshells (JupyterLab 4.5+, ipywidgets 8.1.8+), widget messages are handled on a subshell thread with its own loop, so an answer written from there did not wake a run started from a cell until the resend timer fired, and arefresh()from a widget callback reused a runner from the other loop and failed silently. Answers are now delivered through the future’s own loop, and a refresh from another loop hands the runner over to the caller’s loop (#164)Stop leaking widgets on every refresh. Each pipe status change created a new
PipeStatuswidget, and a displayed pipeline status view rebuilt its rows. Neither closed what it replaced: 9 widgets leaked per refresh, 145 with the view shown. The status view now rebuilds its rows only when the sub-pipes or their views change, and closes the rows it replaces (#176)PipeStatusis no longer anipywidgets.Widget: it is an immutable value with the same attributes and constructors, compared by identity, so every new status still notifiesstatusobserversPipelineStatusView.statusesis removed. It held the view’s row widgets, was never synced, and is now a private list; the rows still reach the frontend aschildren
Stop a diagram from freezing for good on an update that removes elements. Sprotty’s fade-out removed the faded elements when the eased animation time reached 1, which
easeInOutrounds to within 1e-8 of the end, while the animation only stops at the raw time 1. A frame landing a hair before the end (evenly spaced frame timestamps, 15 frames of 1000/60 ms for the 250 ms animation) removed them early, and the real last frame removed them again and threw. The throw happened inside an animation-frame callback, so the animation never finished and sprotty’s command stack waited on it forever: every later layout reached the browser and was never drawn (#95). Reported upstream as eclipse-sprotty/sprotty#573; the workaround can go once that is fixedClose the old pipeline when
diagram.pipeis replaced. Only its run was cancelled: its sub-pipes, intermediate marks and status views stayed open, and the diagram’sstylelink kept it alive, so every replacement left 37 widgets behind, 99 with the status view shown. A diagram now owns its pipe: replacing it closes the old pipe, its sub-pipes and the widgets they created (including astatus_widgetdisplayed elsewhere), neverdiagram.sourceor an inlet/outlet passed in.Pipe.close()closes what the pipe created (aPipelinealso closes its sub-pipes).style(also for a text sizer in a nested pipeline), the progress bar and the tools now follow the new pipe (#180)A pipe belongs to one diagram, with every pipe nested in it. Assigning a pipe that nests a pipe another open diagram owns raises
TraitError: both diagrams used to share its outlet, so one silently showed the other’s graph. Two diagrams sharing a source is a separate question (#178)A replaced pipe is closed, so assigning a pipe that nests a closed pipe, or any part of the current pipe (wrapping it, or reusing its stages), raises
TraitErrortoo: build a new pipe insteadThe rule also holds when a pipeline’s
pipeschanges in place, and before aPipelineis wired. Adding another open diagram’s stage to an assigned pipeline was accepted, so the stage ran in both, andPipeline(pipes=[...])rewired a live diagram’s stage before anything could refuse it. Both now raiseTraitErrorand change nothing, as does apipeslist with a closed stage or a stage listed twice (it was wired into a cycle). A stage added in place belongs to the diagram (and gets itsstyle); a stage removed in place is released, not closed (#191)
Close what a diagram owns when the diagram is closed.
Diagram.close()only released its pipe, so the pipe tree, the view, the tools and the toolbar stayed open: every closed diagram left 68 widgets behind, 130 with the status view shown. It now closes them with their layout and style widgets and drops its links to them, and leaves opendiagram.source, an inlet or outlet passed to a pipe, the viewer’scontrol_overlay, and alayoutor tooluipassed in. A closed diagram’s pipe is closed, so another diagram can no longer take it.Tool.close()also closes theuithe tool built, andViewer.close()its tools (#191)Log the routine “fixing N ids” message from
ValidationPipeat debug level instead of warning: assigning ids to id-less elements is the normal path (fix_null_id=True), so every diagram built from id-less elements logged itKeep the elements behind a collapsed view intact across layouts (#171, #187):
A hidden port, or a port on a hidden node, keeps its size, position, layout options and properties. The slack port that stands in for it in the layout used to overwrite them with its own 5×5 geometry, permanently. Any other port, including one that moved to another node, still takes its layout from the browser.
Each line of a wrapped label gets its own ids, and so do its sub-labels. The lines of a label with id
Lget the idsL#0,L#1, … (sub-labelsubgetssub#0, …). If another element already uses one of these derived ids, indexing gives that line a fresh id; ids you set yourself never change. Before, every line had the same id (L, or one generated id) and the same sub-label ids. A wrapped line shares its label’smetadata.The ELK JSON schema accepts the slack-port
keyandhiddenproperties, so a projection validates after a collapse and an expand.
Center a
Circlein its node:Circleno longer serializesxandyas itsradius. Thenode:roundrenderer takes its radii from the laid-out node, so a node larger than the circle drew the ellipse off center.xandythat you set are still serialized (#200)
Development#
Add
scripts/bench_pipeline.py, a headless benchmark ofDiagram.refresh(): it runs the real kernel pipeline and realelkjsinnodewith only the two browser stages stubbed, over deterministic graphs fromscripts/bench_graphs.py, and reports wall and per-stage time, comm messages and bytes in both directions, layout runs, a collapse refresh and a ten-refresh burst (#164, #167), and the comms opened per refresh (#176)Gate CI on the benchmark’s deterministic counts:
pixi run bench-checkcompares them toscripts/bench_baseline.jsonand fails on any change until the baseline is updated withpixi run bench-update(#176), including the live widgets left by replacingdiagram.pipe(#180) and by closing a diagram (#191)Run the benchmark on the oldest supported ipywidgets too: on 8.0.1 it crashed in its first refresh, because ipywidgets 8.0 opens an
ipykernelcomm directly and the harness never saw a message.pixi run bench-check-oldestgates it against the same baseline in thetest-oldest (ubuntu-latest)job (#191)Add
ast-grepstructural lint rules for Python and TypeScript (sgconfig.yml,scripts/ast-grep/), each with test cases, run bypixi run lint-ast-grepas part ofpixi run lint; a suppression must name its rule and still match something (#147)Lint prose with Vale in the
lintjob:pixi run lint-valechecks US English spelling and theproselintandwrite-goodstyles in the root*.mdfiles,docs/, docstrings and comments insrc/,tests/andscripts/, and notebook markdown, and fails on any warning or error. It replacescheck-spelling, which linted the built HTML and never failed (#181)The nbconvert kernel watchdog writes its stack dump to the kernel’s real stderr, not to the descriptor ipykernel captures, where the dump woke the capture thread and made it look busy. A second dump lists the pending
asynciotasks of the kernel’s event loops, with their await chains, and whether the shell locks are held (#177)Fix the
itest-nbconverthang in03_App.ipynb(#177). ipykernel 7.0 to 7.3 send shell replies raw on the socket their shell stream reads from, which can swallow the wake-up for a request that arrives during the send. The request then waits unread while the kernel idles. In nbconvert that happens when a cell prints into anipywidgets.Output:nbclientanswers with acomm_msgjust as the cell’s reply goes out, and the next cell never runs.The
itestandatestenvironments now lock ipykernel 7.4, which fixed it (ipykernel#1529).ipykernel 7.4 needs Python 3.11 or newer. For the Python 3.10 environments, the kernels of the
nbconvert--task and of the robot tests loadscripts/kernel_shell_reply_fix.py, a startup file that backports the fix and does nothing on ipykernel 7.4 or newer.utest-oldesttests it
Lower the
nbconvert--cell timeout from 1200 s to 40 s. The slowest healthy notebook in 4061 notebook runs from the last 45 CI runs took 8.2 s. The kernel watchdog now restarts with every cell and fires after 30 s without a new cell (#177)Fail CI when the committed
elkschema.jsondiffers from whatjlpm schemagenerates, and rebuild it whenelkgraph-json.tschanges (#187)
Documentation#
Document the shape API: a docstring for each class in
ipyelk.elements.shapes, and a Shapes page in the API reference with one table row for each shape (#66)
2.1.2#
Development#
Migrate to native Pydantic 2 validation and serialization; require
pydantic >=2.12,<3. Usemodel_dump()/model_dump_json()instead ofdict()/json(). Custom models now usemodel_config,Field(exclude=True), and native serializers instead ofConfig,merge_excluded, anddict()overrides. Nested subclass fields and graph references are preserved; shape serialization no longer mutates dimensions. Explicit serialization field selections are now respected.Fix Python typing throughout the package and run mypy as part of
pixi run lint.Minimum supported Python is now
3.10Upgrade the pinned
pixifrom0.34.0to0.67.0(andsetup-pixitov0.10.0); relocking updateslibgfortran513.2.0→14.2.0, fixing a macOS arm64 dyld failure that brokenumpy/bqplotin the example notebooksOnly reinstall requirements in the
07_Simulationexample whenipyelkis missing, and addtooltips to its control widgetsPreserve the normal labextension in
src/_dduring the coverage build (build-js-ext-cov), which emptied it and leftpixi run buildwith an unloadable extension (_build.load: "static")
@jupyrdf/jupyter-elk 2.1.2#
Add optional label tooltips and full-width separators above labels.
Observe source rewiring and apply initial kernel selections after model submission completes.
Report missing browser state for re-sync and surface text-measurement errors through the kernel error channel.
Discard widget views whose attachment host disappears while they load.
Attribute label hover feedback to the nearest hoverable element.
Fix the SVG exporter
enabledflag, which was alwaystrue(F5)Report browser-side layout failures to the kernel instead of silently emitting an empty layout (F6)
Make
ELKLayoutModel.layout()re-entrant: it stripped elementproperties(incl.cssClasses) off the shared inlet value in place, so a duplicaterunmessage or an overlapping refresh re-laid-out the stripped graph and pushed a style-less (black-and-white) diagram (F7)Render edge labels where ELK placed them:
ElkLabelcarried sprotty’sedgeLayoutFeature, whose EdgeLayoutPostprocessor re-anchors edge labels along the route and treats ELK’s absolute coordinates as a relative offset, shifting every edge label by roughly its edge’s origin (F8)Orient edge end symbols along the visible route: the adjacent-segment tangent collapsed to
atan2(0, 0)on the duplicated control points of elkjsSPLINESsections (arrowheads drawn 180° wrong, inside the target node) and followed the short exit stub ofPOLYLINEroutes instead of the visible diagonal; interior bends under a symbol’s footprint no longer make the trimmed shaft double back (F9)Drop the selection write-back race:
getSelection()resolves one action-queue slot later, so twoSelectActions dispatched close together each read the other action’s resulting state and wrote it back, flipping the selection tool’sidsforever — a self-sustaining oscillation that pegged the renderer’s main thread (F11)Render nested JupyterLab widgets in the pass that reveals them: the overlay was built from a registry populated by the previous render’s
snabbdomhooks, so a widget node that had just become visible produced no container until some later, unrelated re-render happened to run (F14)Make the selection write-back set-based so linked views cannot bounce: F11’s generation stamp is per view, but
change:idsis observed by every view of a shared model, and each view gathers the same selection in a different order, so a reordering kept dispatchingSelectActions between two views until the browser ran out of memory (F16)Add a
vitestunit-test harness (F5)Upgrade ELK.js from
0.9.3to0.12.0(#140).ElkEdgegains an optionalcontainerfield in the generated schema, and avitestlays out a graph with the real bundle to pin the port, edge-section andpropertiesround-trip contract.Upgrade
sprottyandsprotty-protocolfrom1.3.0to1.4.0, which requiresinversify ^6.1.3(pinned to6.2.2) andreflect-metadata ^0.2.2. Theinversifyshared module declares itsversionstatically: itslib/esmentry ships a version-lesspackage.json, so webpack module federation would otherwise register it as0and warn on every page load; a unit test keeps the static version exact, equal to the dependency pin and inside the declaredrequiredVersion.Drop the unused
sprotty-elkdependency: nothing injs/ever imported it,sprottyandsprotty-protocoldo not depend on it, and the built extension has no reference to it. Itselkjs ^0.8.2range was the only thing that conflicted with ELK.js0.12.0, so theresolutions.elkjsoverride goes with it (#140).
ipyelk 2.1.2#
Allow diagram construction without a running event loop.
Pipe.schedule_run()andDiagram.refresh()returnNonewhen they cannot schedule a task.Add optional
LabelProperties.tooltipandLabelProperties.separatorfields.Re-send widget state after a browser reports missing state, with throttling.
Echo terminal progress state twice, cancelling pending echoes on updates and close.
Fix
IDReport.message()printing literal{eid}/{el}placeholders (F1)Fix
Pipeline.check()/get_progress_value()crashing on an empty pipeline (F2)Give each
Toolits ownon_runcallback dispatcher (was shared across all tools) (F3)Surface pipe/diagram exceptions via an
on_errorcallback instead of silently dropping them in the asyncio done-callback (F4)Add a configurable
timeoutand a browser→kernel error channel toElkJS/BrowserTextSizerso a failed or silent browser layout no longer hangs the diagram (F6)Re-send the browser
runrequest with backoff until a frontend answers, so a pipe run before its diagram is displayed no longer waits on a message nobody received; a browser-reported layout error stops the retries immediately (F10)Treat an errored run as terminal for progress reporting:
PipeStatus.step()returnedNonefor errored pipes, soPipeline.get_progress_value()raisedTypeErrorinside the error path —on_errorsaw theTypeErrorinstead of the layout error, and thePipelineProgressBarsat “in progress” forever; the bar now fills as a visible warning (F10)Keep hidden elements in the shared
MarkIndexacross browser round trips:Node.dict()always drops hidden children, so rebuilding the index from a value that has been through the browser erased them andToggleCollapsedToolcould never reveal them again — nested widgets (such as the15_Nesting_Plotsfigures) never appeared.ElementIndex.update()now merges instead of requiring every id to be known: existing ids update in place, unknown ids are added, which also fixes aNotFoundErrorwhen slack ports introduced byVisibilityPipecome back from a layout (F12)Log a failed layout in
Diagram.refresh()instead of returning silently: a silently failed pipeline is indistinguishable from a hung diagram (F12)Fix the local
TextSizerfallback, which readself.source.value, assignedself.outlet.changesand returnedself.value— none of which exist onPipe— and therefore raised on its first statement (F13)Do not wait on a browser that cannot answer: with
IPYELK_NO_BROWSERset (asnbconvert --executedoes) a pipe gives up its roundtrip immediately instead of keeping a task alive across cell boundaries re-sendingrunrequests for 30 s, which wedged kernels on slower CI runners (F15)
2.1.1#
@jupyrdf/jupyter-elk 2.1.1#
restore license files
ipyelk 2.1.1#
restore license files
2.1.0 (broken)#
@jupyrdf/jupyter-elk 2.1.0#
support JupyterLab 4.1-4.3
ipyelk 2.1.0#
improve type hints
2.1.0a0#
@jupyrdf/jupyter-elk 2.1.0-alpha0#
Update dependencies
elkjs 0.9.3,sprotty 1.3,jupyterlab 4.2Add shim for
reflect-metadatavsfast-foundation
ipyelk 2.1.0a0#
Support
pydantic >=1,<3
2.0.0#
@jupyrdf/jupyter-elk 2.0.0#
Added control layer to allow jupyterlab widgets to exist on top of the diagram based on current node selection.
Adding controllable render delay for jupyterlab widgets used in diagram nodes.
Updated dependencies to
elkjs 0.8.2.Fixed diagram bounding box issue affecting node visibility (#94).
Improved test sizing that takes into account css properties (#97)
ipyelk 2.0.0#
Migrated to
ipywidgets >=8.0.1,<9Added simple visualizer widget for the diagram pipe status.
Fixed edge parent ownership affecting self edges (#101)
2.0.0a0#
@jupyrdf/jupyter-elk 2.0.0-alpha0#
Label Schema fix ([#73])
Element API overhaul (#88)
Add
mypyfor type checkingUse
pydanticforElementbase models
Overhaul top level interface (#89)
Backporting Sprotty Duplicate ID (#17)
Generalize the processing stages to use a common interface of Marks and simplify processing to composable pipes
Refactoring top level APIs and attempt and more streamlined
Diagramcreation
ipyelk 2.0.0a0#
1.0.1#
@jupyrdf/jupyter-elk 1.0.1#
hides some browser console messages
ipyelk 1.0.1#
1.0.0#
@jupyrdf/jupyter-elk 1.0.0#
updates for JupyterLab 3 (#6)
uses
@luminocomponents
ipyelk 1.0.0#
supports (and depends on) JupyterLab 3 (#6)
labextension is delivered as part of the
ipyelkpython package, no morelab buildnpmtarballs will still be uploaded
0.3.0#
@jupyrdf/jupyter-elk 0.3.0#
ipyelk 0.3.0#
Custom shapes (#60)
Ability to add custom SVG symbols and use as a reference for other elements
Custom node shapes
Custom connector end shapes for edges
Custom shapes for ports
Custom node label shapes
JupyterLab widgets rendering inside Node
Node compartments
Initial level of detail checks for labels
Rendering checks for nodes outside of view bounding box
Initial documentation (#64)
0.2.1#
@jupyrdf/jupyter-elk 0.2.1#
ipyelk 0.2.1#
0.2.0#
@jupyrdf/jupyter-elk 0.2.0#
ipyelk 0.2.0#
adds optional node label positioning with
NodeLabelPlacement(#15)vertical/horizontal alignment
inside/outside the node
improves evented updates of networkx to diagram with
ElkDiagram.connect(XElk)(#15)adds optional
ElkTextSizerfor interacting with browser text sizing (#15)add layout options widgets to control various layout parameters (#24)
add support for multiline node labels, port labels, and edge labels ([#35])
adds possibility of passing CSS classes through to the final DOM elements
0.1.3#
@jupyrdf/jupyter-elk 0.1.3#
includes all files using
npm publish
ipyelk 0.1.3#
updates some metadata for pypi
0.1.2#
ipyelk 0.1.2#
@jupyrdf/jupyter-elk 0.1.2 (broken)#
(failed) fix npm release process
0.1.1#
ipyelk 0.1.1#
initial release
@jupyrdf/jupyter-elk 0.1.1 (broken)#
initial release