Version: 10.1.0
Browser: Chromium (WebView2), also reproducible in any browser
Summary
Circle.resize() stores its computed radius on the node's options object:
this.options.size = diameter / 2;
A shape shares the same options object as its Node (new Circle(this.options, …) in Node.updateShape), so this creates an own size property on the node that shadows the value from nodes.size / group / global defaults. It is never cleared.
As a result, once a node has been rendered as a circle, switching nodes.shape to any label-outside shape sizes that node from the leftover value — dot, star, square etc. compute width = height = 2 * options.size. Every node ends up with the radius of the label it happened to have while it was a circle, so a formerly-circle graph renders as dots of wildly differing sizes with no per-node size anywhere in the data.
Steps to reproduce
const nodes = new vis.DataSet([
{ id: 1, label: 'A' },
{ id: 2, label: 'a very much longer label' },
]);
const network = new vis.Network(container, { nodes, edges: new vis.DataSet([]) }, {
nodes: { shape: 'dot', size: 25 },
});
// Both nodes are 25 here, as configured.
console.log(network.body.nodes[1].options.size,
network.body.nodes[2].options.size); // 25 25
network.setOptions({ nodes: { shape: 'circle', size: 25 } });
// (allow a frame to render, e.g. in network.once('afterDrawing', …))
network.setOptions({ nodes: { shape: 'dot', size: 25 } });
console.log(network.body.nodes[1].options.size,
network.body.nodes[2].options.size); // e.g. 21 78 - label-derived, not 25
Expected: both nodes return to size: 25 and render identically, since neither has a size in its data.
Actual: each node keeps half the diameter of the circle it was, and the dot rendering reflects that. The graph never recovers until the node data is reloaded.
Why it does not self-correct
NodesHandler.setOptions updates only the handler's own options. For shape it calls node.updateShape(), and for size it calls node.needsRefresh() — neither rewrites node.options.size, so the shadowing own property survives and the forced re-measure just reads it back.
- The only restore path is
Node.setValueRange, whose else-branch does this.options.size = this.baseSize. That runs when node data is (re)loaded, not when options change.
Suggested fix
options.size is being used as a scratch field to hand the derived radius to Circle.updateBoundingBox, which reads it for all four bounds:
updateBoundingBox(x, y) {
this.boundingBox.top = y - this.options.size;
this.boundingBox.left = x - this.options.size;
this.boundingBox.right = x + this.options.size;
this.boundingBox.bottom = y + this.options.size;
}
resize() already sets this.radius = this.width / 2 alongside it, so updateBoundingBox can use this.radius (or this.width / 2) and the write to this.options.size can be dropped entirely. That keeps the derived radius on the shape, where it belongs, instead of mutating shared node options.
Note the write cannot simply be removed without that change — updateBoundingBox would then read a stale size and break hit-testing, fit() and rubber-band selection for circle nodes.
Notes
Version: 10.1.0
Browser: Chromium (WebView2), also reproducible in any browser
Summary
Circle.resize()stores its computed radius on the node's options object:A shape shares the same options object as its
Node(new Circle(this.options, …)inNode.updateShape), so this creates an ownsizeproperty on the node that shadows the value fromnodes.size/ group / global defaults. It is never cleared.As a result, once a node has been rendered as a
circle, switchingnodes.shapeto any label-outside shape sizes that node from the leftover value —dot,star,squareetc. computewidth = height = 2 * options.size. Every node ends up with the radius of the label it happened to have while it was a circle, so a formerly-circlegraph renders as dots of wildly differing sizes with no per-nodesizeanywhere in the data.Steps to reproduce
Expected: both nodes return to
size: 25and render identically, since neither has asizein its data.Actual: each node keeps half the diameter of the circle it was, and the
dotrendering reflects that. The graph never recovers until the node data is reloaded.Why it does not self-correct
NodesHandler.setOptionsupdates only the handler's own options. Forshapeit callsnode.updateShape(), and forsizeit callsnode.needsRefresh()— neither rewritesnode.options.size, so the shadowing own property survives and the forced re-measure just reads it back.Node.setValueRange, whose else-branch doesthis.options.size = this.baseSize. That runs when node data is (re)loaded, not when options change.Suggested fix
options.sizeis being used as a scratch field to hand the derived radius toCircle.updateBoundingBox, which reads it for all four bounds:resize()already setsthis.radius = this.width / 2alongside it, soupdateBoundingBoxcan usethis.radius(orthis.width / 2) and the write tothis.options.sizecan be dropped entirely. That keeps the derived radius on the shape, where it belongs, instead of mutating shared node options.Note the write cannot simply be removed without that change —
updateBoundingBoxwould then read a stale size and break hit-testing,fit()and rubber-band selection for circle nodes.Notes
circleis affected.ellipse,box,databaseandtextare autosized too, but size themselves throughwidth/heightand leaveoptions.sizealone.circleignoressize(If the node shapeis circle, how do I set the node size #1975, Can't to change size of circle node vis#4230). That one is about the shape sizing itself to its label, which is intended. This report is about that derived value escaping onto the node and affecting other shapes.