packages feed

matplotlib 0.1.2.1 → 0.2.0

raw patch · 5 files changed

+435/−260 lines, 5 filesdep +containersPVP ok

version bump matches the API change (PVP)

Dependencies added: containers

API changes (from Hackage documentation)

- Graphics.Matplotlib: B :: String -> Option
- Graphics.Matplotlib: Exec :: String -> MplotCommand
- Graphics.Matplotlib: K :: String -> String -> Option
- Graphics.Matplotlib: LoadData :: ByteString -> MplotCommand
- Graphics.Matplotlib: Matplotlib :: [MplotCommand] -> Matplotlib
- Graphics.Matplotlib: class MplotValue val
- Graphics.Matplotlib: data MplotCommand
- Graphics.Matplotlib: instance GHC.Classes.Eq Graphics.Matplotlib.MplotCommand
- Graphics.Matplotlib: instance GHC.Classes.Ord Graphics.Matplotlib.MplotCommand
- Graphics.Matplotlib: instance GHC.Show.Show Graphics.Matplotlib.MplotCommand
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue (GHC.Base.String, GHC.Base.String)
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue (x, y) => Graphics.Matplotlib.MplotValue [(x, y)]
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue GHC.Base.String
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue GHC.Types.Double
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue GHC.Types.Int
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue Graphics.Matplotlib.Option
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue [GHC.Base.String]
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue [Graphics.Matplotlib.Option]
- Graphics.Matplotlib: instance Graphics.Matplotlib.MplotValue x => Graphics.Matplotlib.MplotValue (x, x)
- Graphics.Matplotlib: mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b]
- Graphics.Matplotlib: mplot :: String -> Matplotlib
- Graphics.Matplotlib: options :: [Option] -> String
- Graphics.Matplotlib: pyDetach :: [[Char]]
- Graphics.Matplotlib: pyFigure :: [Char] -> [[Char]]
- Graphics.Matplotlib: pyIncludes :: [[Char]]
- Graphics.Matplotlib: pyOnscreen :: [[Char]]
- Graphics.Matplotlib: pyReadData :: [Char] -> [[Char]]
- Graphics.Matplotlib: python :: Foldable t => t String -> IO (Either String String)
- Graphics.Matplotlib: showLine :: (ToJSON t1, ToJSON t) => t1 -> t -> [Option] -> IO (Either String String)
- Graphics.Matplotlib: showPlot :: (ToJSON t, ToJSON t1) => t1 -> t -> [Option] -> IO (Either String String)
- Graphics.Matplotlib: showScatter :: (ToJSON t1, ToJSON t) => t1 -> t -> [Option] -> IO (Either String String)
- Graphics.Matplotlib: toPy :: MplotCommand -> String
- Graphics.Matplotlib: withMplot :: Matplotlib -> ([String] -> IO a) -> IO a
+ Graphics.Matplotlib: (##) :: MplotValue val => Matplotlib -> val -> Matplotlib
+ Graphics.Matplotlib: (@@) :: Matplotlib -> [Option] -> Matplotlib
+ Graphics.Matplotlib: infixl 6 #
+ Graphics.Matplotlib: mp :: Matplotlib
+ Graphics.Matplotlib: o1 :: String -> Option
+ Graphics.Matplotlib: o2 :: String -> String -> Option
+ Graphics.Matplotlib.Internal: (##) :: MplotValue val => Matplotlib -> val -> Matplotlib
+ Graphics.Matplotlib.Internal: (#) :: (MplotValue val) => Matplotlib -> val -> Matplotlib
+ Graphics.Matplotlib.Internal: (%) :: Matplotlib -> Matplotlib -> Matplotlib
+ Graphics.Matplotlib.Internal: (@@) :: Matplotlib -> [Option] -> Matplotlib
+ Graphics.Matplotlib.Internal: Exec :: String -> MplotCommand
+ Graphics.Matplotlib.Internal: K :: String -> String -> Option
+ Graphics.Matplotlib.Internal: LoadData :: ByteString -> MplotCommand
+ Graphics.Matplotlib.Internal: Matplotlib :: Seq MplotCommand -> Maybe ([Option] -> MplotCommand) -> Seq MplotCommand -> Matplotlib
+ Graphics.Matplotlib.Internal: P :: String -> Option
+ Graphics.Matplotlib.Internal: [es] :: MplotCommand -> String
+ Graphics.Matplotlib.Internal: [mpCommands] :: Matplotlib -> Seq MplotCommand
+ Graphics.Matplotlib.Internal: [mpPendingOption] :: Matplotlib -> Maybe ([Option] -> MplotCommand)
+ Graphics.Matplotlib.Internal: [mpRest] :: Matplotlib -> Seq MplotCommand
+ Graphics.Matplotlib.Internal: class MplotValue val
+ Graphics.Matplotlib.Internal: data Matplotlib
+ Graphics.Matplotlib.Internal: data MplotCommand
+ Graphics.Matplotlib.Internal: data Option
+ Graphics.Matplotlib.Internal: def :: Matplotlib -> [Option] -> Matplotlib
+ Graphics.Matplotlib.Internal: defFn :: [Option] -> [Option] -> [Option]
+ Graphics.Matplotlib.Internal: infixl 5 %
+ Graphics.Matplotlib.Internal: infixl 6 @@
+ Graphics.Matplotlib.Internal: instance GHC.Classes.Eq Graphics.Matplotlib.Internal.MplotCommand
+ Graphics.Matplotlib.Internal: instance GHC.Classes.Eq Graphics.Matplotlib.Internal.Option
+ Graphics.Matplotlib.Internal: instance GHC.Classes.Ord Graphics.Matplotlib.Internal.MplotCommand
+ Graphics.Matplotlib.Internal: instance GHC.Classes.Ord Graphics.Matplotlib.Internal.Option
+ Graphics.Matplotlib.Internal: instance GHC.Show.Show Graphics.Matplotlib.Internal.MplotCommand
+ Graphics.Matplotlib.Internal: instance GHC.Show.Show Graphics.Matplotlib.Internal.Option
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue (x, y) => Graphics.Matplotlib.Internal.MplotValue [(x, y)]
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue GHC.Base.String
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue GHC.Integer.Type.Integer
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue GHC.Types.Double
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue GHC.Types.Int
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue [GHC.Base.String]
+ Graphics.Matplotlib.Internal: instance Graphics.Matplotlib.Internal.MplotValue x => Graphics.Matplotlib.Internal.MplotValue (x, x)
+ Graphics.Matplotlib.Internal: mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b]
+ Graphics.Matplotlib.Internal: mp :: Matplotlib
+ Graphics.Matplotlib.Internal: mplotString :: String -> Matplotlib
+ Graphics.Matplotlib.Internal: o1 :: String -> Option
+ Graphics.Matplotlib.Internal: o2 :: String -> String -> Option
+ Graphics.Matplotlib.Internal: optFn :: ([Option] -> String) -> Matplotlib -> Matplotlib
+ Graphics.Matplotlib.Internal: option :: Matplotlib -> [Option] -> Matplotlib
+ Graphics.Matplotlib.Internal: optionFn :: ([Option] -> [Option]) -> Matplotlib -> Matplotlib
+ Graphics.Matplotlib.Internal: options :: Matplotlib -> Matplotlib
+ Graphics.Matplotlib.Internal: pyDetach :: [[Char]]
+ Graphics.Matplotlib.Internal: pyFigure :: [Char] -> [[Char]]
+ Graphics.Matplotlib.Internal: pyIncludes :: [[Char]]
+ Graphics.Matplotlib.Internal: pyOnscreen :: [[Char]]
+ Graphics.Matplotlib.Internal: pyReadData :: [Char] -> [[Char]]
+ Graphics.Matplotlib.Internal: python :: Foldable t => t String -> IO (Either String String)
+ Graphics.Matplotlib.Internal: readData :: ToJSON a => a -> Matplotlib
+ Graphics.Matplotlib.Internal: renderOptions :: [Option] -> [Char]
+ Graphics.Matplotlib.Internal: resolvePending :: Matplotlib -> Matplotlib
+ Graphics.Matplotlib.Internal: toPy :: MplotCommand -> String
+ Graphics.Matplotlib.Internal: toPython :: MplotValue val => val -> String
+ Graphics.Matplotlib.Internal: withMplot :: Matplotlib -> ([String] -> IO a) -> IO a
- Graphics.Matplotlib: (#) :: MplotValue val => String -> val -> String
+ Graphics.Matplotlib: (#) :: (MplotValue val) => Matplotlib -> val -> Matplotlib
- Graphics.Matplotlib: addSubplot :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: addSubplot :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib
- Graphics.Matplotlib: axisXTickLabels :: MplotValue val => val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: axisXTickLabels :: MplotValue val => val -> Matplotlib
- Graphics.Matplotlib: axisXTickSpacing :: (MplotValue val1, MplotValue val) => val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: axisXTickSpacing :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib
- Graphics.Matplotlib: dataHistogram :: (MplotValue val1, MplotValue val) => val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: dataHistogram :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib
- Graphics.Matplotlib: dataLine :: (MplotValue val1, MplotValue val) => val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: dataLine :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib
- Graphics.Matplotlib: dataPlot :: (MplotValue val, MplotValue val1) => val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: dataPlot :: (MplotValue val, MplotValue val1) => val1 -> val -> Matplotlib
- Graphics.Matplotlib: dataScatter :: (MplotValue val1, MplotValue val) => val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: dataScatter :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib
- Graphics.Matplotlib: def :: Option -> [Option] -> [Option]
+ Graphics.Matplotlib: def :: Matplotlib -> [Option] -> Matplotlib
- Graphics.Matplotlib: histogram :: (MplotValue val, ToJSON t) => t -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: histogram :: (MplotValue val, ToJSON t) => t -> val -> Matplotlib
- Graphics.Matplotlib: line :: (ToJSON t1, ToJSON t) => t1 -> t -> [Option] -> Matplotlib
+ Graphics.Matplotlib: line :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib
- Graphics.Matplotlib: line1 :: (Foldable t, ToJSON (t a)) => t a -> [Option] -> Matplotlib
+ Graphics.Matplotlib: line1 :: (Foldable t, ToJSON (t a)) => t a -> Matplotlib
- Graphics.Matplotlib: lineF :: (ToJSON a, ToJSON b) => (a -> b) -> [a] -> [Option] -> Matplotlib
+ Graphics.Matplotlib: lineF :: (ToJSON a, ToJSON b) => (a -> b) -> [a] -> Matplotlib
- Graphics.Matplotlib: matShow :: ToJSON a => a -> [Option] -> Matplotlib
+ Graphics.Matplotlib: matShow :: ToJSON a => a -> Matplotlib
- Graphics.Matplotlib: mplotSubplot :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: mplotSubplot :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib
- Graphics.Matplotlib: plot :: (ToJSON t, ToJSON t1) => t1 -> t -> [Option] -> Matplotlib
+ Graphics.Matplotlib: plot :: (ToJSON t, ToJSON t1) => t1 -> t -> Matplotlib
- Graphics.Matplotlib: plotInterpolated :: (MplotValue val, ToJSON t, ToJSON t1) => t1 -> t -> val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: plotInterpolated :: (MplotValue val, ToJSON t, ToJSON t1) => t1 -> t -> val -> Matplotlib
- Graphics.Matplotlib: plotMapLinear :: ToJSON b => (Double -> b) -> Double -> Double -> Double -> [Option] -> Matplotlib
+ Graphics.Matplotlib: plotMapLinear :: ToJSON b => (Double -> b) -> Double -> Double -> Double -> Matplotlib
- Graphics.Matplotlib: scatter :: (ToJSON t1, ToJSON t) => t1 -> t -> [Option] -> Matplotlib
+ Graphics.Matplotlib: scatter :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib
- Graphics.Matplotlib: showHistogram :: (ToJSON t, MplotValue val) => t -> val -> [Option] -> IO (Either String String)
+ Graphics.Matplotlib: showHistogram :: (ToJSON t, MplotValue val) => t -> val -> IO (Either String String)
- Graphics.Matplotlib: title :: MplotValue val => val -> [Option] -> Matplotlib
+ Graphics.Matplotlib: title :: MplotValue val => val -> Matplotlib

Files

README.md view
@@ -7,17 +7,17 @@  ![matplotlib contour plot](https://github.com/abarbu/matplotlib-haskell/raw/master/imgs/contour.png) -More info and docs forthcoming. For now see the tests for some examples.--Note that the API will undergo one large change in the next few days to make-optional arguments more pervasive and less of a hassle and to eliminate some-necessary type annotations. After this it will stabilize.+[Documentation is available on Hackage](https://hackage.haskell.org/package/matplotlib). For+more examples see the tests.  ```haskell+{-# LANGUAGE ExtendedDefaultRules #-} import Matplotlib  onscreen $ contourF (\a b -> sin (degreesRadians a) + cos (degreesRadians b)) (-100) 100 (-200) 200 10 ```++We need `-XExtendedDefaultRules` to avoid having to manually having to specify certain types.  ### Installation 
matplotlib.cabal view
@@ -1,5 +1,5 @@ name:                matplotlib-version:             0.1.2.1+version:             0.2.0 synopsis:            Bindings to Matplotlib; a Python plotting library description:         Matplotlib is probably the most full featured plotting library out there. These bindings provide a quick, easy, and extensible way to use it in Haskell. Type-safety is largely non-existent and sacrificed for maximum extensibility. homepage:            https://github.com/abarbu/matplotlib-haskell@@ -15,12 +15,14 @@  library   hs-source-dirs:      src-  exposed-modules:     Graphics.Matplotlib+  exposed-modules:     Graphics.Matplotlib.Internal+                     , Graphics.Matplotlib   build-depends:       base >= 4.7 && < 5                      , process                      , bytestring                      , aeson                      , temporary+                     , containers   default-language:    Haskell2010  test-suite matplotlib-test
src/Graphics/Matplotlib.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE DeriveGeneric, TypeSynonymInstances, FlexibleInstances, ScopedTypeVariables, FlexibleContexts, OverlappingInstances #-}-+{-# LANGUAGE ExtendedDefaultRules #-}+-----------------------------------------------------------------------------+-- | -- Matplotlib bindings and an interface to easily bind to new portions of the -- API. The most essential parts of Matplotlib are wrapped and exposed to -- Haskell through an interface that allows extenisbility. Code is generated on@@ -14,194 +15,76 @@ -- into a data variable that is a python array. Data is transffered via -- json. This data variable is indexed by various rendering commands. ----- Many functions take an optional argument list.--- -- Functions which start with the word data operate on the data array, arguments -- are python code that should access that array. Most other functions take -- haskell objects and load them into python.+--+-- This module should expose enough tools so that you can bind any part of the+-- matplotlib API. A binding with options, such as that of 'plot', looks like:+--+-- @+--   readData (x, y)+--   % mp # "p = plot.plot(data[" # a # "], data[" # b # "]" ## ")"+--   % mp # "plot.xlabel('" # label # "')"+-- @+--+-- Where important functions are:+--+--   [@'readData'@] Load the given data into the python data array by serializing it to JSON.+--   [@'%'@] Sequence two plots+--   [@'mp'@] Create an empty plot+--   [@'#'@] Append python code to the last command in a plot+--   [@'##'@] Just like '#' but also adds in a placeholder for an options list+--+-- You can call this plot with+--+-- > plot [1,2,3,4,5,6] [1,3,2,5,2] @@ [o1 "'go-'", o2 "linewidth" "2"]+--+-- where '@@' applies an options list replacing the last '##'+--+--  [@'o1'@] A single positional option+--  [@'o2'@] A keyword option+--+-- Right now there's no easy way to bind to an option other than the last one+-- unless you want to pass options in as parameters.+----------------------------------------------------------------------------- -module Graphics.Matplotlib where-import System.IO.Temp-import System.Process-import GHC.Generics-import Data.Aeson-import Control.Monad-import System.IO-import qualified Data.ByteString.Lazy as B+module Graphics.Matplotlib+  ( module Graphics.Matplotlib+    -- * Creating custom plots and applying options+  , Matplotlib(), Option(),(@@), (%), o1, o2, (##), (#), mp, def, readData)+where import Data.List-import Control.Exception---- | A handle internal function-mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b]-mapLinear f s e n = map (\v -> f $ s + (v * (e - s) / n)) [0..n]---- | The wrapper type for a matplotlib computation. This is opaque.-data Matplotlib = Matplotlib [MplotCommand]---- | A maplotlib command, right now we have a very shallow embedding essentially--- dealing in opaque strings containing python code as well as the ability to--- load data. The loaded should be a json object.-data MplotCommand-  = LoadData B.ByteString-  | Exec String-  deriving (Show, Eq, Ord)---- | Throughout the API we need to accept options in order to expose--- matplotlib's many configuration options.-data Option =-  -- | results in a=b-  K String String-  -- | just inserts the option verbatim as an argument at the end of the function-  | B String---- | Convert an 'MplotCommand' to python code, doesn't do much right now-toPy (LoadData _) = error "withMplot needed to load data"-toPy (Exec str)   = str---- | The io action is given a list of python commands to execute (note that--- these are commands in the sense of lines of python code; each inidivudal line--- may not be parseable on its own-withMplot :: Matplotlib -> ([String] -> IO a) -> IO a-withMplot (Matplotlib cs) f = preload cs []-  where-    preload [] cmds = f $ map toPy $ reverse cmds-    preload ((LoadData obj):cs) cmds =-          withSystemTempFile "data.json"-            (\dataFile dataHandle -> do-                B.hPutStr dataHandle obj-                hClose dataHandle-                preload cs $ ((map Exec $ pyReadData dataFile) ++ cmds))-    preload (c:cs) cmds = preload cs (c:cmds)-  ---- Python operations---- | Run python given a code string.-python codeStr =-  catch (withSystemTempFile "code.py"-         (\codeFile codeHandle -> do-             forM_ codeStr (hPutStrLn codeHandle)-             hClose codeHandle-             Right <$> readProcess "/usr/bin/python3" [codeFile] ""))-         (\e -> return $ Left $ show (e :: IOException))---- | The standard python includes of every plot-pyIncludes = ["import matplotlib"-             -- TODO Provide a way to set the render backend-             -- ,"matplotlib.use('GtkAgg')"-             ,"import matplotlib.path as mpath"-             ,"import matplotlib.patches as mpatches"-             ,"import matplotlib.pyplot as plot"-             ,"import matplotlib.mlab as mlab"-             ,"from matplotlib import cm"-             ,"from mpl_toolkits.mplot3d import axes3d"-             ,"import numpy as np"-             ,"import os"-             ,"import sys"-             ,"import json"-             ,"import random, datetime"-             ,"from matplotlib.dates import DateFormatter, WeekdayLocator"]---- | The python command that reads external data into the python data array-pyReadData filename = ["data = json.loads(open('" ++ filename ++ "').read())"]---- | Detach python so we don't block (TODO This isn't working reliably)-pyDetach = ["pid = os.fork()"-           ,"if(pid != 0):"-           ,"  exit(0)"]---- | Python code to show a plot-pyOnscreen = ["plot.draw()"-             ,"plot.show()"]---- | Python code that saves a figure-pyFigure output = ["plot.savefig('" ++ output ++ "')"]+import Data.Aeson+import Graphics.Matplotlib.Internal ---- Running a plot+-- * Running a plot  -- | Show a plot, blocks until the figure is closed+onscreen :: Matplotlib -> IO (Either String String) onscreen m = withMplot m (\str -> python $ pyIncludes ++ str ++ pyDetach ++ pyOnscreen)  -- | Print the python code that would be executed+code :: Matplotlib -> IO (Either a String) code m = withMplot m (\str -> return $ Right $ unlines $ pyIncludes ++ str ++ pyDetach ++ pyOnscreen)  -- | Save to a file+figure :: [Char] -> Matplotlib -> IO (Either String String) figure filename m = withMplot m (\str -> python $ pyIncludes ++ str ++ pyFigure filename) --- Creating matplotlib computations---- | Create a plot that executes the string as python code-mplot s = Matplotlib [Exec s]--infixl 5 %--- | combine two matplotlib commands-(%) :: Matplotlib -> Matplotlib -> Matplotlib-(Matplotlib a) % (Matplotlib b) = Matplotlib (a ++ b)--infixl 6 #--- | Values which can be combined together to form a matplotlib command. Right--- now matplotlib commands are strings and this is just a helper to construct--- them.-class MplotValue val where-  (#) :: String -> val -> String-instance MplotValue String where-  s # b = s ++ b-instance MplotValue [String] where-  m # [] = m-  m # (x:xs) = m # x # "," # xs-instance MplotValue Double where-  s # b = s ++ show b-instance MplotValue Int where-  s # b = s ++ show b-instance MplotValue (String, String) where-  m # (n, v) = m # n # "=" # v-instance (MplotValue x) => MplotValue (x, x) where-  m # (n, v) = m # n # "=" # v-instance (MplotValue (x, y)) => MplotValue [(x, y)] where-  m # [] = m-  m # (x:xs) = m # x # "," # xs-instance MplotValue Option where-  m # (B a) = m # a-  m # (K a b) = m # a # "=" # b-instance MplotValue [Option] where-  m # [] = m-  m # (x:xs) = m # x # "," # xs----- Convenience & Data+-- * Plotting commands  -- | Plot the 'a' and 'b' entries of the data object-dataPlot :: (MplotValue val, MplotValue val1) => val1 -> val -> [Option] -> Matplotlib-dataPlot a b opts =-  mplot $ "p = plot.plot(data[" # a # "], data[" # b # "]" # options opts # ")"+dataPlot :: (MplotValue val, MplotValue val1) => val1 -> val -> Matplotlib+dataPlot a b = mp # "p = plot.plot(data[" # a # "], data[" # b # "]" ## ")"  -- | Plot the Haskell objects 'x' and 'y' as a line-plot :: (ToJSON t, ToJSON t1) => t1 -> t -> [Option] -> Matplotlib-plot x y opt =-  readData (x, y) % dataPlot (0::Int) (1::Int) opt---- | Plot & show onscreen-showPlot :: (ToJSON t, ToJSON t1) => t1 -> t -> [Option] -> IO (Either String String)-showPlot x y opt = onscreen (plot x y opt)---- | Load the given data into the 'data' array-readData :: ToJSON a => a -> Matplotlib-readData d = Matplotlib [LoadData $ encode d]---- ;;; Options---- | An internal helper to convert a list of options to the python code that--- applies those options in a call.-options :: [Option] -> String-options [] = ""-options xs = "," # xs---- | Combine a list of user options with a default; useful for options such as--- line styles that require sane defaults but can be overriden.-def o [] = [o]-def o l@((B _):_) = l-def o (x:xs) = x : def o xs+plot :: (ToJSON t, ToJSON t1) => t1 -> t -> Matplotlib+plot x y = readData (x, y) % dataPlot 0 1  -- | Show grid lines-gridLines = mplot "ax.grid(True)"+gridLines :: Matplotlib+gridLines = mp # "ax.grid(True)"  -- | Plot x against y where x is a date. --   xunit is something like 'weeks', yearStart, monthStart, dayStart are an offset to x.@@ -209,120 +92,135 @@ dateLine :: (ToJSON t1, ToJSON t2) => t1 -> t2 -> String -> (Int, Int, Int) -> Matplotlib dateLine x y xunit (yearStart, monthStart, dayStart) =     readData (x, y)-  % mplot ("data[0] = [datetime.timedelta("#xunit#"=i) + datetime.datetime("#yearStart#","#monthStart#","#dayStart#") for i in data[0]]")-  % dataPlot (0::Int) (1::Int) [B "-"]-  % mplot "ax.xaxis.set_major_formatter(DateFormatter('%B'))"-  % mplot "ax.xaxis.set_minor_locator(WeekdayLocator(byweekday=6))"+  % mp # "data[0] = [datetime.timedelta("#xunit#"=i) + datetime.datetime("#yearStart#","#monthStart#","#dayStart#") for i in data[0]]"+  % dataPlot 0 1 `def` [o1 "-"]+  % mp # "ax.xaxis.set_major_formatter(DateFormatter('%B'))"+  % mp # "ax.xaxis.set_minor_locator(WeekdayLocator(byweekday=6))"    -- | Add a label to the x axis-xLabel label = mplot $ "plot.xlabel('" # label # "')"+xLabel :: MplotValue val => val -> Matplotlib+xLabel label = mp # "plot.xlabel('" # label # "')"  -- | Add a label to the y axis-yLabel label = mplot $ "plot.ylabel('" # label # "')"+yLabel :: MplotValue val => val -> Matplotlib+yLabel label = mp # "plot.ylabel('" # label # "')"  -- | Add a label to the z axis-zLabel label = mplot $ "plot.zlabel('" # label # "')"+zLabel :: MplotValue val => val -> Matplotlib+zLabel label = mp # "plot.zlabel('" # label # "')"  -- | Create a histogram for the 'a' entry of the data array-dataHistogram a bins opts = mplot $ "plot.hist(data[" # a # "]," # bins # options opts # ")"+dataHistogram :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+dataHistogram a bins = mp # "plot.hist(data[" # a # "]," # bins ## ")"  -- | Plot a histogram for the given values with 'bins'-histogram values bins opts = readData [values] % dataHistogram (0::Int) bins opts+histogram :: (MplotValue val, ToJSON t) => t -> val -> Matplotlib+histogram values bins = readData [values] % dataHistogram 0 bins  -- | Plot & show the histogram-showHistogram values bins opts = onscreen $ histogram values bins opts+showHistogram :: (ToJSON t, MplotValue val) => t -> val -> IO (Either String String)+showHistogram values bins = onscreen $ histogram values bins  -- | Create a scatter plot accessing the given fields of the data array-dataScatter a b opts = dataPlot a b $ def (B "'.'") opts+dataScatter :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+dataScatter a b = dataPlot a b `def` [o1 "'.'"]  -- | Plot the given values as a scatter plot-scatter x y opts = plot x y $ def (B "'.'") opts---- | Plot and show a scatter plot-showScatter x y opts = showPlot x y $ def (B "'.'") opts+scatter :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib+scatter x y = plot x y `def` [o1 "'.'"]  -- | Create a line accessing the given entires of the data array-dataLine a b opts = dataPlot a b $ def (B "'-'") opts+dataLine :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+dataLine a b = dataPlot a b `def` [o1 "'-'"]  -- | Plot a line-line x y opts = plot x y $ def (B "'-'") opts+line :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib+line x y = plot x y `def` [o1 "'-'"]  -- | Plot a line given a function that will be executed for each element of -- given list. The list provides the x values, the function the y values.-lineF f l opts = plot l (map f l) $ def (B "'-'") opts---- | Plot and show a line-showLine x y opts = showPlot x y $ def (B "'-'") opts+lineF :: (ToJSON a, ToJSON b) => (a -> b) -> [a] -> Matplotlib+lineF f l = plot l (map f l) `def` [o1 "'-'"]  -- | Create a 3D contour contour xs ys zs =   readData (xs, ys, zs)   % axis3DProjection-  % surface (0::Int) (1::Int) (2::Int)-  % contourRaw (0::Int) (1::Int) (2::Int) (maximum2 xs) (maximum2 ys) (minimum2 zs)+  % surface 0 1 2+  % contourRaw 0 1 2 (maximum2 xs) (maximum2 ys) (minimum2 zs)   % axis3DLabels xs ys zs  -- | Create a 3D projection projections xs ys zs =   readData (xs, ys, zs)   % axis3DProjection-  % contourRaw (0::Int) (1::Int) (2::Int) (maximum2 xs) (maximum2 ys) (minimum2 zs)+  % contourRaw 0 1 2 (maximum2 xs) (maximum2 ys) (minimum2 zs)   % axis3DLabels xs ys zs  -- | Given a grid of x and y values and a number of steps call the given -- function and plot the 3D contour+contourF :: (ToJSON val, MplotValue val, Ord val) => (Double -> Double -> val) -> Double -> Double -> Double -> Double -> Double -> Matplotlib contourF f xStart xEnd yStart yEnd steps = contour xs ys zs-  where xs = mapLinear (\x -> (mapLinear (\y -> x) yStart yEnd steps)) xStart xEnd steps-        ys = mapLinear (\x -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps+  where xs = mapLinear (\x -> (mapLinear (\_ -> x) yStart yEnd steps)) xStart xEnd steps+        ys = mapLinear (\_ -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps         zs = mapLinear (\x -> (mapLinear (\y -> f x y) yStart yEnd steps)) xStart xEnd steps  -- | Given a grid of x and y values and a number of steps call the given -- function and plot the 3D projection+projectionsF :: (ToJSON val, MplotValue val, Ord val) => (Double -> Double -> val) -> Double -> Double -> Double -> Double -> Double -> Matplotlib projectionsF f xStart xEnd yStart yEnd steps = projections xs ys zs-  where xs = mapLinear (\x -> (mapLinear (\y -> x) yStart yEnd steps)) xStart xEnd steps-        ys = mapLinear (\x -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps+  where xs = mapLinear (\x -> (mapLinear (\_ -> x) yStart yEnd steps)) xStart xEnd steps+        ys = mapLinear (\_ -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps         zs = mapLinear (\x -> (mapLinear (\y -> f x y) yStart yEnd steps)) xStart xEnd steps  -- | Enable 3D projection-axis3DProjection = mplot $ "ax = plot.figure().gca(projection='3d')"+axis3DProjection :: Matplotlib+axis3DProjection = mp # "ax = plot.figure().gca(projection='3d')"  -- | Plot a 3D wireframe accessing the given elements of the data array-wireframe a b c = mplot $ "ax.plot_wireframe(np.array(data[" # a # "]), np.array(data[" # b # "]), np.array(data[" # c # "]), rstride=1, cstride=1)"+wireframe :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib+wireframe a b c = mp # "ax.plot_wireframe(np.array(data[" # a # "]), np.array(data[" # b # "]), np.array(data[" # c # "]), rstride=1, cstride=1)"  -- | Plot a 3D surface accessing the given elements of the data array-surface a b c = mplot $ "ax.plot_surface(np.array(data[" # a # "]), np.array(data[" # b # "]), np.array(data[" # c # "]), rstride=1, cstride=1, cmap=cm.Blues, alpha=0.3)"+surface :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib+surface a b c = mp # "ax.plot_surface(np.array(data[" # a # "]), np.array(data[" # b # "]), np.array(data[" # c # "]), rstride=1, cstride=1, cmap=cm.Blues, alpha=0.3)"  -- | Plot a contour accessing the given elements of the data array+contourRaw :: (MplotValue val1, MplotValue val2, MplotValue val5,+               MplotValue val4, MplotValue val3, MplotValue val) =>+             val5 -> val4 -> val3 -> val2 -> val1 -> val -> Matplotlib contourRaw a b c maxA maxB minC =-  mplot ("ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='z', offset=" # minC # ")")-  % mplot ("ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='x', offset=-" # maxA # ")")-  % mplot ("ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='y', offset=" # maxB #")")+  mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='z', offset=" # minC # ")"+  % mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='x', offset=-" # maxA # ")"+  % mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='y', offset=" # maxB #")"  -- | Smallest element of a list of lists+minimum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a minimum2 l = minimum $ minimum l -- | Largest element of a list of lists+maximum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a maximum2 l = maximum $ maximum l  -- | Label and set limits of a set of 3D axis -- TODO This is a mess, does both more and less than it claims. axis3DLabels xs ys zs =-  mplot "ax.set_xlabel('X')"-  % mplot ("ax.set_xlim3d(" # minimum2 xs # ", " # maximum2 xs # ")")-  % mplot "ax.set_ylabel('Y')"-  % mplot ("ax.set_ylim3d(" # minimum2 ys # ", " # maximum2 ys # ")")-  % mplot "ax.set_zlabel('Z')"-  % mplot ("ax.set_zlim3d(" # minimum2 zs # ", " # maximum2 zs # ")")+  mp # "ax.set_xlabel('X')"+  % mp # "ax.set_xlim3d(" # minimum2 xs # ", " # maximum2 xs # ")"+  % mp # "ax.set_ylabel('Y')"+  % mp # "ax.set_ylim3d(" # minimum2 ys # ", " # maximum2 ys # ")"+  % mp # "ax.set_zlabel('Z')"+  % mp # "ax.set_zlim3d(" # minimum2 zs # ", " # maximum2 zs # ")"  -- | Draw a bag graph in a subplot -- TODO Why do we need this? subplotDataBar a width offset opts =-  mplot $ "ax.bar(np.arange(len(data[" # a # "]))+" # offset # ", data[" # a # "], " # width # options opts # ")"+  mp # "ax.bar(np.arange(len(data[" # a # "]))+" # offset # ", data[" # a # "], " # width ## ")" @@ opts  -- | Create a subplot with the coordinates (r,c,f)-addSubplot r c f opts = mplot $ "ax = plot.figure().add_subplot(" # r # c # f # options opts # ")"+addSubplot r c f = mp # "ax = plot.figure().add_subplot(" # r # c # f ## ")"  -- | Access a subplot with the coordinates (r,c,f)-mplotSubplot r c f opts = mplot $ "ax = plot.subplot(" # r # "," # c # "," # f # options opts # ")"+mplotSubplot r c f = mp # "ax = plot.subplot(" # r # "," # c # "," # f ## ")"  -- | The default bar with barDefaultWidth nr = 1.0 / (fromIntegral nr + 1)@@ -330,75 +228,91 @@ -- | Create a set of labelled bars of a given height subplotBarsLabelled valuesList labels optsList =   subplotBars valuesList optsList-  % axisXTickSpacing (length $ head $ valuesList) ((1.0 - barDefaultWidth (length valuesList) / 2.0) :: Double) []-  % axisXTickLabels labels []+  % axisXTickSpacing (length $ head $ valuesList) (1.0 - barDefaultWidth (length valuesList) / 2.0)+  % axisXTickLabels labels  -- | Create a subplot and a set of labelled bars -- TODO This is a mess.. subplotBars valuesList optsList =   readData valuesList-  % addSubplot (1::Int) (1::Int) (1::Int) []-  % (let (width :: Double) = barDefaultWidth (length valuesList) in-       foldl1 (%) (zipWith3 (\vs opts i -> subplotDataBar i width (width * i) opts) valuesList optsList [0..]))+  % addSubplot 1 1 1+  % (let width = barDefaultWidth (length valuesList) in+       foldl1 (%) (zipWith3 (\_ opts i -> subplotDataBar i width (width * i) opts) valuesList optsList [0..]))  -- | Add a title-title s opts = mplot $ "plot.title('" # s # options opts # "')"+title :: MplotValue val => val -> Matplotlib+title s = mp # "plot.title('" # s ## "')"  -- | Set the spacing of ticks on the x axis-axisXTickSpacing nr width opts = mplot $ "ax.set_xticks(np.arange(" # nr # ")+" # width # options opts # ")"+axisXTickSpacing :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+axisXTickSpacing nr width = mp # "ax.set_xticks(np.arange(" # nr # ")+" # width ## ")"  -- | Set the labels on the x axis-axisXTickLabels labels opts = mplot $ "ax.set_xticklabels( (" # labels # ") " # options opts # " )"+axisXTickLabels :: MplotValue val => val -> Matplotlib+axisXTickLabels labels = mp # "ax.set_xticklabels( (" # labels # ") " ## " )"  -- | Update the data array to linearly interpolate between array entries+interpolate :: (MplotValue val, MplotValue val2, MplotValue val1) => val2 -> val1 -> val -> Matplotlib interpolate a b n =-  (mplot $ "data[" # b # "] = mlab.stineman_interp(np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # "),data[" # a # "],data[" # b # "],None)")-  % (mplot $ "data[" # a # "] = np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # ")")+  (mp # "data[" # b # "] = mlab.stineman_interp(np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # "),data[" # a # "],data[" # b # "],None)")+  % (mp # "data[" # a # "] = np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # ")")  -- | Plot x against y interpolating with n steps-plotInterpolated x y n opts =+plotInterpolated :: (MplotValue val, ToJSON t, ToJSON t1) => t1 -> t -> val -> Matplotlib+plotInterpolated x y n =   readData (x, y)-  % interpolate (0::Int) (1::Int) n-  % (dataPlot (0::Int) (1::Int) $ def (B "-") opts)+  % interpolate 0 1 n+  % dataPlot 0 1 `def` [o1 "-"]  -- | Square up the aspect ratio of a plot.-squareAxes = mplot "plot.axes().set_aspect('equal')"+squareAxes :: Matplotlib+squareAxes = mp # "plot.axes().set_aspect('equal')"  -- | Set the rotation of the labels on the x axis to the given number of degrees-roateAxesLabels degrees = mplot "labels = plot.axes().get_xticklabels()"-   % mplot "for label in labels:"-   % mplot ("    label.set_rotation("#degrees#")")+roateAxesLabels :: MplotValue val => val -> Matplotlib+roateAxesLabels degrees = mp # "labels = plot.axes().get_xticklabels()"+   % mp # "for label in labels:"+   % mp # "    label.set_rotation("#degrees#")"  -- | Set the x labels to be vertical-verticalAxes = mplot "labels = plot.axes().get_xticklabels()"-   % mplot "for label in labels:"-   % mplot "    label.set_rotation('vertical')"+verticalAxes :: Matplotlib+verticalAxes = mp # "labels = plot.axes().get_xticklabels()"+   % mp # "for label in labels:"+   % mp # "    label.set_rotation('vertical')"  -- | Set the x scale to be logarithmic-logX = mplot "plot.axes().set_xscale('log')"+logX :: Matplotlib+logX = mp # "plot.axes().set_xscale('log')"  -- | Set the y scale to be logarithmic-logY = mplot "plot.axes().set_yscale('log')"+logY :: Matplotlib+logY = mp # "plot.axes().set_yscale('log')"  -- | Set limits on the x axis-xlim l u = mplot $ "plot.xlim([" # l # "," # u # "])"+xlim :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+xlim l u = mp # "plot.xlim([" # l # "," # u # "])"  -- | Set limits on the y axis-ylim l u = mplot $ "plot.ylim([" # l # "," # u # "])"+ylim :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib+ylim l u = mp # "plot.ylim([" # l # "," # u # "])"  -- | A handy function to plot a line between two points give a function and a number o steps-plotMapLinear f s e n opts = line xs ys opts+plotMapLinear :: ToJSON b => (Double -> b) -> Double -> Double -> Double -> Matplotlib+plotMapLinear f s e n = line xs ys   where xs = mapLinear (\x -> x) s e n         ys = mapLinear (\x -> f x) s e n  -- | Plot a line between 0 and the length of the array with the given y values-line1 y opts = line [0..length y] y opts+line1 :: (Foldable t, ToJSON (t a)) => t a -> Matplotlib+line1 y = line [0..length y] y -matShow d opts =-  readData d-  % (mplot $ "plot.matshow(data" # options opts # ")")+-- | Plot a matrix+matShow :: ToJSON a => a -> Matplotlib+matShow d = readData d+            % (mp # "plot.matshow(data" ## ")")  -- | Plot a KDE of the given functions with an optional start/end and a bandwidth h+densityBandwidth :: [Double] -> Double -> Maybe (Double, Double) -> Matplotlib densityBandwidth l h maybeStartEnd =   plotMapLinear f (case maybeStartEnd of                     Nothing -> minimum l@@ -406,13 +320,13 @@                   (case maybeStartEnd of                     Nothing -> maximum l                     (Just (_, end)) -> end)-                  100-                  []+                   100   where f x = sum (map (\xi -> gaussianPdf x xi h) l) / ((fromIntegral $ length l) * h)         gaussianPdf x mu sigma = exp (- sqr (x - mu) / (2 * sigma)) / sqrt (2 * pi * sigma)         sqr x = x * x  -- | Plot a KDE of the given functions; a good bandwith will be chosen automatically+density :: [Double] -> Maybe (Double, Double) -> Matplotlib density l maybeStartEnd =   densityBandwidth l (((4 * (variance ** 5)) / (fromIntegral $ 3 * length l)) ** (1 / 5) / 3) maybeStartEnd   where mean = foldl' (+) 0 l / (fromIntegral $ length l)
+ src/Graphics/Matplotlib/Internal.hs view
@@ -0,0 +1,253 @@+{-# LANGUAGE FlexibleInstances, ScopedTypeVariables, FlexibleContexts, ExtendedDefaultRules #-}+-- |+-- Internal representations of the Matplotlib data. These are not API-stable+-- and may change. You can easily extend the provided bindings without relying+-- on the internals exposed here but they are provided just in case.+module Graphics.Matplotlib.Internal where+import System.IO.Temp+import System.Process+import Data.Aeson+import Control.Monad+import System.IO+import qualified Data.ByteString.Lazy as B+import Data.List+import Control.Exception+import qualified Data.Sequence as S+import Data.Sequence (Seq, (|>), (><))+import Data.Maybe+import GHC.Exts(toList)++-- | A handy miscellaneous function to linearly map over a range of numbers in a given number of steps+mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b]+mapLinear f s e n = map (\v -> f $ s + (v * (e - s) / n)) [0..n]++-- $ Basics++-- | The wrapper type for a matplotlib computation.+data Matplotlib = Matplotlib {+  mpCommands :: Seq MplotCommand   -- ^ Resolved computations that have been transformed to commands+  , mpPendingOption :: Maybe ([Option] -> MplotCommand)   -- ^ A pending computation that is affected by applied options+  , mpRest :: Seq MplotCommand  -- ^ Computations that follow the one that is pending+  }++-- | A maplotlib command, right now we have a very shallow embedding essentially+-- dealing in strings containing python code as well as the ability to load+-- data. The loaded data should be a json object.+data MplotCommand+  = LoadData B.ByteString+  | Exec { es :: String }+  deriving (Show, Eq, Ord)++-- | Throughout the API we need to accept options in order to expose+-- matplotlib's many configuration options.+data Option =+  -- | results in a=b+  K String String+  -- | just inserts the option verbatim as an argument at the end of the function+  | P String+  deriving (Show, Eq, Ord)++-- | Convert an 'MplotCommand' to python code, doesn't do much right now+toPy :: MplotCommand -> String+toPy (LoadData _) = error "withMplot needed to load data"+toPy (Exec str)   = str++-- | Resolve the pending command with no options provided.+resolvePending :: Matplotlib -> Matplotlib+resolvePending m = m { mpCommands =+                       (maybe (mpCommands m)+                              (\pendingCommand -> (mpCommands m |> pendingCommand []))+                              $ mpPendingOption m) >< mpRest m+                     , mpPendingOption = Nothing+                     , mpRest = S.empty}++-- | The io action is given a list of python commands to execute (note that+-- these are commands in the sense of lines of python code; each inidivudal line+-- may not be parseable on its own+withMplot :: Matplotlib -> ([String] -> IO a) -> IO a+withMplot m f = preload cs []+  where+    cs = toList $ mpCommands $ resolvePending m+    preload [] cmds = f $ map toPy $ reverse cmds+    preload ((LoadData obj):l) cmds =+          withSystemTempFile "data.json"+            (\dataFile dataHandle -> do+                B.hPutStr dataHandle obj+                hClose dataHandle+                preload l $ ((map Exec $ pyReadData dataFile) ++ cmds))+    preload (c:l) cmds = preload l (c:cmds)++-- | Create a plot that executes the string as python code+mplotString :: String -> Matplotlib+mplotString s = Matplotlib S.empty Nothing (S.singleton $ Exec s)++-- | Create an empty plot. This the beginning of most plotting commands.+mp :: Matplotlib+mp = Matplotlib S.empty Nothing S.empty++-- | Load the given data into the 'data' array+readData :: ToJSON a => a -> Matplotlib+readData d = Matplotlib (S.singleton $ LoadData $ encode d) Nothing S.empty++infixl 5 %+-- | Combine two matplotlib commands+(%) :: Matplotlib -> Matplotlib -> Matplotlib+a % b | isJust $ mpPendingOption b = b { mpCommands = mpCommands (resolvePending a) >< mpCommands b }+      | otherwise = a { mpRest = mpRest a >< mpCommands b >< mpRest b }++infixl 6 #+-- | Add Python code to the last matplotlib command+(#) :: (MplotValue val) => Matplotlib -> val -> Matplotlib+m # v | S.null $ mpRest m =+        case mpPendingOption m of+          Nothing -> m { mpRest = S.singleton $ Exec $ toPython v }+          (Just f) -> m { mpPendingOption = Just (\o -> Exec $ es (f o) ++ toPython v)}+      | otherwise = m { mpRest = S.adjust (\(Exec s) -> Exec $ s ++ toPython v) (S.length (mpRest m) - 1) (mpRest m) }++-- | Values which can be combined together to form a matplotlib command. These+-- specify how values are rendered in Python code.+class MplotValue val where+  toPython :: val -> String++instance MplotValue String where+  toPython s = s+instance MplotValue [String] where+  toPython [] = ""+  toPython (x:xs) = toPython x ++ "," ++ toPython xs+instance MplotValue Double where+  toPython s = show s+instance MplotValue Integer where+  toPython s = show s+instance MplotValue Int where+  toPython s = show s+instance (MplotValue x) => MplotValue (x, x) where+  toPython (n, v) = toPython n ++ " = " ++ toPython v+instance (MplotValue (x, y)) => MplotValue [(x, y)] where+  toPython [] = ""+  toPython (x:xs) = toPython x ++ ", " ++ toPython xs++default (Integer, Int, Double)++-- $ Options++-- | Add an option to the last matplotlib command. Commands can have only one option!+-- optFn :: Matplotlib -> Matplotlib+optFn :: ([Option] -> String) -> Matplotlib -> Matplotlib+optFn f l | isJust $ mpPendingOption l = error "Commands can have only open option. TODO Enforce this through the type system or relax it!"+          | otherwise = l' { mpPendingOption = Just (\os -> Exec (sl ++ f os)) }+  where (l', (Exec sl)) = removeLast l+        removeLast x@(Matplotlib _ Nothing s) = (x { mpRest = sdeleteAt (S.length s - 1) s }+                                                , fromMaybe (Exec "") (slookup (S.length s - 1) s))+        removeLast _ = error "TODO complex options"+        -- TODO When containers is >0.5.8 replace these+        slookup i s | i < S.length s = Just $ S.index s i+                    | otherwise      = Nothing+        sdeleteAt i s | i < S.length s = S.take i s >< S.drop (i + 1) s+                      | otherwise      = s++-- | Merge two commands with options between+options :: Matplotlib -> Matplotlib+options l = optFn (\o -> renderOptions o) l++infixl 6 ##+-- | A combinator like '#' that also inserts an option+(##) :: MplotValue val => Matplotlib -> val -> Matplotlib+m ## v = options m # v++-- | An internal helper to convert a list of options to the python code that+-- applies those options in a call.+renderOptions :: [Option] -> [Char]+renderOptions [] = ""+renderOptions xs = f xs+  where  f (P a:l) = "," ++ toPython a ++ f l+         f (K a b:l) = "," ++ toPython a ++  "=" ++ toPython b ++ f l+         f [] = ""++-- | An internal helper that modifies the options of a plot.+optionFn :: ([Option] -> [Option]) -> Matplotlib -> Matplotlib+optionFn f m = case mpPendingOption m of+                 (Just cmd) -> m { mpPendingOption = Just (\os -> cmd $ f os) }+                 Nothing -> error "Can't apply an option to a non-option command"++-- | Apply a list of options to a plot resolving any pending options.+option :: Matplotlib -> [Option] -> Matplotlib+option m os = resolvePending $ optionFn (\os' -> os ++ os') m++infixl 6 @@+-- | A combinator for 'option' that applies a list of options to a plot+(@@) :: Matplotlib -> [Option] -> Matplotlib+m @@ os = option m os++-- | Bind a list of default options to a plot. Positional options are kept in+-- order and default that way as well. Keyword arguments are+def :: Matplotlib -> [Option] -> Matplotlib+def m os = optionFn (defFn os) m++defFn :: [Option] -> [Option] -> [Option]+defFn os os' = merge ps' ps ++ (nub $ ks' ++ ks)+           where isK (K _ _) = True+                 isK _ = False+                 isP (P _) = True+                 isP _ = False+                 ps  = filter isP os+                 ps' = filter isP os'+                 ks  = filter isK os+                 ks' = filter isK os'+                 merge l []  = l+                 merge [] l' = l'+                 merge (x:l) (_:l') = (x : merge l l')++-- $ Python operations++-- | Run python given a code string.+python :: Foldable t => t String -> IO (Either String String)+python codeStr =+  catch (withSystemTempFile "code.py"+         (\codeFile codeHandle -> do+             forM_ codeStr (hPutStrLn codeHandle)+             hClose codeHandle+             Right <$> readProcess "/usr/bin/python3" [codeFile] ""))+         (\e -> return $ Left $ show (e :: IOException))++-- | The standard python includes of every plot+pyIncludes :: [[Char]]+pyIncludes = ["import matplotlib"+             -- TODO Provide a way to set the render backend+             -- ,"matplotlib.use('GtkAgg')"+             ,"import matplotlib.path as mpath"+             ,"import matplotlib.patches as mpatches"+             ,"import matplotlib.pyplot as plot"+             ,"import matplotlib.mlab as mlab"+             ,"from matplotlib import cm"+             ,"from mpl_toolkits.mplot3d import axes3d"+             ,"import numpy as np"+             ,"import os"+             ,"import sys"+             ,"import json"+             ,"import random, datetime"+             ,"from matplotlib.dates import DateFormatter, WeekdayLocator"]++-- | The python command that reads external data into the python data array+pyReadData :: [Char] -> [[Char]]+pyReadData filename = ["data = json.loads(open('" ++ filename ++ "').read())"]++-- | Detach python so we don't block (TODO This isn't working reliably)+pyDetach :: [[Char]]+pyDetach = ["pid = os.fork()"+           ,"if(pid != 0):"+           ,"  exit(0)"]++-- | Python code to show a plot+pyOnscreen :: [[Char]]+pyOnscreen = ["plot.draw()"+             ,"plot.show()"]++-- | Python code that saves a figure+pyFigure :: [Char] -> [[Char]]+pyFigure output = ["plot.savefig('" ++ output ++ "')"]++-- | Create a positional option+o1 x = P x++-- | Create a keyword option+o2 x y = K x y
test/Spec.hs view
@@ -1,3 +1,5 @@+{-# language ExtendedDefaultRules #-}+ import Test.Tasty import Test.Tasty.HUnit import System.IO.Unsafe@@ -31,9 +33,10 @@   , testPlot "line-function" m9   , testPlot "quadratic" m10   , testPlot "projections" m11+  , testPlot "line-options" m12   ] -xs = [-0.54571992 :: Double,  1.48409716, -0.57545561,  2.13058156, -0.75740497,+xs = [-0.54571992,  1.48409716, -0.57545561,  2.13058156, -0.75740497,       -1.27879086, -0.96008858, -1.65482373, -1.69086194, -1.41925464,        0.68144401,  1.44847131,  1.12124327,  1.32056244, -0.4555279 ,        1.96002923, -1.34810771,  0.01513306,  1.25298883, -1.07541677,@@ -53,7 +56,7 @@       -1.50321042,  0.03949817,  0.35286613,  1.86994544,  1.16249707,        0.57421291,  1.21151469,  1.74863421,  0.42287859, -1.22785845,       -0.61650528,  1.76743253, -0.45818694, -1.16560907,  0.0677502 ]-ys = [ 1.28231455 :: Double,  1.13480471,  0.57738712,  0.10268954,  1.00162163,+ys = [ 1.28231455,  1.13480471,  0.57738712,  0.10268954,  1.00162163,       -0.85453571, -1.61049343,  1.33194242,  0.12054943, -0.56568776,        2.11550009,  0.03663454,  0.24889313,  0.85458325,  0.77326592,        0.58815223, -0.79997005,  0.54979418,  0.47711544,  0.73004143,@@ -75,35 +78,38 @@       -0.8023867 , -2.81354854,  0.39553427, -0.22235586, -1.34302011]  m1 :: Matplotlib-m1 = histogram xs (8 :: Int) []+m1 = histogram xs 8  m2 :: Matplotlib-m2 = histogram xs (8 :: Int) [K "cumulative" "True"]+m2 = histogram xs 8 @@ [o2 "cumulative" "True"] -m3 = scatter xs ys []+m3 = scatter xs ys  degreesRadians a = a * pi / 180.0 radiansDegrees a = a * 180.0 / pi  m4 = contourF (\a b -> sin (degreesRadians a) + cos (degreesRadians b)) (-100) 100 (-200) 200 10 -m5 = histogram xs (7 :: Int) []+m5 = histogram xs 7    % yLabel "number of queries"    % xLabel "true positives"  m6 = subplotBarsLabelled-       [[(40 :: Double), 50, 20, 50], [10, 20, 30, 40], [40, 50, 20, 50]]+       [[40, 50, 20, 50], [10, 20, 30, 40], [40, 50, 20, 50]]        ["a", "b", "c", "d"] []-      % title "Wee a title" []+      % title "Wee a title"       % xLabel "X"       % yLabel "Y" -m7 = densityBandwidth [2.1, 1.3, 0.4, 1.9, 5.1, 6.2] 1.5 (Just (-6, 11)) % ylim (0::Double) (0.2::Double)+m7 = densityBandwidth [2.1, 1.3, 0.4, 1.9, 5.1, 6.2] 1.5 (Just (-6, 11))+  % ylim 0 0.2  m8 = density [2.1, 1.3, 0.4, 1.9, 5.1, 6.2] (Just (-6, 11)) -m9 = lineF (\x -> x**2) ([0,0.01..1] :: [Double]) []+m9 = lineF (\x -> x**2) [0,0.01..1] -m10 = plotMapLinear (\x -> x**2) (-2) 2 100 [B "'.'"] % title "Quadratic function" []+m10 = plotMapLinear (\x -> x**2) (-2) 2 100 @@ [o1 "'.'"] % title "Quadratic function"  m11 = projectionsF (\a b -> cos (degreesRadians a) + sin (degreesRadians b)) (-100) 100 (-200) 200 10++m12 = plot [1,2,3,4,5,6] [1,3,2,5,2,4] @@ [o1 "'go-'", o2 "linewidth" "2"]