Allow multiple GUI elements to react dynamically to interaction with a single element
n = 120;
names = Range[n];
pts = AssociationThread[names -> N@CirclePoints[n]];
edges = RandomSample[Subsets[names, {2}], 250];
There are two reasons why Dynamic
scales badly:
there is no (documented) way to tell a "DynamicObject" to update, one can only count on dependency tree which is created.
one can track only Symbols
The second one implies that big lists/associations will always update each Dynamic
they are mentioned in. Even when each one only cares about a specific value.
Additionaly symbols renaming/management tools in Mathematica are surprisingly limited/not suited for a type of job I am about to show. The following solution may be unreadable at first sight.
The idea is to create symbols: state1
, state2
,... instead of using state[[1]]
. This way only specific Dynamic
will be triggered when needed, not all of state[[..]]
.
DynamicModule[{},
Graphics[{
(
ToExpression[
"{sA:=state" <> ToString[#] <> ", sB:=state" <> ToString[#2] <> "}",
StandardForm,
Hold
] /. Hold[spec_] :> With[spec,
{ Dynamic @ If[TrueQ[sA || sB], Red, Black],
Line[{pts[#1], pts[#2]}]
}
]
) & @@@ edges
,
PointSize[0.025],
(
ToExpression[
"{sA:=state" <> ToString[#] <> "}",
StandardForm,
Hold
] /. Hold[spec_] :> With[spec,
{ Dynamic @ If[TrueQ[sA], Red, Black],
EventHandler[ Point @ pts[#],
{"MouseEntered" :> (sA = True), "MouseExited" :> (sA = False)}
]
}
]
) & /@ names
},
ImageSize -> Large]
]
Ok, we can go even further. This code still communicates with the Kernel while it doesn't have to:
ClearAll["state*"]
ToExpression[
"{" <> StringJoin[
Riffle[Table["state" <> ToString[i] <> "=False", {i, n}], ","]] <>
"}",
StandardForm,
Function[vars,
DynamicModule[vars,
Graphics[{(ToExpression[
"{sA:=state" <> ToString[#] <> ", sB:=state" <>
ToString[#2] <> "}", StandardForm, Hold] /.
Hold[spec_] :> With[spec, {RawBoxes@DynamicBox[
FEPrivate`If[
FEPrivate`SameQ[FEPrivate`Or[sA, sB], True],
RGBColor[1, 0, 1], RGBColor[0, 1, 0]]],
Line[{pts[#1], pts[#2]}]}]) & @@@ edges,
PointSize[
0.025], (ToExpression["{sA:=state" <> ToString[#] <> "}",
StandardForm, Hold] /.
Hold[spec_] :>
With[spec, {RawBoxes@
DynamicBox[
FEPrivate`If[SameQ[sA, True], RGBColor[1, 0, 1],
RGBColor[0, 1, 0]]],
EventHandler[
Point@pts[#], {"MouseEntered" :> FEPrivate`Set[sA, True],
"MouseExited" :> FEPrivate`Set[sA, False]}]}]) & /@
names}, ImageSize -> Large]]
,
HoldAll
]
]
Finally something neat completely FrontEnd side :)
Here is a refactor of Kuba's wonderful answer. I hope it may help somebody understand the order in which things are evaluated better. This version should also be resistant against conflicting symbol names, though perhaps it would have been easier to achieve that using contexts. A few things that I thought might be unnecessary have been removed.
n = 100;
names = Permute[Range[10*n], RandomPermutation[10*n]][[;; n]];
pts = AssociationThread[names -> N@CirclePoints[n]];
edgesIndices =
RandomSample[Subsets[Range[n], {2}], Quotient[n Log[n], 2]];
edges = Map[names[[#]] &, edgesIndices, {2}];
heldStates =
Join @@ (ToExpression["state" <> ToString[#] , InputForm, Hold] & /@
names);
dynModVars = List @@@ Hold@Evaluate[Set @@@ Thread[{
heldStates,
Hold @@ ConstantArray[False, n]
}, Hold]];
preMapThread = Apply[List,
Hold@Evaluate[
Join[heldStates[[#]] & /@ Transpose@edgesIndices, Transpose@edges]],
{1, 2}];
preAppMap = Thread[{heldStates, Hold @@ names}, Hold];
edgeDisplayerMaker = Function[
{sA, sB, name1, name2},
{DynamicBox[
If[FEPrivate`Or[sA, sB], RGBColor[1, 0, 1], RGBColor[0, 1, 0]]],
Line[{pts[name1], pts[name2]}]}
, HoldAll];
interactivePointMaker = Function[
{sA, name},
{DynamicBox[If[sA, RGBColor[1, 0, 1], RGBColor[0, 1, 0]]],
EventHandler[
Point@pts[name], {"MouseEntered" :> FEPrivate`Set[sA, True],
"MouseExited" :> FEPrivate`Set[sA, False]}]}, HoldAll];
Perhaps the structure of the DynamicModule
is now a little clearer.
DynamicModule @@ {
Unevaluated @@ dynModVars
,
Unevaluated@
Graphics[{
MapThread @@ {
edgeDisplayerMaker,
Unevaluated @@ preMapThread},
PointSize[0.025],
List @@ interactivePointMaker @@@ preAppMap
}, ImageSize -> Large]}