packages feed

hgg-core-0.1.0.0: src/Graphics/Hgg/Spec/Theme.hs

-- |
-- Module      : Graphics.Hgg.Spec.Theme
-- Description : theme preset (ThemeName) + series palette + element 単位 override
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Phase 55: 'Graphics.Hgg.Spec' の module 分割で切り出し。 描画 theme の名前
-- ('ThemeName')、 preset ごとの series palette、 named palette (Okabe-Ito 等)、
-- element 単位の上書き ('ThemeOverride'、 ggplot theme(element_*) 相当) を持つ。
-- 公開 API は従来どおり 'Graphics.Hgg.Spec' (facade) が re-export する。
-- 挙動・出力は完全に不変。
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE DerivingStrategies        #-}
{-# LANGUAGE DerivingVia               #-}
{-# LANGUAGE OverloadedStrings         #-}
module Graphics.Hgg.Spec.Theme
  ( -- * theme preset + palette
    ThemeName(..)
  , themeSeriesPalette
  , okabeIto, tolBright, brewerSet2, brewerDark2
    -- * element 単位 override
  , ThemeOverride(..)
  ) where

import           Data.Aeson      (FromJSON, ToJSON)
import           Data.Monoid     (Last (..))
import           Data.Text       (Text)
import           GHC.Generics    (Generic, Generically (..))

import           Graphics.Hgg.Spec.Decoration (FontSpec)

-- | 描画 theme (= 名前で参照、 関数を持たない = JSON serializable)。
-- ggplot 標準 preset (ThemeGrey) + ブランドテーマを追加。
--   * ThemeGrey            = ggplot 既定 theme_grey (灰背景 #EBEBEB・白 grid・枠なし・軸線なし)
--   * ThemeNoir     = ブランド (暗・上品・寒色アクセント、 コンペ用に残置)
--   * ThemeLumen    = ブランド (白基調・深い差し色・清潔、 コンペ用に残置)
--   * ThemeParchment     = 羊皮紙基調・明の正式テーマ。 配色は cream/gold/ink +
--       Universal Categorical series 由来。
--   * ThemeParchmentDark = 同テーマの暗版 (焦茶インク背景・series は shade 300 で沈み防止)。
data ThemeName = ThemeDefault | ThemeMinimal | ThemeDark | ThemeLight
               | ThemeGrey | ThemeBW | ThemeClassic | ThemeVoid | ThemeLinedraw
               | ThemeNoir | ThemeLumen
               | ThemeParchment | ThemeParchmentDark
  deriving (Show, Eq, Generic)

instance ToJSON   ThemeName
instance FromJSON ThemeName

-- | preset ごとの既定 series palette (= palette 未指定時に使う色順)。
-- ggplot 系 preset は従来通り hggMain (既定配色)、 ブランド 3 種は専用 series。
-- Layout.computeLayout の catPal 既定がこれを参照する (= palette 指定で上書き可)。
themeSeriesPalette :: ThemeName -> [Text]
themeSeriesPalette t = case t of
  ThemeNoir  -> ["#7AA2F7", "#BB9AF7", "#7DCFFF", "#9ECE6A", "#E0AF68", "#F7768E"]
  ThemeLumen -> ["#4C5BD4", "#D6336C", "#2F9E44", "#E8590C", "#7048E8", "#1098AD"]
  -- Parchment 明: 案3 (#1 = White Rabbit Inner Ear Pink #F0A5A0、
  --   #3 = Dormouse 系 Warm Yellow #E8D58A、 他は既定配色)。
  ThemeParchment     -> canvasPal
  -- 暗版 (Charcoal 背景): 案3 の暗色 (purple/teal/rose/wine) を明度調整し沈み防止。色相・順序は維持。
  ThemeParchmentDark -> [ "#F0A5A0", "#A98BD0", "#E8D58A", "#5FA0A8"
                            , "#E0617E", "#B8C7D9", "#D9685F" ]
  -- default 系 (grey/default/minimal/light/dark) は ggplot2 既定 scales::hue_pal() にならう。
  --   ★Phase 28 (2026-06-14): 固定 7 色版でなく **群数 n 依存の hue sentinel** を返す。
  --   ggplot は離散色スケールごとに hue_pal()(n) を再計算するため、 群数 3 なら
  --   赤/緑/青、 4 なら別配色…と変わる。 固定 7 色だと群数 3 でも index 0,1,2 =
  --   赤/金/緑 になり R4DS と食い違っていた。 sentinel は Layout.catPal /
  --   Bridge.resolveGrouped が 'ggplotHue' n で展開する。
  _ -> ["__ggplot_hue__"]
  where
    canvasPal = [ "#F0A5A0", "#7A5C92", "#E8D58A", "#3E6A6F"
                , "#C7445D", "#B8C7D9", "#7E1F23" ]

-- | 学術向け named series palette。 theme とは独立に `palette <名>` で使う (colorblind-safe 中心)。
-- Okabe-Ito (Okabe & Ito 2008、 色覚バリアフリー定番、 R palette.colors("Okabe-Ito") と同一)。
okabeIto :: [Text]
okabeIto = [ "#000000", "#E69F00", "#56B4E9", "#009E73"
           , "#F0E442", "#0072B2", "#D55E00", "#CC79A7" ]

-- | Paul Tol bright (7 色、 色覚バリアフリー)。
tolBright :: [Text]
tolBright = [ "#4477AA", "#EE6677", "#228833", "#CCBB44"
            , "#66CCEE", "#AA3377", "#BBBBBB" ]

-- | ColorBrewer Set2 (8 色、 柔らかい定性)。
brewerSet2 :: [Text]
brewerSet2 = [ "#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3"
             , "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3" ]

-- | ColorBrewer Dark2 (8 色、 濃いめ定性、 白背景向き)。
brewerDark2 :: [Text]
brewerDark2 = [ "#1B9E77", "#D95F02", "#7570B3", "#E7298A"
              , "#66A61E", "#E6AB02", "#A6761D", "#666666" ]

-- ===========================================================================
-- Phase 9 A-2: element 単位 theme override (ggplot theme(element_*) 相当)
-- ===========================================================================
-- | preset (ThemeName) に要素単位で上書きを合成する override。 各 field は Last で
-- 「指定があれば優先」。 'resolveTheme' (Render) が preset palette に合成する。
-- 全 field Monoid なので setter を `<>` で重ねられる (ggplot の theme() 加算と同様)。
data ThemeOverride = ThemeOverride
  { toPlotBg       :: !(Last Text)   -- plot.background fill
  , toPanelBg      :: !(Last Text)   -- panel.background fill
  , toShowPanel    :: !(Last Bool)   -- panel 矩形を塗るか
  , toGridColor    :: !(Last Text)   -- panel.grid colour
  , toShowGrid     :: !(Last Bool)   -- panel.grid on/off
  , toShowBorder   :: !(Last Bool)   -- panel.border on/off
  , toShowAxisLine :: !(Last Bool)   -- axis.line on/off
  , toAxisColor    :: !(Last Text)   -- axis 線/目盛り色
  , toTextColor    :: !(Last Text)   -- 文字色
    -- ★ Phase 9 A-3: 文字 theme 統合 (ggplot theme(text/plot.title/axis.title/...) 相当)。
    --   各 slot の FontSpec を theme から差し替え可能に。 優先順位は
    --   override (これ) > font setter (vsTitleFont 等) > preset 既定 ('mkFontTS')。
  , toTitleFont     :: !(Last FontSpec)  -- plot.title
  , toAxisLabelFont :: !(Last FontSpec)  -- axis.title
  , toTickFont      :: !(Last FontSpec)  -- axis.text
  , toLegendFont    :: !(Last FontSpec)  -- legend.title / legend.text
    -- ★ axis.text の回転角 (度・CCW)。 per-axis 'axisRotate' 未指定時の fallback。
    --   'toAxisTextAngle' = x/y 共通既定、 'toAxisTextAngleX'/'toAxisTextAngleY' = 軸別上書き
    --   (Phase 50 A3・軸別 > 共通 の優先。 'axisTextAngleXOf'/'axisTextAngleYOf' で解決)。
  , toAxisTextAngle  :: !(Last Double)
  , toAxisTextAngleX :: !(Last Double)
  , toAxisTextAngleY :: !(Last Double)
    -- ★ Phase 9 A-4: strip.background (facet strip の灰矩形)。
  , toStripBg       :: !(Last Text)   -- strip.background fill
  , toShowStrip     :: !(Last Bool)   -- strip 矩形を塗るか
    -- ★ Phase 43 A4: プリセット専用だった 4 項目に上書き口を追加 (= 全プロパティ `<>` 上書き
    --   可能に)。対応 'ThemePalette' field = tpTitleHjust / tpTitleColor / tpTickLineColor /
    --   tpLegendKeyBg。generic 導出なので field 追加のみで instance は自動追従。
  , toTitleHjust    :: !(Last Double) -- plot.title の水平揃え (0=左、 0.5=中央)
  , toTitleColor    :: !(Last Text)   -- plot.title / axis.title の文字色
  , toTickLineColor :: !(Last Text)   -- 軸目盛線 (tick mark) の色
  , toLegendKeyBg   :: !(Last Text)   -- legend.key 背景塗り色 ("" なら塗らない)
  } deriving stock (Generic, Show, Eq)
    -- ★ Phase 43 A3: 全 field が `Last` の素直な per-field 合成なので generic 導出。
    --   位置依存の手書き instance (旧 `a1..p1` を数で揃える形) を撲滅し、 以後の field
    --   追加 (A4) を「field を足すだけ」で安全にする。挙動は旧手書きと完全同型。
    deriving (Semigroup, Monoid) via Generically ThemeOverride

instance ToJSON   ThemeOverride
instance FromJSON ThemeOverride