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]
 ]

enter image description here

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]}