You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a problem? Please describe.
Currently, all EdgeHandler implementations are always loaded, even if they are not used. This prevents proper tree-shaking and adds unnecessary weight to applications that don’t use all EdgeStyles.
EdgeHandlers are used to manage interactive behavior on selected edges. maxGraph provides 3 built-in handlers to match the main EdgeStyle implementations.
Historically, handler selection was based on:
the EdgeStyle implementation,
or a handlerKind category defined at registration (v0.20.0).
Historically, the creation logic for handlers lived in factory methods (create...Handler) on AbstractGraph. This had several drawbacks:
all handlers were always imported, even if unused (e.g. for visualization-only use cases),
customizing the handler meant subclassing both the handler and the factory methods.
Once #823 has been merged, those methods are removed from AbstractGraph, and tree-shaking is now possible when the SelectionCellsHandler plugin is not used.
Still, when the SelectionCellsHandler plugin is used, all EdgeHandler classes are declared and imported, even if some of the corresponding EdgeStyles are not registered or used.
This limits optimization, and we’d like to improve that further.
With tree-shaking enabled, only loading the necessary EdgeHandlers can decrease the bundle size.
The following figures are estimates done in #823:
Handlers used
Decreased size
Only default EdgeHandler
−7–8 kB
Only ElbowEdgeHandler
−5 kB (inherits EdgeHandler)
Only SegmentEdgeHandler
−0 kB (inherits Elbow)
Note
These numbers reflect the size reduction compared to the previous behavior, where all handlers were always bundled.
Where the coupling lives
Everything below reflects the state of the #823 branch, which is not merged yet.
The remaining coupling is a single field initializer in packages/core/src/view/plugin/SelectionCellsHandler.ts,
together with the three static imports it makes reachable:
The import graph around it, checked on that branch:
Apart from the public barrel re-exports in index.ts, which are side-effect free and tree-shakeable, SelectionCellsHandler is the only module in packages/core/src that references the four handler classes at runtime. Nothing else has to be untangled: the change is confined to that one file.
The inheritance chain EdgeSegmentHandler → ElbowEdgeHandler → EdgeHandler is a real runtime dependency, which is what makes the savings in the table above asymmetric.
VertexHandler references EdgeHandler only in type positions, and declares it with import type. This is not a blocker: even as a value import TypeScript elided it, and lib/esm/view/handler/VertexHandler.js contains no EdgeHandler import either way. Noted so the edge is not mistaken for one that still has to be cut, nor credited with a bundle win it never produced.
Describe the solution you'd like
This issue originally covered three goals. Two of them are addressed by #823, so only the first is left for this issue:
Goal
Status
Only load the handlers actually used
Open — the subject of this issue
Customize handler selection without subclassing the class managing the selection
Configure per Graph instance (local configuration, not global)
Already the case
#823 adds SelectionCellsHandler.setEdgeHandlerFactory(handlerKind, factory) and setVertexHandlerFactory(factory), along with the EdgeHandlerFactory / VertexHandlerFactory types. edgeHandlerFactories is a private per-instance class field, so there is no state shared between graphs and no global registry involved.
The remaining problem is therefore narrower than originally stated: how the default map is populated, not how users override it. A minimal app that only uses the default connector should only load the default handler.
Note
The original description stated that "in the ts-example-selected-features, the SegmentEdgeHandler is unused but still included". That is not accurate: this example calls registerOrthogonalEdgeStyle() and uses edgeStyle: 'orthogonalEdgeStyle', which is registered with handlerKind: 'segment'. EdgeSegmentHandler is genuinely required there, and by inheritance so are ElbowEdgeHandler and EdgeHandler. Per the table above, that example would gain ~0 kB.
Demonstrating the win needs an example that registers SelectionCellsHandler and only styles whose handlerKind is 'default', or no edge style at all. js-example-selected-features already matches: it registers the plugin and no edge style, so all its edges resolve to the 'default' kind. No new example is needed.
Retained approach
The implementation follows the "Register handlers from the Graph constructor" alternative below, with one refinement: instead of Graph looking up the plugin and calling the setters after initialization, the factories are passed as a construction option.
SelectionCellsHandler keeps only the 'default' entry in its initial factory map.
BaseGraph accepts a new option edgeHandlerFactories?: Record<EdgeStyleHandlerKind, EdgeHandlerFactory>. When set, the AbstractGraph constructor forwards it to the SelectionCellsHandler plugin.
getDefaultEdgeHandlerFactories() is exported from its own module and returns a new Record holding the three built-in factories, following the naming of getDefaultPlugins(). Graph passes it in its constructor, so Graph users keep the current behavior.
Documented in packages/website/docs/usage/cell-handlers.md, so that BaseGraph users know how to opt in.
Compared to calling the setters after plugin initialization, the option keeps the configuration declarative and visible at construction time, and makes it reusable by BaseGraph users who want all three handlers without depending on Graph.
This introduces a breaking change for BaseGraph users who rely on elbow or segment edge styles: without the new option, those edges fall back to the default handler. It is documented in CHANGELOG.md.
A follow-up will generalize the wiring: an optional onConfigure(options) hook in the plugin lifecycle will let every plugin read the graph options, replacing the plugin-specific code introduced here by a generic call over all plugins implementing the hook.
Describe alternatives you've considered
Since the public registration API now exists in #823, the alternatives no longer weigh the same. They are kept below with that in mind.
✅ Register handlers from the Graph constructor
Let Graph be responsible for registering the handlers. It would look up the plugin and register default handlers after plugin initialization.
BaseGraph would not do this
this promotes composition over inheritance
preserves current default behavior when using Graph
This became the cheapest option: it reduces to shipping an edgeHandlerFactories map holding only 'default', and having Graph (or getDefaultPlugins()) call setEdgeHandlerFactory('elbow', …) and setEdgeHandlerFactory('segment', …).
Note
This would need to be clearly documented, since the behavior differs slightly from current defaults.
✅ Plugin with two variants
Have a base version of SelectionCellsHandler that only registers the default handler, and a second one (the current one) that registers all 3 handlers.
BaseSelectionCellsHandler: only registers EdgeHandler
SelectionCellsHandler: extends base and adds ElbowEdgeHandler and SegmentEdgeHandler
Warning
Both plugins would share the same plugin id for compatibility, but this may lead to confusion if misused.
Limitation: doesn’t support composability. Users can’t change the config of existing handlers without subclassing the plugin. Note that this limitation is largely lifted by the setters added in #823, which makes this variant carry more machinery than the problem now needs.
✅ Dedicated plugin for handler selection
Introduce EdgeHandlerSelectorPlugin that stores the handler mapping.
SelectionCellsHandler uses it internally to retrieve the correct handler
for default behavior, we include EdgeHandlerSelectorPlugin in the default plugin list
✅ Pros:
a single implementation of the selection plugin
flexible and composable
handler registration is explicit
⚠️ Cons:
adds a dependency between plugins
when using a custom plugin list, users must remember to include it
overall complexity isn’t lower than having two plugin variants, and higher than registering from the Graph constructor
❌ Global registry
Store the handler map globally, like existing registries.
Drawback: goes against the local configuration pattern we introduced for plugins. Would also introduce implicit behavior, harder to debug. With #823 this can be ruled out: it would regress the per-instance configuration that already exists.
Acceptance criteria
A BaseGraph application that registers SelectionCellsHandler and does not opt in no longer bundles ElbowEdgeHandler or EdgeSegmentHandler.
Graph keeps its current behavior: all three handler kinds available with no extra user setup, and the same bundle size as today.
The gain is measured and reported in the pull request, on js-example-selected-features, which demonstrates it without any change. No example is added: a dedicated one is not worth its maintenance cost.
ts-example-selected-features registers orthogonalEdgeStyle, whose handler kind is 'segment', so it has to declare that factory to keep the per segment handles on its edges. Updating it is part of this work: it is the migration every affected application has to do, and it keeps the continuity of its recorded bundle size.
The migration is documented in CHANGELOG.md, as BaseGraph users must opt in to the elbow/segment handlers.
Bundle baseline
Size of the @maxgraph/core chunk of each example, as measured in #823 with ./scripts/build-all-examples.bash:
Only the without-defaults examples benefit from #823, as they are the ones that do not register SelectionCellsHandler. The four others grew by 0.14 to 0.20 kB, which is the cost of the factory map and its setters now held by the plugin. This issue targets those four, the applications that do register the plugin.
In #823, ts-example-without-defaults was the only example whose chunkSizeWarningLimit needed an update, from 300 to 221.
Is your feature request related to a problem? Please describe.
Currently, all EdgeHandler implementations are always loaded, even if they are not used. This prevents proper tree-shaking and adds unnecessary weight to applications that don’t use all
EdgeStyles.EdgeHandlers are used to manage interactive behavior on selected edges. maxGraph provides 3 built-in handlers to match the main
EdgeStyleimplementations.Historically, handler selection was based on:
EdgeStyleimplementation,handlerKindcategory defined at registration (v0.20.0).Historically, the creation logic for handlers lived in factory methods (
create...Handler) onAbstractGraph. This had several drawbacks:Once #823 has been merged, those methods are removed from
AbstractGraph, and tree-shaking is now possible when theSelectionCellsHandlerplugin is not used.Still, when the
SelectionCellsHandlerplugin is used, all EdgeHandler classes are declared and imported, even if some of the correspondingEdgeStyles are not registered or used.This limits optimization, and we’d like to improve that further.
With tree-shaking enabled, only loading the necessary
EdgeHandlers can decrease the bundle size.The following figures are estimates done in #823:
EdgeHandlerElbowEdgeHandlerSegmentEdgeHandlerNote
These numbers reflect the size reduction compared to the previous behavior, where all handlers were always bundled.
Where the coupling lives
Everything below reflects the state of the #823 branch, which is not merged yet.
The remaining coupling is a single field initializer in
packages/core/src/view/plugin/SelectionCellsHandler.ts,together with the three static imports it makes reachable:
The import graph around it, checked on that branch:
index.ts, which are side-effect free and tree-shakeable,SelectionCellsHandleris the only module inpackages/core/srcthat references the four handler classes at runtime. Nothing else has to be untangled: the change is confined to that one file.EdgeSegmentHandler → ElbowEdgeHandler → EdgeHandleris a real runtime dependency, which is what makes the savings in the table above asymmetric.VertexHandlerreferencesEdgeHandleronly in type positions, and declares it withimport type. This is not a blocker: even as a value import TypeScript elided it, andlib/esm/view/handler/VertexHandler.jscontains noEdgeHandlerimport either way. Noted so the edge is not mistaken for one that still has to be cut, nor credited with a bundle win it never produced.Describe the solution you'd like
This issue originally covered three goals. Two of them are addressed by #823, so only the first is left for this issue:
Graphinstance (local configuration, not global)#823 adds
SelectionCellsHandler.setEdgeHandlerFactory(handlerKind, factory)andsetVertexHandlerFactory(factory), along with theEdgeHandlerFactory/VertexHandlerFactorytypes.edgeHandlerFactoriesis a private per-instance class field, so there is no state shared between graphs and no global registry involved.The remaining problem is therefore narrower than originally stated: how the default map is populated, not how users override it. A minimal app that only uses the default connector should only load the default handler.
Note
The original description stated that "in the
ts-example-selected-features, theSegmentEdgeHandleris unused but still included". That is not accurate: this example callsregisterOrthogonalEdgeStyle()and usesedgeStyle: 'orthogonalEdgeStyle', which is registered withhandlerKind: 'segment'.EdgeSegmentHandleris genuinely required there, and by inheritance so areElbowEdgeHandlerandEdgeHandler. Per the table above, that example would gain ~0 kB.Demonstrating the win needs an example that registers
SelectionCellsHandlerand only styles whosehandlerKindis'default', or no edge style at all.js-example-selected-featuresalready matches: it registers the plugin and no edge style, so all its edges resolve to the'default'kind. No new example is needed.Retained approach
The implementation follows the "Register handlers from the
Graphconstructor" alternative below, with one refinement: instead ofGraphlooking up the plugin and calling the setters after initialization, the factories are passed as a construction option.SelectionCellsHandlerkeeps only the'default'entry in its initial factory map.BaseGraphaccepts a new optionedgeHandlerFactories?: Record<EdgeStyleHandlerKind, EdgeHandlerFactory>. When set, theAbstractGraphconstructor forwards it to theSelectionCellsHandlerplugin.getDefaultEdgeHandlerFactories()is exported from its own module and returns a newRecordholding the three built-in factories, following the naming ofgetDefaultPlugins().Graphpasses it in its constructor, soGraphusers keep the current behavior.packages/website/docs/usage/cell-handlers.md, so thatBaseGraphusers know how to opt in.Compared to calling the setters after plugin initialization, the option keeps the configuration declarative and visible at construction time, and makes it reusable by
BaseGraphusers who want all three handlers without depending onGraph.This introduces a breaking change for
BaseGraphusers who rely on elbow or segment edge styles: without the new option, those edges fall back to the default handler. It is documented inCHANGELOG.md.A follow-up will generalize the wiring: an optional
onConfigure(options)hook in the plugin lifecycle will let every plugin read the graph options, replacing the plugin-specific code introduced here by a generic call over all plugins implementing the hook.Describe alternatives you've considered
Since the public registration API now exists in #823, the alternatives no longer weigh the same. They are kept below with that in mind.
✅ Register handlers from the
GraphconstructorLet
Graphbe responsible for registering the handlers. It would look up the plugin and register default handlers after plugin initialization.BaseGraphwould not do thisGraphThis became the cheapest option: it reduces to shipping an
edgeHandlerFactoriesmap holding only'default', and havingGraph(orgetDefaultPlugins()) callsetEdgeHandlerFactory('elbow', …)andsetEdgeHandlerFactory('segment', …).Note
This would need to be clearly documented, since the behavior differs slightly from current defaults.
✅ Plugin with two variants
Have a base version of
SelectionCellsHandlerthat only registers the default handler, and a second one (the current one) that registers all 3 handlers.BaseSelectionCellsHandler: only registersEdgeHandlerSelectionCellsHandler: extends base and addsElbowEdgeHandlerandSegmentEdgeHandlerWarning
Both plugins would share the same plugin
idfor compatibility, but this may lead to confusion if misused.Limitation: doesn’t support composability. Users can’t change the config of existing handlers without subclassing the plugin. Note that this limitation is largely lifted by the setters added in #823, which makes this variant carry more machinery than the problem now needs.
✅ Dedicated plugin for handler selection
Introduce
EdgeHandlerSelectorPluginthat stores the handler mapping.SelectionCellsHandleruses it internally to retrieve the correct handlerEdgeHandlerSelectorPluginin the default plugin list✅ Pros:
Graphconstructor❌ Global registry
Store the handler map globally, like existing registries.
Drawback: goes against the local configuration pattern we introduced for plugins. Would also introduce implicit behavior, harder to debug. With #823 this can be ruled out: it would regress the per-instance configuration that already exists.
Acceptance criteria
BaseGraphapplication that registersSelectionCellsHandlerand does not opt in no longer bundlesElbowEdgeHandlerorEdgeSegmentHandler.Graphkeeps its current behavior: all three handler kinds available with no extra user setup, and the same bundle size as today.js-example-selected-features, which demonstrates it without any change. No example is added: a dedicated one is not worth its maintenance cost.ts-example-selected-featuresregistersorthogonalEdgeStyle, whose handler kind is'segment', so it has to declare that factory to keep the per segment handles on its edges. Updating it is part of this work: it is the migration every affected application has to do, and it keeps the continuity of its recorded bundle size.CHANGELOG.md, asBaseGraphusers must opt in to the elbow/segment handlers.Bundle baseline
Size of the
@maxgraph/corechunk of each example, as measured in #823 with./scripts/build-all-examples.bash:js-examplejs-example-selected-featuresjs-example-without-defaultsts-examplets-example-selected-featurests-example-without-defaultsOnly the
without-defaultsexamples benefit from #823, as they are the ones that do not registerSelectionCellsHandler. The four others grew by 0.14 to 0.20 kB, which is the cost of the factory map and its setters now held by the plugin. This issue targets those four, the applications that do register the plugin.In #823,
ts-example-without-defaultswas the only example whosechunkSizeWarningLimitneeded an update, from 300 to 221.Additional context
This topic has been previously discussed:
It also relates to ongoing improvements for tree-shaking and plugin modularity.