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 +7/−0
- LICENSE +30/−0
- hgg-frame.cabal +56/−0
- src/Graphics/Hgg/Frame.hs +145/−0
- test/Spec.hs +80/−0
+ 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