packages feed

hgg-core-0.2.0.0: src/Graphics/Hgg/Layout/Grid.hs

-- |
-- Module      : Graphics.Hgg.Layout.Grid
-- Description : Flattens nested subplots / <-> / <:> into a single unified grid
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- [日本語]: subplots / @<->@ / @<:>@ のネストを __単一の統一グリッド__へ
--   平坦化する純関数。 patchwork 流 gtable 配置の前段で、 任意の深さの
--   入れ子を各 leaf パネルに @(rowStart, rowSpan, colStart, colSpan)@ を割り当てた
--   フラットなグリッドへ落とす。 描画 (Render/Layer) はこのグリッド 1 枚に対して
--   「列ごと左右帯・行ごと上下帯」 を 1 回確保するだけになり、 ネスト境界をまたいだ
--   パネル本体の整列が保証される。
--
--   ★方針 (計画書 §設計): ツリーの寸法を整数グリッド単位で再帰計算する。
--     * leaf            : @w=1, h=1@
--     * hbox (横並び)   : @w=Σ child.w@, @h=max child.h@。 各 child は自分の幅 ×
--                         グループ高 (行) を span (縦を揃える)。
--     * vbox (縦並び)   : @h=Σ child.h@, @w=max child.w@。 各 child はグループ幅 (列) ×
--                         自分の高さを span (横を揃える)。
--   leaf を span 方向 (hbox なら縦・vbox なら横) いっぱいに伸ばすことで、
--   @(a<->b<->c)<:>d@ の @d@ が上段 3 列を colSpan=3 で全幅 span し、 上段左端と
--   下段左端が col0 で一致する。
-- [English]: A pure function that flattens nested subplots / @<->@ / @<:>@
--   into __a single unified grid__. As a step before patchwork-style gtable
--   placement, it reduces arbitrarily deep nesting into a flat grid, giving
--   each leaf panel a @(rowStart, rowSpan, colStart, colSpan)@. Rendering
--   (Render/Layer) then only needs to reserve "left/right bands per column,
--   top/bottom bands per row" once for this single grid, which guarantees
--   panel-body alignment across nesting boundaries.
--
--   Approach (see the design section of the plan): recursively computes
--   tree dimensions in integer grid units.
--     * leaf: @w=1, h=1@.
--     * hbox (side-by-side): @w=Σ child.w@, @h=max child.h@. Each child spans
--       its own width × the group height (row), keeping rows aligned.
--     * vbox (stacked): @h=Σ child.h@, @w=max child.w@. Each child spans the
--       group width (column) × its own height, keeping columns aligned.
--   Stretching a leaf to fill the span direction (vertical for hbox,
--   horizontal for vbox) means @d@ in @(a<->b<->c)<:>d@ spans the top row's
--   3 columns at colSpan=3, so the top row's left edge lines up with the
--   bottom row's left edge at col0.
{-# LANGUAGE BangPatterns #-}

module Graphics.Hgg.Layout.Grid
  ( GridCell(..)
  , GridPlacement(..)
  , PTree(..)
  , toPTree
  , gridDims
  , flattenSubplots
  ) where

import           Graphics.Hgg.Spec (VisualSpec, selectedSubplots, vsSubplotCols)
import           Data.List         (mapAccumL)
import           Data.Monoid       (Last (..))

-- ===========================================================================
-- 型
-- ===========================================================================

-- | [日本語]: 統一グリッド上の 1 パネルの占有矩形 (整数セル単位)。
--   [English]: The rectangle a single panel occupies on the unified grid (in
--   integer cell units).
data GridCell = GridCell
  { gcRow     :: !Int  -- ^ [日本語]: 開始行 (0 始まり)。 [English]: The starting row (0-based).
  , gcRowSpan :: !Int  -- ^ [日本語]: またぐ行数 (>= 1)。 [English]: The number of rows spanned (>= 1).
  , gcCol     :: !Int  -- ^ [日本語]: 開始列 (0 始まり)。 [English]: The starting column (0-based).
  , gcColSpan :: !Int  -- ^ [日本語]: またぐ列数 (>= 1)。 [English]: The number of columns spanned (>= 1).
  } deriving (Eq, Show)

-- | [日本語]: 平坦化結果。 グリッド総寸法 + leaf パネルとその占有セル。
--   [English]: The flattening result: overall grid dimensions plus each leaf
--   panel and its occupied cell.
data GridPlacement = GridPlacement
  { gpCols   :: !Int                        -- ^ [日本語]: 統一グリッドの総列数。 [English]: The total column count of the unified grid.
  , gpRows   :: !Int                        -- ^ [日本語]: 統一グリッドの総行数。 [English]: The total row count of the unified grid.
  , gpPanels :: ![(VisualSpec, GridCell)]   -- ^ [日本語]: leaf パネル (描画対象) とセル。 [English]: The leaf panels (render targets) with their cells.
  }

-- | [日本語]: subplots ツリーの中間表現。 @<->@ は 'PH'、 @<:>@ は 'PV'、 単一
--   プロットは 'PLeaf'。 汎用 subplots (cols が 1 でも要素数でもない wrap grid)
--   は @PV [PH ...]@ へ正規化する。
--   [English]: An intermediate representation of the subplots tree. @<->@ is
--   'PH', @<:>@ is 'PV', and a single plot is 'PLeaf'. A general subplots
--   layout (a wrap grid whose cols is neither 1 nor the element count) is
--   normalized to @PV [PH ...]@.
data PTree
  = PLeaf VisualSpec
  | PH    [PTree]   -- ^ [日本語]: 横並び (hconcat / @<->@)。 [English]: Side-by-side (hconcat / @<->@).
  | PV    [PTree]   -- ^ [日本語]: 縦並び (vconcat / @<:>@)。 [English]: Stacked (vconcat / @<:>@).

-- ===========================================================================
-- VisualSpec → PTree
-- ===========================================================================

-- | [日本語]: subplots ネストを 'PTree' へ。 cols でグループ方向を判定:
--   @cols<=1@ → 縦・@cols>=n@ → 横・それ以外 → cols 列の wrap grid (=縦に並ぶ横行)。
--   既定 cols は @renderSubplots@ と同じ @min n 3@ (parity 維持)。
--   [English]: Converts subplots nesting to a 'PTree'. cols determines the
--   grouping direction: @cols<=1@ means stacked; @cols>=n@ means
--   side-by-side; otherwise it is a wrap grid of cols columns (rows of
--   side-by-side panels, stacked). The default cols matches
--   @renderSubplots@, @min n 3@ (preserving parity).
toPTree :: VisualSpec -> PTree
toPTree s =
  case selectedSubplots s of
    []   -> PLeaf s
    subs ->
      let n    = length subs
          cols = maybe (min n 3) id (getLast (vsSubplotCols s))
          kids = map toPTree subs
      in if cols <= 1   then PV kids
         else if cols >= n then PH kids
         else PV [ PH chunk | chunk <- chunksOf cols kids ]

-- | [日本語]: リストを長さ n ずつに分割 (最後は端数)。
--   [English]: Splits a list into chunks of length n (the last chunk may be
--   shorter).
chunksOf :: Int -> [a] -> [[a]]
chunksOf n xs
  | n <= 0    = [xs]
  | null xs   = []
  | otherwise = let (a, b) = splitAt n xs in a : chunksOf n b

-- ===========================================================================
-- 寸法 (整数グリッド単位) と配置
-- ===========================================================================

-- | [日本語]: サブツリーのグリッド寸法 @(cols, rows)@。
--   [English]: The grid dimensions of a subtree, @(cols, rows)@.
gridDims :: PTree -> (Int, Int)
gridDims (PLeaf _) = (1, 1)
gridDims (PH ts)   = ( sum     (map (fst . gridDims) ts)
                     , maximum (1 : map (snd . gridDims) ts) )
gridDims (PV ts)   = ( maximum (1 : map (fst . gridDims) ts)
                     , sum     (map (snd . gridDims) ts) )

-- | [日本語]: @placeT availRows availCols row0 col0 tree@:
--   左上 @(row0,col0)@ から @availRows × availCols@ の領域にツリーを配置し、
--   leaf パネルとその占有セルを返す。 leaf は与えられた領域いっぱいを span する。
--   [English]: @placeT availRows availCols row0 col0 tree@: places the tree
--   in the @availRows × availCols@ region starting at the top-left
--   @(row0,col0)@, and returns the leaf panels with their occupied cells.
--   Each leaf spans the entire region given to it.
placeT :: Int -> Int -> Int -> Int -> PTree -> [(VisualSpec, GridCell)]
placeT !ar !ac !r0 !c0 (PLeaf s) =
  [(s, GridCell r0 ar c0 ac)]
placeT !ar _   !r0 !c0 (PH ts) =
  -- [日本語]: 各 child は自分の幅 × グループ高 (= ar 行) を span。 列を順に消費。
  -- [English]: Each child spans its own width × the group height (ar rows); columns are consumed in order.
  concat . snd $ mapAccumL
    (\cAcc t -> let w = fst (gridDims t)
                in (cAcc + w, placeT ar w r0 cAcc t))
    c0 ts
placeT _   !ac !r0 !c0 (PV ts) =
  -- [日本語]: 各 child はグループ幅 (= ac 列) × 自分の高さ を span。 行を順に消費。
  -- [English]: Each child spans the group width (ac columns) × its own height; rows are consumed in order.
  concat . snd $ mapAccumL
    (\rAcc t -> let h = snd (gridDims t)
                in (rAcc + h, placeT h ac rAcc c0 t))
    r0 ts

-- | [日本語]: VisualSpec の subplots ツリーを統一グリッドへ平坦化する。
--   [English]: Flattens a VisualSpec's subplots tree into a unified grid.
flattenSubplots :: VisualSpec -> GridPlacement
flattenSubplots s =
  let t            = toPTree s
      (cols, rows) = gridDims t
  in GridPlacement cols rows (placeT rows cols 0 0 t)