Skip to content

Commit 14be2e6

Browse files
authored
Fixes balancing in tidy tree (#191)
1 parent 811f856 commit 14be2e6

2 files changed

Lines changed: 83 additions & 5 deletions

File tree

src/Hierarchy/Tidy.elm

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,20 @@ type alias YList =
275275
List { index : Int, id : Int, y : Float }
276276

277277

278-
moveSubtree : Int -> Maybe { y : Float, id : Int, index : Int } -> Int -> Float -> TidyLayout a -> TidyLayout a
279-
moveSubtree currentIndex fromMaybe currentId dist lay =
278+
moveSubtree : Array Id -> Int -> Maybe { y : Float, id : Int, index : Int } -> Int -> Float -> TidyLayout a -> TidyLayout a
279+
moveSubtree nodeChildren currentIndex fromMaybe currentId dist lay =
280280
case fromMaybe of
281281
Just from ->
282282
if from.index /= currentIndex - 1 then
283283
let
284284
normDist =
285285
dist / toFloat (currentIndex - from.index)
286+
287+
-- The slack is distributed across the interior subtrees between
288+
-- `from` and `current`, so the positive acceleration is planted on
289+
-- the first interior sibling (children[from.index + 1])
290+
interiorStartId =
291+
Array.get (from.index + 1) nodeChildren |> Maybe.withDefault from.id
286292
in
287293
lay
288294
|> updateTidyData
@@ -298,7 +304,7 @@ moveSubtree currentIndex fromMaybe currentId dist lay =
298304
(\from_ ->
299305
{ from_ | shiftAcceleration = from_.shiftAcceleration + normDist }
300306
)
301-
from.id
307+
interiorStartId
302308

303309
else
304310
lay
@@ -430,7 +436,7 @@ separate peerMargin childIndex nodeId lay_ ylist_ =
430436
if dist > 0 then
431437
-- left and right are too close. move right part with distance of dist
432438
( { rightContour | modifierSum = rightContour.modifierSum + dist }
433-
, moveSubtree childIndex (yList2 |> List.head) (nodeChildren |> Maybe.andThen (Array.get childIndex) |> Maybe.withDefault -1) dist lay
439+
, moveSubtree (nodeChildren |> Maybe.withDefault Array.empty) childIndex (yList2 |> List.head) (nodeChildren |> Maybe.andThen (Array.get childIndex) |> Maybe.withDefault -1) dist lay
434440
)
435441

436442
else
@@ -617,7 +623,7 @@ layout getters tree =
617623
update tail
618624

619625
else
620-
{ index = index, y = maxY, id = id } :: tail
626+
{ index = index, y = maxY, id = id } :: lst
621627
in
622628
{ yList = update yList1, layN = lay3, index = index + 1 }
623629
)

tests/Hierarchy/TidyTests.elm

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,17 @@ suite =
3333
tree
3434
|> doLayout
3535
|> expectNodesToBeOrdered
36+
, Test.fuzz fuzzHierarchy "Rule 7: Laying out a mirrored tree gives the horizontal flip of the original layout" <|
37+
\tree ->
38+
Expect.equalLists (nodeCenters (mirror tree)) (flippedNodeCenters tree)
3639

3740
-- sanity checks
3841
, test1
3942
, test2
4043
, test3
44+
45+
-- regression for https://github.com/gampleman/elm-visualization/issues/190
46+
, test190
4147
]
4248

4349

@@ -169,6 +175,39 @@ doLayout =
169175
Hierarchy.tidy [ Hierarchy.nodeSize (\( _, w, h ) -> ( w, h )), Hierarchy.parentChildMargin 1, Hierarchy.peerMargin 1 ]
170176

171177

178+
{-| Reverse the order of every node's children, top to bottom.
179+
-}
180+
mirror : Tree a -> Tree a
181+
mirror t =
182+
Tree.tree (Tree.label t) (List.reverse (List.map mirror (Tree.children t)))
183+
184+
185+
{-| The sorted multiset of node-center positions. The layout reports left-edge
186+
x (center - width / 2), so we add width / 2 back to recover centers.
187+
-}
188+
nodeCenters : Tree ( Int, Float, Float ) -> List ( Float, Float )
189+
nodeCenters tree =
190+
doLayout tree
191+
|> Tree.toList
192+
|> List.map (\n -> ( round3 (n.x + n.width / 2), round3 n.y ))
193+
|> List.sort
194+
195+
196+
{-| The original layout's node centers, flipped horizontally about x = 0.
197+
-}
198+
flippedNodeCenters : Tree ( Int, Float, Float ) -> List ( Float, Float )
199+
flippedNodeCenters tree =
200+
doLayout tree
201+
|> Tree.toList
202+
|> List.map (\n -> ( round3 -(n.x + n.width / 2), round3 n.y ))
203+
|> List.sort
204+
205+
206+
round3 : Float -> Float
207+
round3 v =
208+
toFloat (round (v * 1000)) / 1000
209+
210+
172211
formatTree : FinishedLayout -> String
173212
formatTree =
174213
let
@@ -210,3 +249,36 @@ test3 =
210249
Tree.tree ( 0, 1, 1 ) [ Tree.tree ( 0, 1, 1 ) [ Tree.tree ( 0, 1, 10 ) [ Tree.singleton ( 0, 1, 1 ), Tree.singleton ( 0, 1, 1 ) ], Tree.tree ( 0, 1, 1 ) [ Tree.singleton ( 0, 1, 1 ) ], Tree.tree ( 0, 1, 10 ) [ Tree.singleton ( 0, 10, 1 ) ] ] ]
211250
|> doLayout
212251
|> expectNoOverlapNodes
252+
253+
254+
255+
{-
256+
See <https://github.com/gampleman/elm-visualization/issues/190>.
257+
-}
258+
259+
260+
test190 : Test
261+
test190 =
262+
Test.test "Issue 190: slack between a node's children is distributed evenly" <|
263+
\() ->
264+
let
265+
leaf =
266+
Tree.singleton ( 0, 1, 1 )
267+
268+
xs =
269+
Tree.tree ( 0, 1, 1 )
270+
[ Tree.tree ( 0, 1, 1 ) [ leaf, leaf, leaf, leaf, leaf, leaf, Tree.tree ( 0, 1, 1 ) [ leaf ] ]
271+
, leaf
272+
, leaf
273+
, Tree.tree ( 0, 1, 1 ) [ leaf ]
274+
, leaf
275+
, Tree.tree ( 0, 1, 1 ) [ Tree.tree ( 0, 1, 1 ) [ leaf, leaf, leaf, leaf, leaf, leaf, leaf ] ]
276+
]
277+
|> doLayout
278+
|> Tree.children
279+
|> List.map (\c -> (Tree.label c).x)
280+
281+
gaps =
282+
List.map2 (\p q -> q - p) xs (List.drop 1 xs)
283+
in
284+
Expect.equalLists (List.map round3 gaps) [ 3.067, 3.067, 3.067, 2.4, 2.4 ]

0 commit comments

Comments
 (0)