19
$\begingroup$

Has somebody tried to program for data visualization an interactive Collapsible Tree like shown here: http://bl.ocks.org/mbostock/4339083. This implements the Reingold-Tilford algorithm.

enter image description here

data=ImportString[

  ("{ \"name\": \"flare\", \"children\": [  {   \"name\": \"analytics\",   \"children\": [    {     \"name\": \"cluster\",     \"children\": [      {\"name\": \"AgglomerativeCluster\", \"size\": 3938},      {\"name\": \"CommunityStructure\", \"size\": 3812},      {\"name\": \"HierarchicalCluster\", \"size\": 6714},      {\"name\": \"MergeEdge\", \"size\": 743}     ]    },    {     \"name\": \"graph\",     \"children\": [      {\"name\": \"BetweennessCentrality\", \"size\": 3534},      {\"name\": \"LinkDistance\", \"size\": 5731},      {\"name\": \"MaxFlowMinCut\", \"size\": 7840},      {\"name\": \"ShortestPaths\", \"size\": 5914},      {\"name\": \"SpanningTree\", \"size\": 3416}     ]    },    {     \"name\": \"optimization\",     \"children\": [      {\"name\": \"AspectRatioBanker\", \"size\": 7074}     ]    }   ]  },  {   \"name\": \"animate\",   \"children\": [    {\"name\": \"Easing\", \"size\": 17010},    {\"name\": \"FunctionSequence\", \"size\": 5842},    {     \"name\": \"interpolate\",     \"children\": [      {\"name\": \"ArrayInterpolator\", \"size\": 1983},      {\"name\": \"ColorInterpolator\", \"size\": 2047},      {\"name\": \"DateInterpolator\", \"size\": 1375},      {\"name\": \"Interpolator\", \"size\": 8746},      {\"name\": \"MatrixInterpolator\", \"size\": 2202},      {\"name\": \"NumberInterpolator\", \"size\": 1382},      {\"name\": \"ObjectInterpolator\", \"size\": 1629},      {\"name\": \"PointInterpolator\", \"size\": 1675},      {\"name\": \"RectangleInterpolator\", \"size\": 2042}     ]    },    {\"name\": \"ISchedulable\", \"size\": 1041},    {\"name\": \"Parallel\", \"size\": 5176},    {\"name\": \"Pause\", \"size\": 449},    {\"name\": \"Scheduler\", \"size\": 5593},    {\"name\": \"Sequence\", \"size\": 5534},    {\"name\": \"Transition\", \"size\": 9201},    {\"name\": \"Transitioner\", \"size\": 19975},    {\"name\": \"TransitionEvent\", \"size\": 1116},    {\"name\": \"Tween\", \"size\": 6006}   ]  },  {   \"name\": \"data\",   \"children\": [    {     \"name\": \"converters\",     \"children\": [      {\"name\": \"Converters\", \"size\": 721},      {\"name\": \"DelimitedTextConverter\", \"size\": 4294},      {\"name\": \"GraphMLConverter\", \"size\": 9800},      {\"name\": \"IDataConverter\", \"size\": 1314},      {\"name\": \"JSONConverter\", \"size\": 2220}     ]    },    {\"name\": \"DataField\", \"size\": 1759},    {\"name\": \"DataSchema\", \"size\": 2165},    {\"name\": \"DataSet\", \"size\": 586},    {\"name\": \"DataSource\", \"size\": 3331},    {\"name\": \"DataTable\", \"size\": 772},    {\"name\": \"DataUtil\", \"size\": 3322}   ]  },  {   \"name\": \"display\",   \"children\": [    {\"name\": \"DirtySprite\", \"size\": 8833},    {\"name\": \"LineSprite\", \"size\": 1732},    {\"name\": \"RectSprite\", \"size\": 3623},    {\"name\": \"TextSprite\", \"size\": 10066}   ]  },  {   \"name\": \"flex\",   \"children\": [    {\"name\": \"FlareVis\", \"size\": 4116}   ]  },  {   \"name\": \"physics\",   \"children\": [    {\"name\": \"DragForce\", \"size\": 1082},    {\"name\": \"GravityForce\", \"size\": 1336},    {\"name\": \"IForce\", \"size\": 319},    {\"name\": \"NBodyForce\", \"size\": 10498},    {\"name\": \"Particle\", \"size\": 2822},    {\"name\": \"Simulation\", \"size\": 9983},    {\"name\": \"Spring\", \"size\": 2213},    {\"name\": \"SpringForce\", \"size\": 1681}   ]  },  {   \"name\": \"query\",   \"children\": [    {\"name\": \"AggregateExpression\", \"size\": 1616},    {\"name\": \"And\", \"size\": 1027},    {\"name\": \"Arithmetic\", \"size\": 3891},    {\"name\": \"Average\", \"size\": 891},    {\"name\": \"BinaryExpression\", \"size\": 2893},    {\"name\": \"Comparison\", \"size\": 5103},    {\"name\": \"CompositeExpression\", \"size\": 3677},    {\"name\": \"Count\", \"size\": 781},    {\"name\": \"DateUtil\", \"size\": 4141},    {\"name\": \"Distinct\", \"size\": 933},    {\"name\": \"Expression\", \"size\": 5130},    {\"name\": \"ExpressionIterator\", \"size\": 3617},    {\"name\": \"Fn\", \"size\": 3240},    {\"name\": \"If\", \"size\": 2732},    {\"name\": \"IsA\", \"size\": 2039},    {\"name\": \"Literal\", \"size\": 1214},    {\"name\": \"Match\", \"size\": 3748},    {\"name\": \"Maximum\", \"size\": 843},    {     \"name\": \"methods\",     \"children\": [      {\"name\": \"add\", \"size\": 593},      {\"name\": \"and\", \"size\": 330},      {\"name\": \"average\", \"size\": 287},      {\"name\": \"count\", \"size\": 277},      {\"name\": \"distinct\", \"size\": 292},      {\"name\": \"div\", \"size\": 595},      {\"name\": \"eq\", \"size\": 594},      {\"name\": \"fn\", \"size\": 460},      {\"name\": \"gt\", \"size\": 603},      {\"name\": \"gte\", \"size\": 625},      {\"name\": \"iff\", \"size\": 748},      {\"name\": \"isa\", \"size\": 461},      {\"name\": \"lt\", \"size\": 597},      {\"name\": \"lte\", \"size\": 619},      {\"name\": \"max\", \"size\": 283},      {\"name\": \"min\", \"size\": 283},      {\"name\": \"mod\", \"size\": 591},      {\"name\": \"mul\", \"size\": 603},      {\"name\": \"neq\", \"size\": 599},      {\"name\": \"not\", \"size\": 386},      {\"name\": \"or\", \"size\": 323},      {\"name\": \"orderby\", \"size\": 307},      {\"name\": \"range\", \"size\": 772},      {\"name\": \"select\", \"size\": 296},      {\"name\": \"stddev\", \"size\": 363},      {\"name\": \"sub\", \"size\": 600},      {\"name\": \"sum\", \"size\": 280},      {\"name\": \"update\", \"size\": 307},      {\"name\": \"variance\", \"size\": 335},      {\"name\": \"where\", \"size\": 299},      {\"name\": \"xor\", \"size\": 354},      {\"name\": \"_\", \"size\": 264}     ]    },    {\"name\": \"Minimum\", \"size\": 843},    {\"name\": \"Not\", \"size\": 1554},    {\"name\": \"Or\", \"size\": 970},    {\"name\": \"Query\", \"size\": 13896},    {\"name\": \"Range\", \"size\": 1594},    {\"name\": \"StringUtil\", \"size\": 4130},    {\"name\": \"Sum\", \"size\": 791},    {\"name\": \"Variable\", \"size\": 1124},    {\"name\": \"Variance\", \"size\": 1876},    {\"name\": \"Xor\", \"size\": 1101}   ]  },  {   \"name\": \"scale\",   \"children\": [    {\"name\": \"IScaleMap\", \"size\": 2105},    {\"name\": \"LinearScale\", \"size\": 1316},    {\"name\": \"LogScale\", \"size\": 3151},    {\"name\": \"OrdinalScale\", \"size\": 3770},    {\"name\": \"QuantileScale\", \"size\": 2435},    {\"name\": \"QuantitativeScale\", \"size\": 4839},    {\"name\": \"RootScale\", \"size\": 1756},    {\"name\": \"Scale\", \"size\": 4268},    {\"name\": \"ScaleType\", \"size\": 1821},    {\"name\": \"TimeScale\", \"size\": 5833}   ]  },  {   \"name\": \"util\",   \"children\": [    {\"name\": \"Arrays\", \"size\": 8258},    {\"name\": \"Colors\", \"size\": 10001},    {\"name\": \"Dates\", \"size\": 8217},    {\"name\": \"Displays\", \"size\": 12555},    {\"name\": \"Filter\", \"size\": 2324},    {\"name\": \"Geometry\", \"size\": 10993},    {     \"name\": \"heap\",     \"children\": [      {\"name\": \"FibonacciHeap\", \"size\": 9354},      {\"name\": \"HeapNode\", \"size\": 1233}     ]    },    {\"name\": \"IEvaluable\", \"size\": 335},    {\"name\": \"IPredicate\", \"size\": 383},    {\"name\": \"IValueProxy\", \"size\": 874},    {     \"name\": \"math\",     \"children\": [      {\"name\": \"DenseMatrix\", \"size\": 3165},      {\"name\": \"IMatrix\", \"size\": 2815},      {\"name\": \"SparseMatrix\", \"size\": 3366}     ]    },    {\"name\": \"Maths\", \"size\": 17705},    {\"name\": \"Orientation\", \"size\": 1486},    {     \"name\": \"palette\",     \"children\": [      {\"name\": \"ColorPalette\", \"size\": 6367},      {\"name\": \"Palette\", \"size\": 1229},      {\"name\": \"ShapePalette\", \"size\": 2059},      {\"name\": \"SizePalette\", \"size\": 2291}     ]    },    {\"name\": \"Property\", \"size\": 5559},    {\"name\": \"Shapes\", \"size\": 19118},    {\"name\": \"Sort\", \"size\": 6887},    {\"name\": \"Stats\", \"size\": 6557},    {\"name\": \"Strings\", \"size\": 22026}   ]  },  {   \"name\": \"vis\",   \"children\": [    {     \"name\": \"axis\",     \"children\": [      {\"name\": \"Axes\", \"size\": 1302},      {\"name\": \"Axis\", \"size\": 24593},      {\"name\": \"AxisGridLine\", \"size\": 652},      {\"name\": \"AxisLabel\", \"size\": 636},      {\"name\": \"CartesianAxes\", \"size\": 6703}     ]    },    {     \"name\": \"controls\",     \"children\": [      {\"name\": \"AnchorControl\", \"size\": 2138},      {\"name\": \"ClickControl\", \"size\": 3824},      {\"name\": \"Control\", \"size\": 1353},      {\"name\": \"ControlList\", \"size\": 4665},      {\"name\": \"DragControl\", \"size\": 2649},      {\"name\": \"ExpandControl\", \"size\": 2832},      {\"name\": \"HoverControl\", \"size\": 4896},      {\"name\": \"IControl\", \"size\": 763},      {\"name\": \"PanZoomControl\", \"size\": 5222},      {\"name\": \"SelectionControl\", \"size\": 7862},      {\"name\": \"TooltipControl\", \"size\": 8435}     ]    },    {     \"name\": \"data\",     \"children\": [      {\"name\": \"Data\", \"size\": 20544},      {\"name\": \"DataList\", \"size\": 19788},      {\"name\": \"DataSprite\", \"size\": 10349},      {\"name\": \"EdgeSprite\", \"size\": 3301},      {\"name\": \"NodeSprite\", \"size\": 19382},      {       \"name\": \"render\",       \"children\": [        {\"name\": \"ArrowType\", \"size\": 698},        {\"name\": \"EdgeRenderer\", \"size\": 5569},        {\"name\": \"IRenderer\", \"size\": 353},        {\"name\": \"ShapeRenderer\", \"size\": 2247}       ]      },      {\"name\": \"ScaleBinding\", \"size\": 11275},      {\"name\": \"Tree\", \"size\": 7147},      {\"name\": \"TreeBuilder\", \"size\": 9930}     ]    },    {     \"name\": \"events\",     \"children\": [      {\"name\": \"DataEvent\", \"size\": 2313},      {\"name\": \"SelectionEvent\", \"size\": 1880},      {\"name\": \"TooltipEvent\", \"size\": 1701},      {\"name\": \"VisualizationEvent\", \"size\": 1117}     ]    },    {     \"name\": \"legend\",     \"children\": [      {\"name\": \"Legend\", \"size\": 20859},      {\"name\": \"LegendItem\", \"size\": 4614},      {\"name\": \"LegendRange\", \"size\": 10530}     ]    },    {     \"name\": \"operator\",     \"children\": [      {       \"name\": \"distortion\",       \"children\": [        {\"name\": \"BifocalDistortion\", \"size\": 4461},        {\"name\": \"Distortion\", \"size\": 6314},        {\"name\": \"FisheyeDistortion\", \"size\": 3444}       ]      },      {       \"name\": \"encoder\",       \"children\": [        {\"name\": \"ColorEncoder\", \"size\": 3179},        {\"name\": \"Encoder\", \"size\": 4060},        {\"name\": \"PropertyEncoder\", \"size\": 4138},        {\"name\": \"ShapeEncoder\", \"size\": 1690},        {\"name\": \"SizeEncoder\", \"size\": 1830}       ]      },      {       \"name\": \"filter\",       \"children\": [        {\"name\": \"FisheyeTreeFilter\", \"size\": 5219},        {\"name\": \"GraphDistanceFilter\", \"size\": 3165},        {\"name\": \"VisibilityFilter\", \"size\": 3509}       ]      },      {\"name\": \"IOperator\", \"size\": 1286},      {       \"name\": \"label\",       \"children\": [        {\"name\": \"Labeler\", \"size\": 9956},        {\"name\": \"RadialLabeler\", \"size\": 3899},        {\"name\": \"StackedAreaLabeler\", \"size\": 3202}       ]      },      {       \"name\": \"layout\",       \"children\": [        {\"name\": \"AxisLayout\", \"size\": 6725},        {\"name\": \"BundledEdgeRouter\", \"size\": 3727},        {\"name\": \"CircleLayout\", \"size\": 9317},        {\"name\": \"CirclePackingLayout\", \"size\": 12003},        {\"name\": \"DendrogramLayout\", \"size\": 4853},        {\"name\": \"ForceDirectedLayout\", \"size\": 8411},        {\"name\": \"IcicleTreeLayout\", \"size\": 4864},        {\"name\": \"IndentedTreeLayout\", \"size\": 3174},        {\"name\": \"Layout\", \"size\": 7881},        {\"name\": \"NodeLinkTreeLayout\", \"size\": 12870},        {\"name\": \"PieLayout\", \"size\": 2728},        {\"name\": \"RadialTreeLayout\", \"size\": 12348},        {\"name\": \"RandomLayout\", \"size\": 870},        {\"name\": \"StackedAreaLayout\", \"size\": 9121},        {\"name\": \"TreeMapLayout\", \"size\": 9191}       ]      },      {\"name\": \"Operator\", \"size\": 2490},      {\"name\": \"OperatorList\", \"size\": 5248},      {\"name\": \"OperatorSequence\", \"size\": 4190},      {\"name\": \"OperatorSwitch\", \"size\": 2581},      {\"name\": \"SortOperator\", \"size\": 2023}     ]    },    {\"name\": \"Visualization\", \"size\": 16540}   ]  } ]}")
  ,
  "RawJSON"
]
$\endgroup$
  • 3
    $\begingroup$ How about providing a sample dataset to facilitate experiments? $\endgroup$ – Yves Klett Dec 14 '16 at 12:45
  • 1
    $\begingroup$ I would use the "LayeredEmbedding" GraphLayout (which probably implements something Reingold-Tilford-like) on a tree where some branches were trimmed, then hope that it produces a consitent vertex ordering, regardless of how much you trim. If you run into issues with preserving ordering, also try IGLayoutReingoldTilford from the IGraph/M package. $\endgroup$ – Szabolcs Dec 14 '16 at 12:48
  • 4
    $\begingroup$ Realistically, this question is just too big. I upvoted because I would like to see a solution, but to actually get a solution you would have to break the problem down into pieces (as I did in the comment above), and ask about the piece you are having difficulties with. Is it the visualization? Is it the trimming? Is it making a node react to clicks? Is it storing the trimming state? Etc. $\endgroup$ – Szabolcs Dec 14 '16 at 12:52
  • 1
    $\begingroup$ @Yves Klett: the sample dataset for the upper diagram is given in the source code at the bottom of the following site: bl.ocks.org/mbostock/4339083 $\endgroup$ – mrz Dec 14 '16 at 13:13
  • 1
    $\begingroup$ This isn't exactly what you are looking for, but somebody did something (functionally) like this in Wolfram's last year's one-liner competition with OpenerView (last one the page / winning entry) $\endgroup$ – V-J Dec 21 '16 at 5:53
11
+100
$\begingroup$

So I took a slightly different tack from lowriniak, making a tree object, rather than just using a tree hierarchy, but the result is much the same. What's worth noting, though, is that this gives you a somewhat more dynamic tree.

Here's the tree making boiler plate:

$tree = <|Root -> <|"Title" -> ".", "Children" -> {}, 
     "Open" -> True|>|>;
$id = 1;
childNodes[parent : _Integer | Root] := $tree[parent]["Children"];
nodeOpen[node : _Integer | Root] := TrueQ@$tree[node]["Open"];
nodeTitle[node_] := $tree[node]["Title"];
toggleNode[node_] := $tree[node]["Open"] = ! $tree[node]["Open"];
addNode[parent : _Integer | Root : Root, node_] := (
   AssociateTo[$tree, $id -> <|"Title" -> node, "Children" -> {}, 
      "Open" -> True|>];
   AppendTo[$tree[parent]["Children"], $id];
   $id++
   );
removeNode[node_Integer] :=

  KeyDropFrom[$tree, Prepend[childNodes[node], node]];
retitleNode[node_Integer, name_] := $tree[node]["Title"] = name;

Then we'll want a way to collect our visible nodes and assign them the right coordinates:

$viewNodes :=
  Block[{
    processing = {},
    nodeStack = {Root},
    visibleNodes = {{Root}},
    layerNodes = {},
    nodeDepth = 1
    },
   While[Length@nodeStack > 0,
    processing = nodeStack;
    nodeStack = {};
    layerNodes = {};
    Do[
     If[nodeOpen@node,
      AppendTo[layerNodes, childNodes@node];
      nodeStack = Join[nodeStack, childNodes@node]
      ],
     {node, processing}];
    AppendTo[visibleNodes, Flatten@layerNodes]
    ];
   visibleNodes
   ];
$allNodes := Block[{nodeOpen = (True &)}, $viewNodes];
$graphNodes :=

  With[{totalTree = $allNodes, nodes = Flatten@$viewNodes},
   Flatten@
    With[{d = Length@totalTree},
     Table[
      With[{l = Length@totalTree[[i]]},
       Table[
        If[MemberQ[nodes, totalTree[[i, j]]],
         {(i - 1), (j - Floor[l/2])/2.2} -> totalTree[[i, j]],
         Nothing],
        {j, l}]
       ],
      {i, d}
      ]
     ]
   ];

Then we'll make a graph and formatting wrapper:

$viewTree :=
 With[{nodes = $graphNodes},
  If[Length@nodes > 1,
   Graph[
    Flatten@
     Table[
      If[nodeOpen@node, Thread[node -> childNodes@node], {}],
      {node, Last /@ nodes}],
    VertexShape -> Table[
      With[{node = node},
       node ->
        EventHandler[
         Graphics[{
           EdgeForm[Hue[.6, .5, .25]],
           GrayLevel[.95],
           Disk[{0, 0}, 50],
           Black,
           Inset@nodeTitle@node
           }
          ], "MouseClicked" :> (toggleNode@node)]
       ], {node, Last /@ nodes}],
    VertexSize -> .15,
    VertexCoordinates -> Reverse /@ nodes
    ],
   With[{node = Last@First@nodes},
    EventHandler[
     Graphics[{
       EdgeForm[Hue[.6, .5, .25]],
       GrayLevel[.95], ,
       Disk[{0, 0}, 50],
       Black,
       Inset@nodeTitle@node
       }
      ],
     "MouseClicked" :> (toggleNode@node)]
    ]
   ]
  ]

Format[HoldPattern[$TreeObject]] :=
 Interpretation[
  Dynamic[$viewTree, TrackedSymbols :> {$viewTree, $tree}],
  $TreeObject]

And this gives you a dynamically editable tree with toggle-able nodes:

Do[
  With[{i = addNode[n]},
   Do[
    With[{j = addNode[i, m]},
     Do[addNode[j, k], {k, RandomInteger[3]}]
     ],
    {m, RandomInteger[5]}
    ]
   ],
  {n, RandomInteger[10]}
  ];
$TreeObject

tree full

Then toggle some nodes:

tree toggles

It isn't as elegant as your source example, but it works as one would hope.

$\endgroup$
  • $\begingroup$ Wonderful solution ... $\endgroup$ – mrz Dec 22 '16 at 9:59
6
$\begingroup$

I've made a start, but getting the vertices to stay static relative to each other is a challenge...

Set up the values:

hierarchy = {
   1 -> 11, 1 -> 12, 1 -> 13,

   11 -> 111, 11 -> 112, 11 -> 113,
   12 -> 121, 12 -> 122, 12 -> 123,
   13 -> 131, 13 -> 132, 13 -> 133,

   121 -> 1211, 121 -> 1212, 121 -> 1213
};

Make an association to track 'openness':

select = Association[# -> True & /@ Union[Flatten[List @@@ hierarchy]]];

A function to toggle the 'openness' of an object:

toggle[obj_] := select[obj] = ! select[obj];

Make a nested version so you can do it on trees:

togglenested[obj_] := (
  If[obj != 1, toggle[obj]];
  togglenested /@ Cases[
    hierarchy, 
    rule : Rule[left_, right_] /; left == obj :> right
  ];
  Null
)

Creating the vertex primitive with event handler for clicks:

action[obj_, loc_] :=  Inset[EventHandler[obj, {"MouseClicked" :> togglenested[obj]}], loc]

And make a graph that can be interacted with:

Dynamic[
  GraphPlot[
    Cases[hierarchy, rule : Rule[left_, right_] /; select[left]], 
    VertexRenderingFunction -> (action[#2, #] &), 
    Method -> "LayeredDigraphDrawing"
  ]
]

graph

It's not pretty but that's not too hard to add. The current drawbacks: collapsing the first value means the GraphPlot fails so I turned that off. Also the vertices do rearrange themselves - maybe giving specific coordinates to the values in the select association and then drawing it from there could work?

$\endgroup$
  • 1
    $\begingroup$ You are excellent ... after Cases should be a [ ? togglenested[obj_] produces an error ... Syntax::sntxf: "(" cannot be followed by "If[obj!=1,toggle[obj]];togglenested/@Caseshierarchy,rule:Rule[left_,right_]/;left==obj:>right Null)". $\endgroup$ – mrz Dec 21 '16 at 17:13
  • $\begingroup$ Ah sorry about that, I must have deleted it while formatting the code - thanks for fixing it Kuba. $\endgroup$ – lowriniak Dec 22 '16 at 9:07
  • $\begingroup$ Another question since probably you can answer it: mathematica.stackexchange.com/questions/134054/… and the mentioned links. I am really disappointed ... $\endgroup$ – mrz Dec 22 '16 at 13:14
  • $\begingroup$ I took a look and put in what I know, though I am not a developer and don't know the internals of the front end so I might not be so much use. $\endgroup$ – lowriniak Dec 22 '16 at 14:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.