@@ -607,12 +607,23 @@ fn layout_positions<'a>(
607607 . max ( 1 ) ;
608608 let center_x = ( chart. canvas . pixel_width ( ) / 2 ) as isize ;
609609 let center_y = ( chart. canvas . pixel_height ( ) / 2 ) as isize ;
610- let min_dimension = chart. canvas . pixel_width ( ) . min ( chart. canvas . pixel_height ( ) ) as f64 ;
610+ let px_w = chart. canvas . pixel_width ( ) as f64 ;
611+ let px_h = chart. canvas . pixel_height ( ) as f64 ;
612+ let min_dimension = px_w. min ( px_h) ;
611613 let margin = ( min_dimension * 0.12 ) . clamp ( 4.0 , 12.0 ) ;
612- let max_radius = ( chart. canvas . pixel_width ( ) . min ( chart. canvas . pixel_height ( ) ) as f64 / 2.0
613- - margin)
614- . max ( 8.0 ) ;
615- let radius_step = max_radius / ( max_depth as f64 + 0.35 ) ;
614+ let base_radius = ( min_dimension / 2.0 - margin) . max ( 8.0 ) ;
615+ let radius_step = base_radius / ( max_depth as f64 + 0.35 ) ;
616+
617+ // Use an oval layout when there is meaningfully more horizontal space than
618+ // vertical space, stretching the x-axis radius up to the available width.
619+ let aspect = px_w / px_h. max ( 1.0 ) ;
620+ let ( x_radius_step, y_radius_step) = if aspect > 1.15 {
621+ let max_x_radius = ( px_w / 2.0 - margin) . max ( 8.0 ) ;
622+ let x_step = max_x_radius / ( max_depth as f64 + 0.35 ) ;
623+ ( x_step, radius_step)
624+ } else {
625+ ( radius_step, radius_step)
626+ } ;
616627
617628 let mut weights = HashMap :: new ( ) ;
618629 let mut positions = HashMap :: new ( ) ;
@@ -632,7 +643,8 @@ fn layout_positions<'a>(
632643 TAU ,
633644 center_x,
634645 center_y,
635- radius_step,
646+ x_radius_step,
647+ y_radius_step,
636648 & child_map,
637649 & mut weights,
638650 & mut positions,
@@ -648,7 +660,8 @@ fn assign_child_positions<'a>(
648660 angle_span : f64 ,
649661 center_x : isize ,
650662 center_y : isize ,
651- radius_step : f64 ,
663+ x_radius_step : f64 ,
664+ y_radius_step : f64 ,
652665 child_map : & HashMap < String , Vec < & ' a KnowledgeGraphViewNode > > ,
653666 weights : & mut HashMap < String , usize > ,
654667 positions : & mut HashMap < String , PositionedNode < ' a > > ,
@@ -674,9 +687,9 @@ fn assign_child_positions<'a>(
674687 angle_span * weight as f64 / total_weight as f64
675688 } ;
676689 let angle = cursor + child_span / 2.0 ;
677- let radius = radius_step * child. depth as f64 ;
678- let x = center_x as f64 + radius * angle. cos ( ) ;
679- let y = center_y as f64 + radius * angle. sin ( ) ;
690+ let depth = child. depth as f64 ;
691+ let x = center_x as f64 + x_radius_step * depth * angle. cos ( ) ;
692+ let y = center_y as f64 + y_radius_step * depth * angle. sin ( ) ;
680693
681694 positions. insert (
682695 child. id . clone ( ) ,
@@ -699,7 +712,8 @@ fn assign_child_positions<'a>(
699712 child_sector,
700713 center_x,
701714 center_y,
702- radius_step,
715+ x_radius_step,
716+ y_radius_step,
703717 child_map,
704718 weights,
705719 positions,
@@ -738,23 +752,30 @@ fn draw_orbits(
738752) {
739753 let center_x = ( chart. canvas . pixel_width ( ) / 2 ) as isize ;
740754 let center_y = ( chart. canvas . pixel_height ( ) / 2 ) as isize ;
741- let mut depth_radii: BTreeMap < usize , Vec < isize > > = projection
755+
756+ // Accumulate (|dx|, |dy|) per depth so we can draw an ellipse that fits
757+ // the actual node layout, which may be oval when horizontal space allows.
758+ let mut depth_axes: BTreeMap < usize , Vec < ( isize , isize ) > > = projection
742759 . nodes
743760 . iter ( )
744761 . filter ( |node| node. kind != KnowledgeGraphNodeKind :: World && node. depth > 0 )
745762 . filter_map ( |node| positions. get ( & node. id ) )
746763 . fold ( BTreeMap :: new ( ) , |mut acc, position| {
747- let dx = position. x - center_x;
748- let dy = position. y - center_y;
749- let radius = ( ( ( dx * dx + dy * dy) as f64 ) . sqrt ( ) . round ( ) as isize ) . max ( 1 ) ;
750- acc. entry ( position. node . depth ) . or_default ( ) . push ( radius) ;
764+ let dx = ( position. x - center_x) . abs ( ) ;
765+ let dy = ( position. y - center_y) . abs ( ) ;
766+ acc. entry ( position. node . depth ) . or_default ( ) . push ( ( dx, dy) ) ;
751767 acc
752768 } ) ;
753769
754- for radii in depth_radii. values_mut ( ) {
755- let total: isize = radii. iter ( ) . copied ( ) . sum ( ) ;
756- let radius = ( total as f64 / radii. len ( ) as f64 ) . round ( ) as isize ;
757- draw_circle_screen ( chart, center_x, center_y, radius, Some ( Color :: BrightBlack ) ) ;
770+ for axes in depth_axes. values_mut ( ) {
771+ let count = axes. len ( ) as f64 ;
772+ let rx = ( axes. iter ( ) . map ( |( dx, _) | * dx) . sum :: < isize > ( ) as f64 / count) . round ( ) as isize ;
773+ let ry = ( axes. iter ( ) . map ( |( _, dy) | * dy) . sum :: < isize > ( ) as f64 / count) . round ( ) as isize ;
774+ if rx == ry {
775+ draw_circle_screen ( chart, center_x, center_y, rx, Some ( Color :: BrightBlack ) ) ;
776+ } else {
777+ draw_ellipse_screen ( chart, center_x, center_y, rx, ry, Some ( Color :: BrightBlack ) ) ;
778+ }
758779 }
759780}
760781
@@ -963,12 +984,17 @@ fn draw_labels(
963984 continue ;
964985 }
965986
966- let label = if matches ! (
967- positioned. node. kind,
968- KnowledgeGraphNodeKind :: Mission
969- | KnowledgeGraphNodeKind :: Watch
970- | KnowledgeGraphNodeKind :: Heartbeat
971- ) {
987+ let is_focused = projection
988+ . focus
989+ . as_ref ( )
990+ . is_some_and ( |focus| focus. id == positioned. node . id ) ;
991+ let label = if is_focused
992+ || matches ! (
993+ positioned. node. kind,
994+ KnowledgeGraphNodeKind :: Mission
995+ | KnowledgeGraphNodeKind :: Watch
996+ | KnowledgeGraphNodeKind :: Heartbeat
997+ ) {
972998 positioned. node . title . clone ( )
973999 } else {
9741000 depth_label_text (
@@ -1126,6 +1152,37 @@ fn draw_circle_screen(
11261152 }
11271153}
11281154
1155+ fn draw_ellipse_screen (
1156+ chart : & mut ChartContext ,
1157+ center_x : isize ,
1158+ center_y : isize ,
1159+ rx : isize ,
1160+ ry : isize ,
1161+ color : Option < Color > ,
1162+ ) {
1163+ if rx <= 0 || ry <= 0 {
1164+ chart
1165+ . canvas
1166+ . set_pixel_screen ( center_x. max ( 0 ) as usize , center_y. max ( 0 ) as usize , color) ;
1167+ return ;
1168+ }
1169+
1170+ let segments = ( rx. max ( ry) * 8 ) . clamp ( 24 , 144 ) as usize ;
1171+ let mut previous = None ;
1172+ for step in 0 ..=segments {
1173+ let angle = TAU * step as f64 / segments as f64 ;
1174+ let x = center_x as f64 + rx as f64 * angle. cos ( ) ;
1175+ let y = center_y as f64 + ry as f64 * angle. sin ( ) ;
1176+ let current = ( x. round ( ) as isize , y. round ( ) as isize ) ;
1177+ if let Some ( ( prev_x, prev_y) ) = previous {
1178+ chart
1179+ . canvas
1180+ . line_screen ( prev_x, prev_y, current. 0 , current. 1 , color) ;
1181+ }
1182+ previous = Some ( current) ;
1183+ }
1184+ }
1185+
11291186fn draw_circle_filled_screen (
11301187 chart : & mut ChartContext ,
11311188 center_x : isize ,
0 commit comments