NAME

Kubernetes::Comb::SVG::Layout - Places cells in groups and a honeycomb, by dependency depth or packed

VERSION

version 0.001

SYNOPSIS

use Kubernetes::Comb::SVG::Layout;

my $layout = Kubernetes::Comb::SVG::Layout->new(
  cells   => \@cells,
  columns => 6,
  size    => 56
)->layout;

for my $cell ( @{ $layout->{cells} } ) {
  # $cell->{id}, $cell->{x}, $cell->{y}, $cell->{row}, $cell->{column}
}

# packed, for a 16:9 screen: no dependency rows, a compact block
my $packed = Kubernetes::Comb::SVG::Layout->new(
  cells  => \@cells,
  mode   => 'packed',
  aspect => 16 / 9
)->layout;

DESCRIPTION

Places cells in a honeycomb and returns plain data. It knows neither the custom resource nor SVG: a cell is anything answering id, name, group and dependencies, as Kubernetes::Comb::SVG::Cell does.

There are two modes, see "mode". In depth, the default, a cell sits in the row of its dependency depth. In packed, the status monitor, the dependencies play no part in placement: the cells of a group are sorted by id and fill one compact honeycomb, its shape chosen by "columns", else "rows", else "aspect". The rules below on groups, hexagons and the result hold for both; those on depth and rows by name are the depth mode.

  • Groups are stacked top to bottom in name order, the cells without a group last. With more than one group, or one named group, every group has a heading; a lone unnamed group has none.

  • Inside a group a cell sits in the row of its dependency depth: depth 0 without a dependency in the picture, else one below its deepest dependency. Depth is computed over all cells, not per group, so an edge between groups still points the right way. Dependencies on ids that are not among the cells do not count.

  • The cells of a dependency cycle share one depth, one below the deepest dependency outside the cycle. A cell depending on itself counts as no dependency. A cycle, or a long chain, never loops or dies.

  • Rows are sorted by name (then by id), and a depth with more cells than "columns" wraps into further rows. A group therefore has one or more rows for each depth that occurs in it; a depth no cell of the group has is skipped, so the row number is not the depth.

  • Pointy-top hexagons; every second row is shifted by half a step, so rows interlock.

cells

Default empty. ArrayRef of cell objects, each answering id, name, group (a string or undef) and dependencies (the ids it depends on). Of several with one id the first is kept; an object without an id is left out.

columns

Default 6, a positive integer. Cells per row before a row wraps. In the packed "mode" it is the cells per row of every block, and only when it was given to the constructor: the default does not count there, so "rows" and "aspect" can apply.

mode

Default depth: rows by dependency depth, as described above. packed ignores the dependencies for placement: the cells of a group are sorted by id and fill the rows left to right, top to bottom, so a cell keeps its place as long as the set of cells is the same. Each group is its own packed block. The grid comes from "columns" when given, else from "rows", else from "aspect". The depth of each cell and the edges in the result are computed the same way in both modes; in packed the depth is data only and does not decide the row.

rows

Optional, a positive integer; packed only, and only without a given "columns". Every block gets the fewest columns that fit its cells into this many rows, so a block has at most rows rows -- fewer when its cells do not fill them.

aspect

Default 16/9, a positive number; packed only, and only with neither a given "columns" nor "rows". Width divided by height of the area to fill. All blocks get the one column count whose picture -- width by height of "layout", group headings included, plus the "frame_width" and "frame_height" -- comes closest to it; of two equally close the one with fewer columns.

frame_width

Default 0, a number of at least zero. The width the caller will add around the content (padding on both sides), so that "aspect" is met by the whole picture and not by the honeycomb alone. Only counted when choosing the columns by aspect; the result of "layout" never includes it.

frame_height

Default 0, a number of at least zero. The height the caller will add around the content (padding, title, legend); see "frame_width".

size

Default 56, a positive number. Radius of a hexagon, centre to corner, in the units of the result. Every measure below derives from it.

hex_width

Width of one pointy-top hexagon, flat side to flat side: sqrt(3) * size.

hex_height

Height of one hexagon, corner to corner: 2 * size.

gap

Air between the sides of two neighbouring hexagons: size / 7.

step_x

Distance between the centres of two neighbours in a row: "hex_width" plus "gap".

step_y

Distance between the centres of two rows: "step_x" * sqrt(3) / 2, so the gap is the same on all six sides of a hexagon.

heading_height

Room above a group for its heading: 0.6 * size. Only taken when the picture has headings.

heading_baseline

Distance of the heading's baseline from the top of its group: 0.4 * size.

group_gap

Space between the lowest hexagon of a group and the heading band of the next: 0.35 * size.

layout

my $layout = $self->layout;

Places the cells and returns a plain hash. With the defaults (size 56) and two cells in one group, db without dependency and api depending on it:

{
  width  => 149.49,
  height => 236.53,
  groups => [
    { name => 'alpha', heading => { x => 0, y => 22.4 }, y => 0, height => 236.53 }
  ],
  cells => [
    { id => 'lab/db',  name => 'db',  group => 'alpha', depth => 0,
      row => 0, column => 0, x => 48.5,  y => 89.6 },
    { id => 'lab/api', name => 'api', group => 'alpha', depth => 1,
      row => 1, column => 0, x => 100.99, y => 180.53 }
  ],
  edges => [ { from => 'lab/api', to => 'lab/db' } ]
}
  • width, height: the extent of the content, which starts at 0,0. The canvas is the caller's: padding and title are not included.

  • groups: one hash per group, in order. name is the group name, undef for the cells without one. heading is { x, y }, the left end of the baseline of the heading, or undef when the picture has no headings. y and height span the group including its heading band.

  • cells: one hash per cell, group by group and row by row. id, name and group are the cell's; depth is the dependency depth over the whole picture; row and column count inside the group; x and y are the centre of the hexagon.

  • edges: { from, to } by id, from a cell to a cell it depends on, sorted by from then to, without duplicates. Only ids among the cells; a cell depending on itself gives none.

All coordinates are rounded to two decimals, so the result is the same on every platform. No cells give width and height 0 and empty lists.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-kubernetes-comb-svg/issues.

IRC

Join #kubernetes on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.