packages feed

hgg-core-0.2.0.0: src/Graphics/Hgg/Spec/Column.hs

-- |
-- Module      : Graphics.Hgg.Spec.Column
-- Description : Column references (ColRef/Resolver), inline conversion, and Point2 — a Spec leaf
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- [日本語]: 'Graphics.Hgg.Spec' (3420 行) の module 分割で切り出した leaf。
-- 列参照の 3 variant ('ColByName' / 'ColNum' / 'ColTxt') と render 時解決
-- ('Resolver')、 inline 列変換 ('Numeric' / 'Categorical')、 2D 点 ('Point2') を
-- 持つ。 Spec 内の他 module に依存しない最下層。 公開 API は従来どおり
-- 'Graphics.Hgg.Spec' (facade) が re-export する。 挙動・出力は完全に不変。
--
-- [English]: A leaf split out of 'Graphics.Hgg.Spec' (3420 lines) during
-- its module split. Carries the 3 column-reference variants ('ColByName' /
-- 'ColNum' / 'ColTxt'), their render-time resolution ('Resolver'), inline
-- column conversion ('Numeric' / 'Categorical'), and the 2D point
-- ('Point2'). The bottom layer, depending on no other module within Spec.
-- The public API is still re-exported by the 'Graphics.Hgg.Spec' facade as
-- before; behavior and output are completely unchanged.
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE OverloadedStrings         #-}
module Graphics.Hgg.Spec.Column
  ( -- * ColRef + Resolver
    ColRef(..)
  , ColData(..)
  , Resolver
  , emptyResolver
  , resolveCol
  , resolveNum
  , resolveTxt
  , colRefName
    -- * Inline column conversion
  , Numeric(..)
  , Categorical(..)
  , inline
  , inlineCat
    -- * Point2
  , Point2(..)
  ) where

import           Data.Aeson      (FromJSON, ToJSON)
import           Data.String     (IsString (..))
import           Data.Text       (Text)
import qualified Data.Text       as T
import           Data.Vector     (Vector)
import qualified Data.Vector     as V
import           GHC.Generics    (Generic)

-- ===========================================================================
-- ColRef + Resolver
-- ===========================================================================

-- | [日本語]: データ列の参照方法。 3 つの variant:
--
--     * 'ColByName' ─ 文字列 col 名。 'Resolver' で実 Vector に解決される。
--     * 'ColNum'    ─ 数値 Vector を inline (= 即値、 resolver 不要)
--     * 'ColTxt'    ─ 文字列 Vector を inline (= categorical encoding 用)
--
--   @OverloadedStrings@ で `"weight" :: ColRef` が `ColByName "weight"` に。
--   [English]: How a data column is referenced. Three variants:
--
--     * 'ColByName' — a string column name, resolved to an actual Vector
--       by 'Resolver'.
--     * 'ColNum'    — an inline numeric Vector (an immediate value, no
--       resolver needed)
--     * 'ColTxt'    — an inline text Vector (for categorical encoding)
--
--   With @OverloadedStrings@, `"weight" :: ColRef` becomes `ColByName
--   "weight"` automatically.
data ColRef
  = ColByName !Text
  | ColNum    !(Vector Double)
  | ColTxt    !(Vector Text)
  deriving (Generic, Show, Eq)

instance ToJSON   ColRef
instance FromJSON ColRef

instance IsString ColRef where
  fromString = ColByName . T.pack

-- | [日本語]: Resolver が返すデータ形 (= 数値 or 文字列)。
--   [English]: The data shape returned by a Resolver (numeric or text).
data ColData
  = NumData !(Vector Double)
  | TxtData !(Vector Text)
  deriving (Show, Eq)

-- | [日本語]: render 時に col 名を Vector に解決する callback。
--   数値列 / 文字列列 どちらも返せるよう 'ColData' で union。
--   [English]: A callback that resolves a column name to a Vector at
--   render time. Unions over 'ColData' so it can return either a numeric
--   or a text column.
type Resolver = Text -> Maybe ColData

emptyResolver :: Resolver
emptyResolver _ = Nothing

-- | [日本語]: 'ColRef' を 'ColData' に解決。 inline は variant に応じて直接返す。
--   [English]: Resolves a 'ColRef' to 'ColData'. Inline variants are
--   returned directly according to their variant.
resolveCol :: Resolver -> ColRef -> Maybe ColData
resolveCol r (ColByName n) = r n
resolveCol _ (ColNum v)    = Just (NumData v)
resolveCol _ (ColTxt v)    = Just (TxtData v)

-- | [日本語]: 数値解決 (= 数値列 or 数値 inline のみ成功、 文字列は 'Nothing')。
--   [English]: Numeric resolution (succeeds only for a numeric column or
--   numeric inline data; text yields 'Nothing').
resolveNum :: Resolver -> ColRef -> Maybe (Vector Double)
resolveNum r cr = case resolveCol r cr of
  Just (NumData v) -> Just v
  _                -> Nothing

-- | [日本語]: 文字列解決 (= 文字列 inline or 文字列列のみ成功)。
--   [English]: Text resolution (succeeds only for text inline data or a
--   text column).
resolveTxt :: Resolver -> ColRef -> Maybe (Vector Text)
resolveTxt r cr = case resolveCol r cr of
  Just (TxtData v) -> Just v
  _                -> Nothing

-- | [日本語]: ColRef の表示名 (= hover tooltip / legend 等)。
--   [English]: The display name of a 'ColRef' (for hover tooltips, the
--   legend, etc.).
colRefName :: ColRef -> Text
colRefName (ColByName n) = n
colRefName (ColNum _)    = "<inline-num>"
colRefName (ColTxt _)    = "<inline-txt>"

-- ===========================================================================
-- Inline column conversion
-- ===========================================================================

-- | [日本語]: 数値系 (Vector n / [n]、 n は Real instance を持つ任意型) を
--   'ColRef' に。
--   [English]: Converts numeric data (Vector n / [n], where n is any type
--   with a Real instance) into a 'ColRef'.
class Numeric a where
  toNumVec :: a -> Vector Double

instance Real n => Numeric (Vector n) where
  toNumVec = V.map realToFrac

instance Real n => Numeric [n] where
  toNumVec = V.fromList . map realToFrac

-- | [日本語]: 文字列系 (= categorical encoding 用)。
--   [English]: Text data (for categorical encoding).
class Categorical a where
  toTxtVec :: a -> Vector Text

instance Categorical (Vector Text) where toTxtVec = id
instance Categorical [Text]        where toTxtVec = V.fromList
instance Categorical [String]      where toTxtVec = V.fromList . map T.pack

-- | [日本語]: 数値 (Vector / List) を inline 'ColRef' に。 'Int' / 'Double' /
--   'Float' / 'Integer' / 'Word' 等 'Real' instance を持つ任意型に対応。
--   [English]: Converts numeric data (a Vector or list) into an inline
--   'ColRef'. Works for any type with a 'Real' instance, such as 'Int',
--   'Double', 'Float', 'Integer', or 'Word'.
--
-- > scatter (inline xs) (inline ys)
-- > scatter (inline [1, 2, 3]) (inline [4.0, 5.0, 6.0])
inline :: Numeric a => a -> ColRef
inline = ColNum . toNumVec

-- | [日本語]: 文字列系を inline 'ColRef' に (= categorical encoding 用)。
--   [English]: Converts text data into an inline 'ColRef' (for categorical
--   encoding).
--
-- > colorBy (inlineCat ["red", "blue", "green"])
inlineCat :: Categorical a => a -> ColRef
inlineCat = ColTxt . toTxtVec

-- ===========================================================================
-- Point2 (= 2D 点・3D 'Point3' と対称)
-- ===========================================================================

-- | [日本語]: 2D 点 (= world space)。 @Graphics.Hgg.ThreeD.Types.Point3@ と対称の
--   直積型。 inline の点単位 API (@scatterPoints@ / @linePoints@) で使う。
--   [English]: A 2D point (in world space); a product type symmetric to
--   @Graphics.Hgg.ThreeD.Types.Point3@. Used by the inline point-wise API
--   (@scatterPoints@ / @linePoints@).
--
--   [日本語]: JSON: positional field は array @[x, y]@ になる (= aeson の
--   Generic 既定挙動・@Point3@ と同形式)。 ※ @Graphics.Hgg.Render@ の @Point@
--   は screen 空間の別物。
--   [English]: JSON: positional fields become the array @[x, y]@ (the
--   default aeson Generic behavior, in the same shape as @Point3@). Note:
--   the @Point@ type in @Graphics.Hgg.Render@ is a distinct, screen-space
--   type.
data Point2 = Point2 !Double !Double
  deriving (Show, Eq, Generic)
instance ToJSON   Point2
instance FromJSON Point2