packages feed

hgg-frame (empty) → 0.1.0.0

raw patch · 5 files changed

+318/−0 lines, 5 filesdep +basedep +containersdep +hgg-core

Dependencies added: base, containers, hgg-core, hgg-frame, hspec, text, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# Changelog for `hgg-frame`++## 0.1.0.0 — 2026-07-18++First public release on Hackage.++- DataFrame abstraction: `class PlotData` and the `df |>> spec` binding.
+ LICENSE view
@@ -0,0 +1,30 @@+BSD 3-Clause License++Copyright (c) 2026, Toshiaki Honda+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+   this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from this+   software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ hgg-frame.cabal view
@@ -0,0 +1,56 @@+cabal-version:      3.0+name:               hgg-frame+version:            0.1.0.0+extra-doc-files:    CHANGELOG.md+synopsis:           DataFrame abstraction (class PlotData) and the df |>> spec binding for hgg+description:+  A dataframe-independent abstraction for writing hgg plots as+  "dataframe + column names".+  .+  * 'PlotData' — a typeclass that bridges any dataframe type to a+    'Resolver' (column name to ColData). Zero-dependency instances for+    'Map' and assoc-lists are included, so it works without any dataframe+    library.+  * '(|>>)' — binds a dataframe to a spec, producing a 'BoundPlot'.+    (The Hackage @dataframe@ package already uses @|>@, hence @|>>@.)+  .+  Rendering wrappers (saveSVGBound etc.) live in the backend packages+  (hgg-svg etc.), so this package depends only on hgg-core.+license:            BSD-3-Clause+homepage:           https://github.com/frenzieddoll/hgg+license-file:       LICENSE+author:             Toshiaki Honda+maintainer:         frenzieddoll@gmail.com+copyright:          2026 Aelysce Project (Toshiaki Honda)+category:           Graphics+build-type:         Simple++common warnings+  ghc-options:        -Wall -Wcompat -Widentities -Wincomplete-record-updates+                      -Wincomplete-uni-patterns -Wpartial-fields+                      -Wredundant-constraints++library+  import:           warnings+  exposed-modules:  Graphics.Hgg.Frame+  hs-source-dirs:   src+  build-depends:    base       >= 4.17 && < 5+                  , vector     >= 0.13 && < 0.14+                  , text       >= 2.0  && < 2.2+                  , containers >= 0.6  && < 0.8+                  , hgg-core ^>= 0.1+  default-language: Haskell2010++test-suite hgg-frame-tests+  import:           warnings+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  hs-source-dirs:   test+  build-depends:    base+                  , vector+                  , text+                  , containers+                  , hgg-core+                  , hgg-frame+                  , hspec      >= 2.10 && < 2.12+  default-language: Haskell2010
+ src/Graphics/Hgg/Frame.hs view
@@ -0,0 +1,145 @@+-- |+-- Module      : Graphics.Hgg.Frame+-- Description : DataFrame 抽象 (class PlotData) ─ 列名で図を書くための df 非依存橋+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- ggplot2 のように「データフレーム + 列名」 で図を書くための抽象。 Haskell に+-- 統一 df ライブラリが無い事情に対応し、 **df 型に依存しない** typeclass+-- 'PlotData' で「列名 → 実ベクタ」 (= 既存の 'Resolver') を取り出す。+--+-- @+-- import           Graphics.Hgg.Easy   (scatter, layer)+-- import           Graphics.Hgg.Frame+-- import qualified Data.Map.Strict as M+--+-- df = M.fromList [(\"x\", inline [1,2,3]), (\"y\", inline [4,5,6])]+-- -- df |>> layer (scatter \"x\" \"y\")   -- バインドは A3 ((|>>)) で+-- @+--+-- 本 module はゼロ依存 instance (assoc-list / 'Data.Map.Map') のみを持つ。+-- Hackage @dataframe@ 等の外部 df 型の instance は各橋 package が所有する+-- (orphan 回避、 proposal spec-2 §3.1)。+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+module Graphics.Hgg.Frame+  ( -- * df 抽象+    PlotData (..)+    -- * バインド (df → spec)+  , BoundPlot (..)+  , (|>>)+  , BindableSpec (..)+  , emptyDfDiagnostics+  , unBound+    -- * 補助+  , colLen+  ) where++import           Graphics.Hgg.Spec     (ColData (..), Resolver, VisualSpec)+import           Graphics.Hgg.Validate (PlotDiagnostic (..), validatePlotWith)+import           Data.Map.Strict       (Map)+import qualified Data.Map.Strict       as M+import           Data.Text             (Text)+import qualified Data.Vector           as V++-- ===========================================================================+-- class PlotData (リッチ版: toResolver + columnNames + nrows)+-- ===========================================================================++-- | 任意の df 型を hgg の描画系に橋渡しする typeclass。+--+-- リッチ版 (proposal spec-2 §3.1 のユーザ決定): 'toResolver' だけでも描画は+-- できるが、 'columnNames' で **バインド時の列存在検証** (Phase 14 A5) と将来の+-- pairs / auto-aes が、 'nrows' で **空 df 検出**が可能になる。+class PlotData df where+  -- | 列名 → 'ColData'。 既存の 'Resolver' 型をそのまま再利用する+  --   (= @saveSVG@ 等が第 2 引数に取るのと同じ「実質 df」)。+  toResolver  :: df -> Resolver++  -- | 全列名。 列存在チェック・将来の auto-aes / pairs に使う。+  columnNames :: df -> [Text]++  -- | 行数。 空 df (@nrows == 0@) 検出に使う。 列ごとに長さが違う場合は+  --   **最長の列の長さ**を返す (= 描画が要求しうる最大 index)。+  nrows       :: df -> Int++-- | 'ColData' (数値列 / 文字列列) の要素数。+colLen :: ColData -> Int+colLen (NumData v) = V.length v+colLen (TxtData v) = V.length v++-- ===========================================================================+-- ゼロ依存 instance ─ df ライブラリ無しでも使える最小実装+-- ===========================================================================++-- | assoc-list (列名と列の対の並び)。 列名重複時は **先勝ち** ('lookup' 準拠)。+instance PlotData [(Text, ColData)] where+  toResolver  pairs = \name -> lookup name pairs+  columnNames       = map fst+  nrows       pairs = maximum (0 : map (colLen . snd) pairs)++-- | 'Data.Map.Strict.Map' 版。 列名重複は Map が解決済 (一意)。+instance PlotData (Map Text ColData) where+  toResolver  m = \name -> M.lookup name m+  columnNames   = M.keys+  nrows       m = maximum (0 : map colLen (M.elems m))++-- ===========================================================================+-- バインド境界 ─ df を spec に結びつけた純値 'BoundPlot'+-- ===========================================================================++-- | df バインド済の plot。 描画関数 (saveSVGBound 等、 backend package 側) が+-- 消費する束。+--+-- 'bpDiagnostics' は '(|>>)' バインド時の検証結果を **値として**運ぶ+-- (proposal spec-2 §3.3 / Phase 14 検証案1)。 '(|>>)' 自身は例外を投げない+-- 純関数なので、 @let p = df |>> spec@ を list に詰める・テストで比較するが+-- 成り立つ (= 「plot は値」)。 Error severity の報告は描画関数が実行時に行う。+data BoundPlot = BoundPlot+  { bpResolver    :: Resolver+  , bpSpec        :: VisualSpec+  , bpDiagnostics :: [PlotDiagnostic]+  }++-- | df を spec にバインド。 **純関数** (例外を投げない)。+--+-- 演算子が @|>@ でなく @|>>@ なのは、 Hackage @dataframe@ が @|>@ を public+-- export しており衝突するため (proposal spec-2 / Phase 14 計画書 §設計判断)。+-- 'infixl' 1 で @<>@ (infixr 6) より弱く、 @df |>> (layer a <> layer b)@ を+-- カッコ無しで書ける。+--+-- 検証 (A5): バインド時に 'validatePlotWith' で spec の 'ColByName' を df の+-- 'columnNames' と突合し、 結果を 'bpDiagnostics' に **値として**格納する+-- (存在しない列 → 'ColumnNotFound' + 編集距離 suggestion、 型不一致 →+-- 'ColumnTypeMismatch'、 必須 aesthetic 欠落、 空 plot を検出)。 空 df+-- (@nrows == 0@) は専用 error kind が無いため 'PlotInfo' で残す (lenient)。+-- 例外は一切投げない。 Error の報告は描画関数 ('saveSVGBound') が実行時に行う。+-- Phase 24 A6: spec 型で束の型を選ぶ (2D = 'BoundPlot'、 3D = BoundPlot3D)。+-- 2D の意味論は従来と同一 (instance が旧実装そのもの)。 3D instance は+-- hgg-3d 側 (型の定義 package = 非 orphan)。+class BindableSpec spec where+  type BoundOf spec+  bindData :: PlotData df => df -> spec -> BoundOf spec++instance BindableSpec VisualSpec where+  type BoundOf VisualSpec = BoundPlot+  bindData df spec =+    BoundPlot r spec (emptyDfDiagnostics df ++ validatePlotWith (columnNames df) r spec)+   where+    r = toResolver df++-- | 空 df の共通診断 (2D/3D instance で共有)。+emptyDfDiagnostics :: PlotData df => df -> [PlotDiagnostic]+emptyDfDiagnostics df+  | nrows df == 0 = [PlotInfo "DataFrame が空です (nrows == 0)。 描画は空になります。"]+  | otherwise     = []++(|>>) :: (PlotData df, BindableSpec spec) => df -> spec -> BoundOf spec+(|>>) = bindData+infixl 1 |>>++-- | 検証を逃がす raw 経路。 'BoundPlot' から (Resolver, VisualSpec) を取り出し、+-- 既存の @saveSVGWith@ / @renderSVGWith@ に直接渡せる。+unBound :: BoundPlot -> (Resolver, VisualSpec)+unBound (BoundPlot r s _) = (r, s)
+ test/Spec.hs view
@@ -0,0 +1,80 @@+-- | hgg-frame テスト。 A1 = class PlotData のゼロ依存 instance 検証。+{-# LANGUAGE OverloadedStrings #-}+module Main (main) where++-- 注: renderBound (df |>> spec → SVG) の smoke は hgg-svg の test に置く。+-- frame:test が svg に依存すると cabal が frame↔svg をパッケージ循環と見なすため。+import           Graphics.Hgg.Frame+import           Graphics.Hgg.Spec     (ColData (..), layer, scatter)+import           Graphics.Hgg.Validate (PlotDiagnostic (..), PlotErrorKind (..),+                                        Severity (..), diagnosticSeverity)+import           Data.Map.Strict       (Map)+import qualified Data.Map.Strict       as M+import           Data.Text             (Text)+import qualified Data.Vector           as V+import           Test.Hspec++-- 長さ違いの列を含むサンプル (g が最長 = 4 行)+assocDF :: [(Text, ColData)]+assocDF =+  [ ("x", NumData (V.fromList [1, 2, 3]))+  , ("g", TxtData (V.fromList ["a", "b", "c", "d"]))+  ]++mapDF :: Map Text ColData+mapDF = M.fromList assocDF++-- scatter 用の x/y を持つ df (bind/render テスト用)。 列順違いで同一データ+xyAssoc :: [(Text, ColData)]+xyAssoc =+  [ ("x", NumData (V.fromList [1, 2, 3]))+  , ("y", NumData (V.fromList [4, 5, 6]))+  ]++xyMap :: Map Text ColData+xyMap = M.fromList xyAssoc++main :: IO ()+main = hspec $ do+  describe "PlotData [(Text, ColData)]" $ do+    it "columnNames は宣言順" $+      columnNames assocDF `shouldBe` ["x", "g"]+    it "nrows は最長列の長さ" $+      nrows assocDF `shouldBe` 4+    it "toResolver は列を引ける" $+      toResolver assocDF "x" `shouldBe` Just (NumData (V.fromList [1, 2, 3]))+    it "toResolver は不在列で Nothing" $+      toResolver assocDF "zzz" `shouldBe` Nothing++  describe "PlotData (Map Text ColData)" $ do+    it "columnNames は Map のキー (昇順)" $+      columnNames mapDF `shouldBe` ["g", "x"]+    it "nrows は最長列の長さ" $+      nrows mapDF `shouldBe` 4+    it "空 df の nrows は 0" $+      nrows (M.empty :: Map Text ColData) `shouldBe` 0++  describe "(|>>) バインド" $ do+    it "BoundPlot に resolver/spec を載せ、 診断は空 (A3 時点)" $ do+      let bp = xyMap |>> layer (scatter "x" "y")+      bpDiagnostics bp `shouldBe` []+      fmap colLen (bpResolver bp "x") `shouldBe` Just 3+    it "純値: unBound で (resolver, spec) を取り出せる" $ do+      let (r, _) = unBound (xyAssoc |>> layer (scatter "x" "y"))+      fmap colLen (r "y") `shouldBe` Just 3+    it "全列存在・数値なら診断ゼロ" $+      bpDiagnostics (xyMap |>> layer (scatter "x" "y")) `shouldBe` []++  describe "(|>>) 検証 (A5、 案1: 純値・例外なし)" $ do+    it "存在しない列 → ColumnNotFound Error (例外は投げない)" $ do+      let diags = bpDiagnostics (xyAssoc |>> layer (scatter "x" "nope"))+          isNopeNotFound (PlotError (ColumnNotFound n _) _) = n == "nope"+          isNopeNotFound _                                  = False+      any isNopeNotFound diags `shouldBe` True+      any ((== SevError) . diagnosticSeverity) diags `shouldBe` True+    it "空 df → PlotInfo (lenient、 Error ではない)" $ do+      let emptyAssoc = [] :: [(Text, ColData)]+          diags = bpDiagnostics (emptyAssoc |>> layer (scatter "x" "y"))+          isInfo (PlotInfo _) = True+          isInfo _            = False+      any isInfo diags `shouldBe` True