├── .github └── workflows │ └── CI.yaml ├── .gitignore ├── LICENSE ├── README.md ├── Setup.hs ├── data └── heightmap.png ├── default.nix ├── generate-table.py ├── imgs └── contour.png ├── imgur.sh ├── matplotlib.cabal ├── nix ├── sources.json └── sources.nix ├── shell.nix ├── src └── Graphics │ ├── Matplotlib.hs │ └── Matplotlib │ └── Internal.hs ├── stack.yaml ├── stack.yaml.lock └── test └── Spec.hs /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | workflow_dispatch: 6 | jobs: 7 | nix-build: 8 | name: Nix build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: cachix/install-nix-action@v14 13 | with: 14 | extra_nix_config: | 15 | substituters = https://hydra.iohk.io https://cache.nixos.org/ file://$HOME/nix.store 16 | trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= 17 | require-sigs = false 18 | - uses: cachix/cachix-action@v10 19 | with: 20 | name: matplotlib-haskell 21 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 22 | - name: Build dependencies 23 | run: nix-build shell.nix 24 | - name: Build package 25 | run: nix-build -A matplotlib.components.tests 26 | - name: Build development tools 27 | run: stack --nix test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .stack-work 3 | dist 4 | dist-* 5 | cabal-dev 6 | *.o 7 | *.hi 8 | *.hie 9 | *.chi 10 | *.chs.h 11 | *.dyn_o 12 | *.dyn_hi 13 | .hpc 14 | .hsenv 15 | .cabal-sandbox/ 16 | cabal.sandbox.config 17 | *.prof 18 | *.aux 19 | *.hp 20 | *.eventlog 21 | .stack-work/ 22 | cabal.project.local 23 | cabal.project.local~ 24 | .HTF/ 25 | .ghc.environment.* 26 | result/ 27 | *.sqlite* 28 | notes* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Author name here (c) 2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Author name here nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![matplotlib contour plot](https://github.com/abarbu/matplotlib-haskell/raw/master/imgs/contour.png) 2 | 3 | # Matplotlib for Haskell 4 | 5 | [![Build Status](https://github.com/abarbu/matplotlib-haskell/actions/workflows/CI.yaml/badge.svg)](https://github.com/abarbu/matplotlib-haskell/actions/workflows/CI.yaml) 6 | [![Hackage](https://img.shields.io/hackage/v/matplotlib.svg)](https://hackage.haskell.org/package/matplotlib) 7 | 8 | Haskell bindings to Python's Matplotlib. It's high time that Haskell had a 9 | fully-fledged plotting library! Examples below. 10 | 11 | [Documentation is available on Hackage](https://hackage.haskell.org/package/matplotlib). 12 | 13 | In GHCi: 14 | 15 | ```haskell 16 | :set -XExtendedDefaultRules 17 | import Graphics.Matplotlib 18 | onscreen $ contourF (\a b -> sin (a*pi/180.0) + cos (b*pi/180.0)) (-100) 100 (-200) 200 10 19 | ``` 20 | 21 | Or in a standalone file 22 | 23 | ```haskell 24 | {-# LANGUAGE ExtendedDefaultRules #-} 25 | 26 | import Graphics.Matplotlib 27 | 28 | main = onscreen $ contourF (\a b -> sin (a*pi/180.0) + cos (degreesRadians b)) (-100) 100 (-200) 200 10 29 | ``` 30 | 31 | We need `-XExtendedDefaultRules` to avoid having to manually having to specify certain types. 32 | 33 | ### Installation 34 | 35 | You will need several python libraries to run this code which can be installed 36 | on Ubuntu machines with the following command: 37 | 38 | ```bash 39 | sudo apt-get install -y python3 python3-pip python3-matplotlib python3-numpy python3-tk python-mpltoolkits.basemap python3-scipy dvipng 40 | ``` 41 | 42 | If you're using conda 43 | 44 | ```bash 45 | conda install -y matplotlib scipy tk 46 | ``` 47 | 48 | If you have instructions for other machines or OSes let me know. We require the 49 | `python3` binary to be available somewhere in the PATH. We run with 50 | `env python3`. 51 | 52 | Once you have the prerequisites you can install using the standard incantation 53 | 54 | ```bash 55 | stack install matplotlib 56 | ``` 57 | 58 | If you use LaTeX markup you will need the requisite packages 59 | 60 | ```bash 61 | sudo apt-get install -y texlive-full 62 | ``` 63 | 64 | Or with conda 65 | 66 | ```bash 67 | conda install -y -c conda-forge texlive-core 68 | ``` 69 | 70 | ### Examples 71 | 72 | Click on any of the examples below to go to the corresponding test that 73 | generates it. Depending on your matplotlib version default colors might be 74 | different. 75 | 76 | [![integral][img_integral]][url_integral] 77 | [![griddata][img_griddata]][url_griddata] 78 | [![streamplot][img_streamplot]][url_streamplot] 79 | [![hist2DLog][img_hist2DLog]][url_hist2DLog] 80 | [![quadratic][img_quadratic]][url_quadratic] 81 | [![spines][img_spines]][url_spines] 82 | [![annotation][img_annotation]][url_annotation] 83 | [![corr][img_corr]][url_corr] 84 | [![bivariateNormal][img_bivariateNormal]][url_bivariateNormal] 85 | [![images][img_images]][url_images] 86 | [![labelled-histogram][img_labelled-histogram]][url_labelled-histogram] 87 | [![projections][img_projections]][url_projections] 88 | [![histogram][img_histogram]][url_histogram] 89 | [![pcolorlog][img_pcolorlog]][url_pcolorlog] 90 | [![scatter][img_scatter]][url_scatter] 91 | [![stacked][img_stacked]][url_stacked] 92 | [![legend][img_legend]][url_legend] 93 | [![errorbar][img_errorbar]][url_errorbar] 94 | [![line-options][img_line-options]][url_line-options] 95 | [![quiver-fancy][img_quiver-fancy]][url_quiver-fancy] 96 | [![contour][img_contour]][url_contour] 97 | [![boxplot][img_boxplot]][url_boxplot] 98 | [![show-matrix][img_show-matrix]][url_show-matrix] 99 | [![scatterhist][img_scatterhist]][url_scatterhist] 100 | [![hinton][img_hinton]][url_hinton] 101 | [![density][img_density]][url_density] 102 | [![violinplot][img_violinplot]][url_violinplot] 103 | [![histMulti][img_histMulti]][url_histMulti] 104 | [![cumulative][img_cumulative]][url_cumulative] 105 | [![polar][img_polar]][url_polar] 106 | [![hists][img_hists]][url_hists] 107 | [![tex][img_tex]][url_tex] 108 | [![eventplot][img_eventplot]][url_eventplot] 109 | [![line-function][img_line-function]][url_line-function] 110 | [![density-bandwidth][img_density-bandwidth]][url_density-bandwidth] 111 | [![quiver][img_quiver]][url_quiver] 112 | [![pie][img_pie]][url_pie] 113 | 114 | [img_violinplot]: https://i.imgur.com/iBOfnuL.png "violinplot" 115 | [url_violinplot]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L299 116 | [img_contour]: https://i.imgur.com/KoAIf9Z.png "contour" 117 | [url_contour]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L223 118 | [img_tex]: https://i.imgur.com/bR8r579.png "tex" 119 | [url_tex]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L258 120 | [img_scatterhist]: https://i.imgur.com/9ZIVotE.png "scatterhist" 121 | [url_scatterhist]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L306 122 | [img_line-options]: https://i.imgur.com/Fahp7QA.png "line-options" 123 | [url_line-options]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L248 124 | [img_griddata]: https://i.imgur.com/SH83pJK.png "griddata" 125 | [url_griddata]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L603 126 | [img_pcolorlog]: https://i.imgur.com/ZLUoUqy.png "pcolorlog" 127 | [url_pcolorlog]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L496 128 | [img_cumulative]: https://i.imgur.com/u5I8NYF.png "cumulative" 129 | [url_cumulative]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L216 130 | [img_annotation]: https://i.imgur.com/9tdHiaT.png "annotation" 131 | [url_annotation]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L540 132 | [img_density]: https://i.imgur.com/KS2OhbH.png "density" 133 | [url_density]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L240 134 | [img_line-function]: https://i.imgur.com/zkpfQqW.png "line-function" 135 | [url_line-function]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L242 136 | [img_boxplot]: https://i.imgur.com/KigvYSc.png "boxplot" 137 | [url_boxplot]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L293 138 | [img_show-matrix]: https://i.imgur.com/ajY0A9l.png "show-matrix" 139 | [url_show-matrix]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L268 140 | [img_histMulti]: https://i.imgur.com/FxEI3EI.png "histMulti" 141 | [url_histMulti]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L335 142 | [img_streamplot]: https://i.imgur.com/IfHLmkC.png "streamplot" 143 | [url_streamplot]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L583 144 | [img_pie]: https://i.imgur.com/ljgWXf6.png "pie" 145 | [url_pie]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L517 146 | [img_corr]: https://i.imgur.com/GnBpDJL.png "corr" 147 | [url_corr]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L253 148 | [img_projections]: https://i.imgur.com/IlK7Oy3.png "projections" 149 | [url_projections]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L246 150 | [img_scatter]: https://i.imgur.com/dceKS4I.png "scatter" 151 | [url_scatter]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L218 152 | [img_legend]: https://i.imgur.com/X46KiUJ.png "legend" 153 | [url_legend]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L271 154 | [img_density-bandwidth]: https://i.imgur.com/Qgjvrox.png "density-bandwidth" 155 | [url_density-bandwidth]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L237 156 | [img_bivariateNormal]: https://i.imgur.com/fTSfEzo.png "bivariateNormal" 157 | [url_bivariateNormal]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L466 158 | [img_hinton]: https://i.imgur.com/m9a4IwL.png "hinton" 159 | [url_hinton]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L381 160 | [img_quadratic]: https://i.imgur.com/E4AafPD.png "quadratic" 161 | [url_quadratic]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L244 162 | [img_histogram]: https://i.imgur.com/X37Rmy4.png "histogram" 163 | [url_histogram]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L213 164 | [img_polar]: https://i.imgur.com/4DAOrF1.png "polar" 165 | [url_polar]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L451 166 | [img_quiver]: https://i.imgur.com/TcayDLc.png "quiver" 167 | [url_quiver]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L424 168 | [img_quiver-fancy]: https://i.imgur.com/NsOFHhx.png "quiver-fancy" 169 | [url_quiver-fancy]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L435 170 | [img_stacked]: https://i.imgur.com/rWIyizX.png "stacked" 171 | [url_stacked]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L524 172 | [img_spines]: https://i.imgur.com/BryQOY9.png "spines" 173 | [url_spines]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L353 174 | [img_hist2DLog]: https://i.imgur.com/2fL8oEX.png "hist2DLog" 175 | [url_hist2DLog]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L276 176 | [img_integral]: https://i.imgur.com/PkepIKR.png "integral" 177 | [url_integral]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L397 178 | [img_errorbar]: https://i.imgur.com/gi0zEiz.png "errorbar" 179 | [url_errorbar]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L288 180 | [img_labelled-histogram]: https://i.imgur.com/lCVEpge.png "labelled-histogram" 181 | [url_labelled-histogram]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L225 182 | [img_eventplot]: https://i.imgur.com/UMT1yku.png "eventplot" 183 | [url_eventplot]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L281 184 | [img_hists]: https://i.imgur.com/KurE2Sr.png "hists" 185 | [url_hists]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L372 186 | [img_images]: https://i.imgur.com/R1fhDXC.png "images" 187 | [url_images]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L483 188 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /data/heightmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abarbu/matplotlib-haskell/f85bb16bffc746d62b22c1e30c9c75501af5d81c/data/heightmap.png -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | sources = import nix/sources.nix {}; 3 | haskell-nix = (import sources."haskell.nix" {}); 4 | nixpkgs = haskell-nix.pkgs; 5 | gitignore = (import sources."gitignore.nix" { 6 | inherit (nixpkgs) lib; 7 | }).gitignoreSource; 8 | 9 | src = nixpkgs.lib.cleanSourceWith { 10 | name = "matplotlib"; 11 | src = gitignore ./.; 12 | }; 13 | in 14 | nixpkgs.haskell-nix.stackProject { 15 | inherit src; 16 | modules = [({pkgs, ...}: { 17 | packages.matplotlib.components.library.build-tools = 18 | [ pkgs.buildPackages.python39Packages.matplotlib 19 | pkgs.buildPackages.python39Packages.scipy 20 | pkgs.buildPackages.texlive.combined.scheme-small ]; 21 | packages.matplotlib.components.tests.matplotlib-test.build-tools = 22 | [ pkgs.buildPackages.python39Packages.matplotlib 23 | pkgs.buildPackages.python39Packages.scipy 24 | pkgs.buildPackages.texlive.combined.scheme-small ]; 25 | doHaddock = false; 26 | })]; 27 | } 28 | -------------------------------------------------------------------------------- /generate-table.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Upload data to imgur if it's changed. 3 | 4 | Usage: 5 | generate-table.py 6 | """ 7 | from docopt import docopt 8 | from collections import defaultdict 9 | import os 10 | import json 11 | import subprocess 12 | 13 | def gen(name, md5): 14 | subprocess.check_output(["convert", name, "-resize", "x200", "-unsharp", "0x1", "/tmp/a.png"]) 15 | r = subprocess.check_output(["./imgur.sh", "/tmp/a.png"]).strip().split() 16 | return {'md5': md5, 'link': r[0].decode("utf-8"), 'deleteLink': r[3].decode("utf-8")} 17 | 18 | def doit(): 19 | data = json.loads(open('uploaded-figures.json').read()) 20 | for img in os.listdir("imgs"): 21 | img = 'imgs/' +img 22 | md5 = subprocess.check_output(["/usr/bin/md5sum", img]).strip().split()[0].decode("utf-8") 23 | if img in data and data[img]['md5'] == md5: 24 | pass 25 | else: 26 | data[img] = gen(img, md5) 27 | print(data[img]) 28 | json.dump(data, open('uploaded-figures.json', 'w')) 29 | 30 | def links(): 31 | data = json.loads(open('uploaded-figures.json').read()) 32 | for k, v in data.items(): 33 | label = os.path.splitext(os.path.basename(k))[0] 34 | print('[img_%s]: %s "%s"' % (label, v['link'], label)) 35 | fnName = subprocess.check_output(["/bin/grep", "-n", '"' + label + '"', "test/Spec.hs"]).split()[4].decode("utf-8") 36 | line = subprocess.check_output(["/bin/grep", "-ne", "^" + fnName + ' ', "test/Spec.hs"]).decode("utf-8").split(':')[0] 37 | print("[url_%s]: https://github.com/abarbu/matplotlib-haskell/blob/master/test/Spec.hs#L%s" % (label, line)) 38 | for k, v in data.items(): 39 | label = os.path.splitext(os.path.basename(k))[0] 40 | print("[![%s][img_%s]][url_%s]" % (label, label, label)) 41 | # "| %s | %s | %s |" 42 | # | col 2 is | centered | $12 | 43 | # | zebra stripes | are neat | $1 | 44 | 45 | # [You can use numbers for reference-style link definitions][1] 46 | # ![alt text][logo] 47 | # [![alt text][logo]][1] 48 | # '[1]: http://slashdot.org' 49 | # '[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"' 50 | 51 | if __name__ == '__main__': 52 | arguments = docopt(__doc__, version='0.1') 53 | doit() 54 | links() 55 | -------------------------------------------------------------------------------- /imgs/contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abarbu/matplotlib-haskell/f85bb16bffc746d62b22c1e30c9c75501af5d81c/imgs/contour.png -------------------------------------------------------------------------------- /imgur.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Imgur script by Bart Nagel 4 | # Improvements by Tino Sino 5 | # Version 6 or more 6 | # I release this into the public domain. Do with it what you will. 7 | # The latest version can be found at https://github.com/tremby/imgur.sh 8 | 9 | # API Key provided by Bart; 10 | # replace with your own or specify yours as IMGUR_CLIENT_ID envionment variable 11 | # to avoid limits 12 | default_client_id=c9a6efb3d7932fd 13 | client_id="${IMGUR_CLIENT_ID:=$default_client_id}" 14 | 15 | # Function to output usage instructions 16 | function usage { 17 | echo "Usage: $(basename $0) [ [...]]" >&2 18 | echo 19 | echo "Upload images to imgur and output their new URLs to stdout. Each one's" >&2 20 | echo "delete page is output to stderr between the view URLs." >&2 21 | echo 22 | echo "A filename can be - to read from stdin. If no filename is given, stdin is read." >&2 23 | echo 24 | echo "If xsel, xclip, or pbcopy is available, the URLs are put on the X selection for" >&2 25 | echo "easy pasting." >&2 26 | } 27 | 28 | # Function to upload a path 29 | # First argument should be a content spec understood by curl's -F option 30 | function upload { 31 | curl -s -H "Authorization: Client-ID $client_id" -H "Expect: " -F "image=$1" https://api.imgur.com/3/image.xml 32 | # The "Expect: " header is to get around a problem when using this through 33 | # the Squid proxy. Not sure if it's a Squid bug or what. 34 | } 35 | 36 | # Check arguments 37 | if [ "$1" == "-h" -o "$1" == "--help" ]; then 38 | usage 39 | exit 0 40 | elif [ $# -eq 0 ]; then 41 | echo "No file specified; reading from stdin" >&2 42 | exec "$0" - 43 | fi 44 | 45 | # Check curl is available 46 | type curl &>/dev/null || { 47 | echo "Couldn't find curl, which is required." >&2 48 | exit 17 49 | } 50 | 51 | clip="" 52 | errors=false 53 | 54 | # Loop through arguments 55 | while [ $# -gt 0 ]; do 56 | file="$1" 57 | shift 58 | 59 | # Upload the image 60 | if [[ "$file" =~ ^https?:// ]]; then 61 | # URL -> imgur 62 | response=$(upload "$file") 2>/dev/null 63 | else 64 | # File -> imgur 65 | # Check file exists 66 | if [ "$file" != "-" -a ! -f "$file" ]; then 67 | echo "File '$file' doesn't exist; skipping" >&2 68 | errors=true 69 | continue 70 | fi 71 | response=$(upload "@$file") 2>/dev/null 72 | fi 73 | 74 | if [ $? -ne 0 ]; then 75 | echo "Upload failed" >&2 76 | errors=true 77 | continue 78 | elif echo "$response" | grep -q 'success="0"'; then 79 | echo "Error message from imgur:" >&2 80 | msg="${response##*}" 81 | echo "${msg%%*}" >&2 82 | errors=true 83 | continue 84 | fi 85 | 86 | # Parse the response and output our stuff 87 | url="${response##*}" 88 | url="${url%%*}" 89 | delete_hash="${response##*}" 90 | delete_hash="${delete_hash%%*}" 91 | echo $url | sed 's/^http:/https:/' 92 | echo "Delete page: https://imgur.com/delete/$delete_hash" 93 | 94 | # Append the URL to a string so we can put them all on the clipboard later 95 | clip+="$url" 96 | if [ $# -gt 0 ]; then 97 | clip+=$'\n' 98 | fi 99 | done 100 | 101 | # Put the URLs on the clipboard if we can 102 | if type pbcopy &>/dev/null; then 103 | echo -n "$clip" | pbcopy 104 | elif [ $DISPLAY ]; then 105 | if type xsel &>/dev/null; then 106 | echo -n "$clip" | xsel 107 | elif type xclip &>/dev/null; then 108 | echo -n "$clip" | xclip 109 | else 110 | echo "Haven't copied to the clipboard: no xsel or xclip" >&2 111 | fi 112 | else 113 | echo "Haven't copied to the clipboard: no \$DISPLAY or pbcopy" >&2 114 | fi 115 | 116 | if $errors; then 117 | exit 1 118 | fi 119 | -------------------------------------------------------------------------------- /matplotlib.cabal: -------------------------------------------------------------------------------- 1 | name: matplotlib 2 | version: 0.7.7 3 | synopsis: Bindings to Matplotlib; a Python plotting library 4 | description: 5 | Matplotlib is probably the most full featured plotting library out there. 6 | These bindings provide a quick, easy, and extensible way to use it in 7 | Haskell. 8 | . 9 | <> 10 | . 11 | > onscreen $ contourF (\a b -> sin (a*pi/180.0) + cos (b*pi/180.0)) (-100) 100 (-200) 200 10 12 | homepage: https://github.com/abarbu/matplotlib-haskell 13 | license: BSD3 14 | license-file: LICENSE 15 | author: Andrei Barbu 16 | maintainer: andrei@0xab.com 17 | copyright: 2017 Andrei Barbu 18 | category: Graphics 19 | build-type: Simple 20 | extra-source-files: README.md imgs/contour.png 21 | cabal-version: >=1.10 22 | 23 | library 24 | hs-source-dirs: src 25 | exposed-modules: Graphics.Matplotlib.Internal 26 | , Graphics.Matplotlib 27 | build-depends: base >= 4.7 && < 5 28 | , deepseq 29 | , process 30 | , bytestring 31 | , aeson 32 | , temporary 33 | , containers 34 | , split 35 | , filepath 36 | default-language: Haskell2010 37 | 38 | test-suite matplotlib-test 39 | type: exitcode-stdio-1.0 40 | hs-source-dirs: test 41 | main-is: Spec.hs 42 | build-depends: base 43 | , matplotlib 44 | , tasty 45 | , tasty-hunit 46 | , tasty-expected-failure 47 | , tasty-golden 48 | , directory 49 | , bytestring 50 | , process 51 | , temporary 52 | , random 53 | , raw-strings-qq 54 | , split 55 | , ad 56 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 57 | default-language: Haskell2010 58 | 59 | source-repository head 60 | type: git 61 | location: https://github.com/abarbu/matplotlib-haskell 62 | -------------------------------------------------------------------------------- /nix/sources.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitignore.nix": { 3 | "branch": "master", 4 | "description": "Nix functions for filtering local git sources", 5 | "homepage": "", 6 | "owner": "hercules-ci", 7 | "repo": "gitignore", 8 | "rev": "80463148cd97eebacf80ba68cf0043598f0d7438", 9 | "sha256": "1l34rmh4lf4w8a1r8vsvkmg32l1chl0p593fl12r28xx83vn150v", 10 | "type": "tarball", 11 | "url": "https://github.com/hercules-ci/gitignore/archive/80463148cd97eebacf80ba68cf0043598f0d7438.tar.gz", 12 | "url_template": "https://github.com///archive/.tar.gz" 13 | }, 14 | "haskell.nix": { 15 | "branch": "master", 16 | "description": "Alternative Haskell Infrastructure for Nixpkgs", 17 | "homepage": "https://input-output-hk.github.io/haskell.nix", 18 | "owner": "input-output-hk", 19 | "repo": "haskell.nix", 20 | "rev": "89a69afd820506f6032cd805bc18e127c2af47a5", 21 | "sha256": "0qr5wlypvxwqy8kqd7524xdbqcd9s47rhnpvsa2wf60jrs4axbb9", 22 | "type": "tarball", 23 | "url": "https://github.com/input-output-hk/haskell.nix/archive/89a69afd820506f6032cd805bc18e127c2af47a5.tar.gz", 24 | "url_template": "https://github.com///archive/.tar.gz" 25 | }, 26 | "niv": { 27 | "branch": "master", 28 | "description": "Easy dependency management for Nix projects", 29 | "homepage": "https://github.com/nmattia/niv", 30 | "owner": "nmattia", 31 | "repo": "niv", 32 | "rev": "65a61b147f307d24bfd0a5cd56ce7d7b7cc61d2e", 33 | "sha256": "17mirpsx5wyw262fpsd6n6m47jcgw8k2bwcp1iwdnrlzy4dhcgqh", 34 | "type": "tarball", 35 | "url": "https://github.com/nmattia/niv/archive/65a61b147f307d24bfd0a5cd56ce7d7b7cc61d2e.tar.gz", 36 | "url_template": "https://github.com///archive/.tar.gz" 37 | }, 38 | "nixpkgs": { 39 | "branch": "release-20.03", 40 | "description": "Nix Packages collection", 41 | "homepage": "", 42 | "owner": "NixOS", 43 | "repo": "nixpkgs", 44 | "rev": "eb73405ecceb1dc505b7cbbd234f8f94165e2696", 45 | "sha256": "06k21wbyhhvq2f1xczszh3c2934p0m02by3l2ixvd6nkwrqklax7", 46 | "type": "tarball", 47 | "url": "https://github.com/NixOS/nixpkgs/archive/eb73405ecceb1dc505b7cbbd234f8f94165e2696.tar.gz", 48 | "url_template": "https://github.com///archive/.tar.gz" 49 | }, 50 | "stackage.nix": { 51 | "branch": "master", 52 | "description": "Automatically generated Nix expressions of Stackage snapshots", 53 | "homepage": "", 54 | "owner": "input-output-hk", 55 | "repo": "stackage.nix", 56 | "rev": "1301f5d364ed6c704103a558e49b08b63096b810", 57 | "sha256": "0l7wslsm8ipci2bsc7j87wa7f9qf5an4qpp4s15i60ij5lfyphvk", 58 | "type": "tarball", 59 | "url": "https://github.com/input-output-hk/stackage.nix/archive/1301f5d364ed6c704103a558e49b08b63096b810.tar.gz", 60 | "url_template": "https://github.com///archive/.tar.gz" 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /nix/sources.nix: -------------------------------------------------------------------------------- 1 | # This file has been generated by Niv. 2 | 3 | let 4 | 5 | # 6 | # The fetchers. fetch_ fetches specs of type . 7 | # 8 | 9 | fetch_file = pkgs: name: spec: 10 | let 11 | name' = sanitizeName name + "-src"; 12 | in 13 | if spec.builtin or true then 14 | builtins_fetchurl { inherit (spec) url sha256; name = name'; } 15 | else 16 | pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; 17 | 18 | fetch_tarball = pkgs: name: spec: 19 | let 20 | name' = sanitizeName name + "-src"; 21 | in 22 | if spec.builtin or true then 23 | builtins_fetchTarball { name = name'; inherit (spec) url sha256; } 24 | else 25 | pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; 26 | 27 | fetch_git = name: spec: 28 | let 29 | ref = 30 | if spec ? ref then spec.ref else 31 | if spec ? branch then "refs/heads/${spec.branch}" else 32 | if spec ? tag then "refs/tags/${spec.tag}" else 33 | abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; 34 | in 35 | builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; }; 36 | 37 | fetch_local = spec: spec.path; 38 | 39 | fetch_builtin-tarball = name: throw 40 | ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. 41 | $ niv modify ${name} -a type=tarball -a builtin=true''; 42 | 43 | fetch_builtin-url = name: throw 44 | ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. 45 | $ niv modify ${name} -a type=file -a builtin=true''; 46 | 47 | # 48 | # Various helpers 49 | # 50 | 51 | # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 52 | sanitizeName = name: 53 | ( 54 | concatMapStrings (s: if builtins.isList s then "-" else s) 55 | ( 56 | builtins.split "[^[:alnum:]+._?=-]+" 57 | ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) 58 | ) 59 | ); 60 | 61 | # The set of packages used when specs are fetched using non-builtins. 62 | mkPkgs = sources: system: 63 | let 64 | sourcesNixpkgs = 65 | import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; 66 | hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; 67 | hasThisAsNixpkgsPath = == ./.; 68 | in 69 | if builtins.hasAttr "nixpkgs" sources 70 | then sourcesNixpkgs 71 | else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then 72 | import {} 73 | else 74 | abort 75 | '' 76 | Please specify either (through -I or NIX_PATH=nixpkgs=...) or 77 | add a package called "nixpkgs" to your sources.json. 78 | ''; 79 | 80 | # The actual fetching function. 81 | fetch = pkgs: name: spec: 82 | 83 | if ! builtins.hasAttr "type" spec then 84 | abort "ERROR: niv spec ${name} does not have a 'type' attribute" 85 | else if spec.type == "file" then fetch_file pkgs name spec 86 | else if spec.type == "tarball" then fetch_tarball pkgs name spec 87 | else if spec.type == "git" then fetch_git name spec 88 | else if spec.type == "local" then fetch_local spec 89 | else if spec.type == "builtin-tarball" then fetch_builtin-tarball name 90 | else if spec.type == "builtin-url" then fetch_builtin-url name 91 | else 92 | abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; 93 | 94 | # If the environment variable NIV_OVERRIDE_${name} is set, then use 95 | # the path directly as opposed to the fetched source. 96 | replace = name: drv: 97 | let 98 | saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; 99 | ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; 100 | in 101 | if ersatz == "" then drv else 102 | # this turns the string into an actual Nix path (for both absolute and 103 | # relative paths) 104 | if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; 105 | 106 | # Ports of functions for older nix versions 107 | 108 | # a Nix version of mapAttrs if the built-in doesn't exist 109 | mapAttrs = builtins.mapAttrs or ( 110 | f: set: with builtins; 111 | listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) 112 | ); 113 | 114 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 115 | range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); 116 | 117 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 118 | stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); 119 | 120 | # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 121 | stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); 122 | concatMapStrings = f: list: concatStrings (map f list); 123 | concatStrings = builtins.concatStringsSep ""; 124 | 125 | # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 126 | optionalAttrs = cond: as: if cond then as else {}; 127 | 128 | # fetchTarball version that is compatible between all the versions of Nix 129 | builtins_fetchTarball = { url, name ? null, sha256 }@attrs: 130 | let 131 | inherit (builtins) lessThan nixVersion fetchTarball; 132 | in 133 | if lessThan nixVersion "1.12" then 134 | fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 135 | else 136 | fetchTarball attrs; 137 | 138 | # fetchurl version that is compatible between all the versions of Nix 139 | builtins_fetchurl = { url, name ? null, sha256 }@attrs: 140 | let 141 | inherit (builtins) lessThan nixVersion fetchurl; 142 | in 143 | if lessThan nixVersion "1.12" then 144 | fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) 145 | else 146 | fetchurl attrs; 147 | 148 | # Create the final "sources" from the config 149 | mkSources = config: 150 | mapAttrs ( 151 | name: spec: 152 | if builtins.hasAttr "outPath" spec 153 | then abort 154 | "The values in sources.json should not have an 'outPath' attribute" 155 | else 156 | spec // { outPath = replace name (fetch config.pkgs name spec); } 157 | ) config.sources; 158 | 159 | # The "config" used by the fetchers 160 | mkConfig = 161 | { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null 162 | , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) 163 | , system ? builtins.currentSystem 164 | , pkgs ? mkPkgs sources system 165 | }: rec { 166 | # The sources, i.e. the attribute set of spec name to spec 167 | inherit sources; 168 | 169 | # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers 170 | inherit pkgs; 171 | }; 172 | 173 | in 174 | mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } 175 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ./default.nix).shellFor { 2 | tools = { 3 | cabal = "latest"; 4 | hpack = "latest"; 5 | hlint = "latest"; 6 | ormolu = "latest"; 7 | haskell-language-server = "latest"; 8 | }; 9 | 10 | # Prevents cabal from choosing alternate plans, so that 11 | # *all* dependencies are provided by Nix. 12 | exactDeps = true; 13 | } 14 | -------------------------------------------------------------------------------- /src/Graphics/Matplotlib.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ExtendedDefaultRules #-} 2 | ----------------------------------------------------------------------------- 3 | -- | 4 | -- Matplotlib bindings and an interface to easily bind to new portions of the 5 | -- API. The most essential parts of Matplotlib are wrapped and exposed to 6 | -- Haskell through an interface that allows extenisbility. Code is generated on 7 | -- the fly and Python is called. 8 | -- 9 | -- You should start by looking at the tests, they demonstrate how to create many 10 | -- different types of plots. 11 | -- 12 | -- This is not a very Haskell-ish library. Type safety is non-existent. It's 13 | -- easy to generate incorrect Python code. But in exchange, we can bind to 14 | -- arbitrary matplotlib APIs with ease, so it's also easy to generate correct 15 | -- Python code. 16 | -- 17 | -- The generated code follows a few simple conventions. Data is always loaded 18 | -- into a data variable that is a Python array. Data is transffered via 19 | -- JSON. This data variable is indexed by various rendering commands. 20 | -- 21 | -- Functions which start with the word data operate on the data array, arguments 22 | -- are Python code that should access that array. Most other functions take 23 | -- Haskell objects and load them into Python. 24 | -- 25 | -- This module should expose enough tools so that you can bind any part of the 26 | -- matplotlib API. A binding with options, such as that of 'plot', looks like: 27 | -- 28 | -- @ 29 | -- readData (x, y) 30 | -- % mp \# "p = plot.plot(data[" \# a \# "], data[" \# b \# "]" ## ")" 31 | -- % mp \# "plot.xlabel(" \# str label \# ")" 32 | -- @ 33 | -- 34 | -- Where important functions are: 35 | -- 36 | -- [@'readData'@] Load the given data by serializing it to JSON and place it in a Python array named "data". 37 | -- [@'readImage'@] Load an image from a given path and place it in a Python variable named "img". 38 | -- [@'%'@] Sequence two plots 39 | -- [@'mp'@] Create an empty plot 40 | -- [@'#'@] Append Python code to the last command in a plot 41 | -- [@'##'@] Just like '#' but also adds in a placeholder for an options list 42 | -- [@'bindDefault'@] Set a default in the last options list, keeping it open for additions 43 | -- 44 | -- You can call this plot with 45 | -- 46 | -- > plot [1,2,3,4,5,6] [1,3,2,5,2] @@ [o1 "go-", o2 "linewidth" 2] 47 | -- 48 | -- where '@@' applies an options list replacing the last '##' 49 | -- 50 | -- [@'o1'@] A single positional option. The value is rendered into Python as 51 | -- the appropriate datatype. Strings become Python strings, bools become bools, 52 | -- etc. If you want to insert code verbatim into an option use 'lit'. If you 53 | -- want to have a raw string with no escapes use 'raw'. 54 | -- [@'o2'@] A keyword option. The key is always a string, the value is treated 55 | -- the same way that the option in 'o1' is treated. 56 | -- 57 | -- Right now there's no easy way to bind to an option other than the last one 58 | -- unless you want to pass options in as parameters. 59 | -- 60 | -- The generated Python code should follow some invariants. It must maintain the 61 | -- current figure in "fig", all available axes in "axes", and the current axis 62 | -- in "ax". Plotting commands should use the current axis, never the plot 63 | -- itself; the two APIs are almost identical. When creating low-level bindings 64 | -- one must remember to call "plot.sci" to set the current image when plotting a 65 | -- graph. The current spine of the axes that's being manipulated is in 66 | -- "spine". The current quiver is in "q". 67 | ----------------------------------------------------------------------------- 68 | 69 | module Graphics.Matplotlib 70 | ( module Graphics.Matplotlib 71 | -- * Creating custom plots and applying options 72 | , Matplotlib(), Option(),(@@), (%), o1, o2, (##), (#), mp, bindDefault, readData, readImage 73 | , str, raw, lit, updateAxes, updateFigure, mapLinear) 74 | where 75 | import Data.List 76 | import Data.Aeson 77 | import Graphics.Matplotlib.Internal 78 | import Control.Concurrent(forkIO) 79 | 80 | -- * Running a plot 81 | 82 | -- | Show a plot, blocks until the figure is closed 83 | onscreen :: Matplotlib -> IO () 84 | onscreen m = (forkIO $ (withMplot m (\s -> python $ pyIncludes "" ++ s ++ pyOnscreen) >> return ())) >> return () 85 | 86 | -- | Print the python code that would be executed 87 | code :: Matplotlib -> IO String 88 | code m = withMplot m (\s -> return $ unlines $ pyIncludes (pyBackend "agg") ++ s ++ pyOnscreen) 89 | 90 | -- | Save to a file 91 | file :: [Char] -> Matplotlib -> IO (Either String String) 92 | file filename m = withMplot m (\s -> python $ pyIncludes (pyBackend "agg") ++ s ++ pyFigure filename) 93 | 94 | -- | Get the SVG for a figure 95 | toSvg :: Matplotlib -> IO (Either String String) 96 | toSvg m = withMplot m (\s -> python $ pyIncludes "" ++ s ++ pySVG) 97 | 98 | -- * Useful plots 99 | 100 | -- | Plot the cross-correlation and autocorrelation of several variables. TODO Due to 101 | -- a limitation in the options mechanism this takes explicit options. 102 | xacorr xs ys opts = readData (xs, ys) 103 | % figure 104 | % addSubplot 2 1 1 105 | % xcorr xs ys @@ opts 106 | % grid True 107 | % axhline 0 @@ [o1 0, o2 "color" "black", o2 "lw" 2] 108 | % addSubplot 2 1 2 @@ [o2 "sharex" $ lit "ax"] 109 | % acorr xs @@ opts 110 | % grid True 111 | % axhline 0 @@ [o2 "color" "black", o2 "lw" 2] 112 | 113 | -- | Plot a histogram for the given values with 'bins' 114 | histogram :: (MplotValue val, ToJSON t) => t -> val -> Matplotlib 115 | histogram values bins = readData [values] % dataHistogram 0 bins 116 | 117 | -- | Plot a 2D histogram for the given values with 'bins' 118 | histogram2D x y = readData [x,y] % 119 | mp # "plot.sci(ax.hist2d(data[0], data[1]" ## ")[-1])" 120 | 121 | -- | Plot the given values as a scatter plot 122 | scatter :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib 123 | scatter x y = readData (x, y) 124 | % mp # "plot.sci(ax.scatter(data[0], data[1]" ## "))" 125 | 126 | -- | Create a bar at a position with a height 127 | bar :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib 128 | bar left height = readData (left, height) 129 | % mp # "ax.bar(data[0], data[1]" ## ")" 130 | 131 | -- | Plot a line 132 | line :: (ToJSON t1, ToJSON t) => t1 -> t -> Matplotlib 133 | line x y = plot x y `bindDefault` [o1 "-"] 134 | 135 | -- | Like 'plot' but takes an error bar value per point 136 | -- errorbar :: (ToJSON x, ToJSON y, ToJSON xs, ToJSON ys) => x -> y -> Maybe xs -> Maybe ys -> Matplotlib 137 | errorbar xs ys xerrs yerrs = readData (xs, ys, xerrs, yerrs) 138 | % mp # "ax.errorbar(data[0], data[1], xerr=data[2], yerr=data[3]" ## ")" 139 | 140 | -- | Plot a line given a function that will be executed for each element of 141 | -- given list. The list provides the x values, the function the y values. 142 | lineF :: (ToJSON a, ToJSON b) => (a -> b) -> [a] -> Matplotlib 143 | lineF f l = plot l (map f l) `bindDefault` [o1 "-"] 144 | 145 | -- | Create a box plot for the given data 146 | boxplot d = readData d 147 | % mp # "ax.boxplot(data" ## ")" 148 | 149 | -- | Create a violin plot for the given data 150 | violinplot d = readData d 151 | % mp # "ax.violinplot(data" ## ")" 152 | 153 | -- | Given a grid of x and y values and a number of steps call the given 154 | -- function and plot the 3D contour 155 | contourF :: (ToJSON val, MplotValue val, Ord val) => (Double -> Double -> val) -> Double -> Double -> Double -> Double -> Double -> Matplotlib 156 | contourF f xStart xEnd yStart yEnd steps = contour xs ys zs 157 | where xs = mapLinear (\x -> (mapLinear (\_ -> x) yStart yEnd steps)) xStart xEnd steps 158 | ys = mapLinear (\_ -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps 159 | zs = mapLinear (\x -> (mapLinear (\y -> f x y) yStart yEnd steps)) xStart xEnd steps 160 | 161 | -- | Given a grid of x and y values and a number of steps call the given 162 | -- function and plot the 3D projection 163 | projectionsF :: (ToJSON val, MplotValue val, Ord val) => (Double -> Double -> val) -> Double -> Double -> Double -> Double -> Double -> Matplotlib 164 | projectionsF f xStart xEnd yStart yEnd steps = projections xs ys zs 165 | where xs = mapLinear (\x -> (mapLinear (\_ -> x) yStart yEnd steps)) xStart xEnd steps 166 | ys = mapLinear (\_ -> (mapLinear (\y -> y) yStart yEnd steps)) xStart xEnd steps 167 | zs = mapLinear (\x -> (mapLinear (\y -> f x y) yStart yEnd steps)) xStart xEnd steps 168 | 169 | -- | Plot x against y interpolating with n steps 170 | plotInterpolated :: (MplotValue val, ToJSON t, ToJSON t1) => t1 -> t -> val -> Matplotlib 171 | plotInterpolated x y n = 172 | readData (x, y) 173 | % interpolate 0 1 n 174 | % dataPlot 0 1 `bindDefault` [o1 "-"] 175 | 176 | -- | A handy function to plot a line between two points give a function and a number o steps 177 | plotMapLinear :: ToJSON b => (Double -> b) -> Double -> Double -> Double -> Matplotlib 178 | plotMapLinear f s e n = line xs ys 179 | where xs = mapLinear (\x -> x) s e n 180 | ys = mapLinear (\x -> f x) s e n 181 | 182 | -- | Plot a line between 0 and the length of the array with the given y values 183 | line1 :: (Foldable t, ToJSON (t a)) => t a -> Matplotlib 184 | line1 y = line [0..length y] y 185 | 186 | -- | Plot a matrix 187 | matShow :: ToJSON a => a -> Matplotlib 188 | matShow d = readData d 189 | % (mp # "plot.sci(ax.matshow(data" ## "))") 190 | 191 | -- | Plot an image 192 | imshow :: MplotImage a => a -> Matplotlib 193 | imshow i = readImage i 194 | % (mp # "plot.sci(ax.imshow(img" ## "))") 195 | 196 | -- | Plot a matrix 197 | pcolor :: ToJSON a => a -> Matplotlib 198 | pcolor d = readData d 199 | % (mp # "plot.sci(ax.pcolor(np.array(data)" ## "))") 200 | 201 | -- | Plot a matrix 202 | pcolor3 x y z = readData (x,y,z) 203 | % (mp # "plot.sci(ax.pcolor(np.array(data[0]),np.array(data[1]),np.array(data[2])" ## "))") 204 | 205 | -- | Create a non-uniform image from samples 206 | nonUniformImage x y z = readData (x,y,z) 207 | % mp # "im = mpimg.NonUniformImage(ax" ## ")" 208 | % mp # "im.set_data(data[0], data[1], data[2])" 209 | 210 | -- | Create a pie chart 211 | pie l = readData l 212 | % mp # "plot.pie(" # l ## ")" 213 | 214 | -- | Plot a KDE of the given functions; a good bandwith will be chosen automatically 215 | density :: [Double] -> Maybe (Double, Double) -> Matplotlib 216 | density l maybeStartEnd = 217 | densityBandwidth l (((4 * (variance ** 5)) / (fromIntegral $ 3 * length l)) ** (1 / 5) / 3) maybeStartEnd 218 | where mean = foldl' (+) 0 l / (fromIntegral $ length l) 219 | variance = foldl' (+) 0 (map (\x -> sqr (x - mean)) l) / (fromIntegral $ length l) 220 | sqr x = x * x 221 | 222 | -- * Matplotlib configuration 223 | 224 | -- | Set an rc parameter 225 | rc s = mp # "plot.rc(" # str s ## ")" 226 | 227 | -- | Set an rcParams key-value 228 | setParameter k v = mp # "matplotlib.rcParams["# str k #"] = " # v 229 | 230 | -- | Enable or disable TeX 231 | setTeX :: Bool -> Matplotlib 232 | setTeX b = mp # "plot.rc('text', usetex="# b #")" 233 | 234 | -- * Basic plotting commands 235 | 236 | -- | Plot the 'a' and 'b' entries of the data object 237 | dataPlot :: (MplotValue val, MplotValue val1) => val1 -> val -> Matplotlib 238 | dataPlot a b = mp # "p = ax.plot(data[" # a # "], data[" # b # "]" ## ")" 239 | 240 | -- | Plot the Haskell objects 'x' and 'y' as a line 241 | plot :: (ToJSON t, ToJSON t1) => t1 -> t -> Matplotlib 242 | plot x y = readData (x, y) % dataPlot 0 1 243 | 244 | streamplot x y u v = readData (x, y, u, v) 245 | % mp # "ax.streamplot(np.asarray(data[0]), np.asarray(data[1]), np.asarray(data[2]), np.asarray(data[3])" ## ")" 246 | 247 | -- | Plot x against y where x is a date. 248 | -- xunit is something like 'weeks', yearStart, monthStart, dayStart are an offset to x. 249 | -- TODO This isn't general enough; it's missing some settings about the format. The call is also a mess. 250 | dateLine :: (ToJSON t1, ToJSON t2) => t1 -> t2 -> String -> (Int, Int, Int) -> Matplotlib 251 | dateLine x y xunit (yearStart, monthStart, dayStart) = 252 | readData (x, y) 253 | % mp # "data[0] = [datetime.timedelta("#xunit#"=i) + datetime.datetime("#yearStart#","#monthStart#","#dayStart#") for i in data[0]]" 254 | % dataPlot 0 1 `bindDefault` [o1 "-"] 255 | % mp # "ax.xaxis.set_major_formatter(DateFormatter('%B'))" 256 | % mp # "ax.xaxis.set_minor_locator(WeekdayLocator(byweekday=6))" 257 | 258 | -- | Create a histogram for the 'a' entry of the data array 259 | dataHistogram :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 260 | dataHistogram a bins = mp # "ax.hist(data[" # a # "]," # bins ## ")" 261 | 262 | -- | Create a scatter plot accessing the given fields of the data array 263 | dataScatter :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 264 | dataScatter a b = dataPlot a b `bindDefault` [o1 "."] 265 | 266 | -- | Create a line accessing the given entires of the data array 267 | dataLine :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 268 | dataLine a b = dataPlot a b `bindDefault` [o1 "-"] 269 | 270 | -- | Create a 3D contour 271 | contour xs ys zs = 272 | readData (xs, ys, zs) 273 | % axis3DProjection 274 | % surface 0 1 2 275 | % contourRaw 0 1 2 (maximum2 xs) (maximum2 ys) (minimum2 zs) 276 | % axis3DLabels xs ys zs 277 | 278 | -- | Create a 3D projection 279 | projections xs ys zs = 280 | readData (xs, ys, zs) 281 | % axis3DProjection 282 | % contourRaw 0 1 2 (maximum2 xs) (maximum2 ys) (minimum2 zs) 283 | % axis3DLabels xs ys zs 284 | 285 | -- | Plot a 3D wireframe accessing the given elements of the data array 286 | wireframe :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib 287 | wireframe a b c = mp # "ax.plot_wireframe(np.array(data[" # a # "]), np.array(data[" # b # "]), np.array(data[" # c # "]), rstride=1, cstride=1)" 288 | 289 | -- | Plot a 3D surface accessing the given elements of the data array 290 | surface :: (MplotValue val2, MplotValue val1, MplotValue val) => val2 -> val1 -> val -> Matplotlib 291 | 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)" 292 | 293 | -- | Plot a contour accessing the given elements of the data array 294 | contourRaw :: (MplotValue val1, MplotValue val2, MplotValue val5, 295 | MplotValue val4, MplotValue val3, MplotValue val) => 296 | val5 -> val4 -> val3 -> val2 -> val1 -> val -> Matplotlib 297 | contourRaw a b c maxA maxB minC = 298 | mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='z', offset=" # minC # ")" 299 | % mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='x', offset=-" # maxA # ")" 300 | % mp # "ax.contour(data[" # a # "], data[" # b # "], data[" # c # "], zdir='y', offset=" # maxB #")" 301 | 302 | -- | Draw a bag graph in a subplot 303 | -- TODO Why do we need this? 304 | subplotDataBar a width offset opts = 305 | mp # "ax.bar(np.arange(len(data[" # a # "]))+" # offset # ", data[" # a # "], " # width ## ")" @@ opts 306 | 307 | -- | The default bar with 308 | barDefaultWidth nr = 1.0 / (fromIntegral nr + 1) 309 | 310 | -- | Create a set of labelled bars of a given height 311 | subplotBarsLabelled valuesList labels optsList = 312 | subplotBars valuesList optsList 313 | % axisXTickSpacing (length $ head $ valuesList) (1.0 - barDefaultWidth (length valuesList) / 2.0) 314 | % axisXTickLabels labels 315 | 316 | -- | Create a subplot and a set of labelled bars 317 | -- TODO This is a mess.. 318 | subplotBars valuesList optsList = 319 | readData valuesList 320 | % addSubplot 1 1 1 321 | % (let width = barDefaultWidth (length valuesList) in 322 | foldl1 (%) (zipWith3 (\_ opts i -> subplotDataBar i width (width * i) opts) valuesList optsList [0..])) 323 | 324 | -- | Update the data array to linearly interpolate between array entries 325 | interpolate :: (MplotValue val, MplotValue val2, MplotValue val1) => val2 -> val1 -> val -> Matplotlib 326 | interpolate a b n = 327 | (mp # "data[" # b # "] = mlab.stineman_interp(np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # "),data[" # a # "],data[" # b # "],None)") 328 | % (mp # "data[" # a # "] = np.linspace(data[" # a # "][0],data[" # a # "][-1]," # n # ")") 329 | 330 | -- | Plot a KDE of the given functions with an optional start/end and a bandwidth h 331 | densityBandwidth :: [Double] -> Double -> Maybe (Double, Double) -> Matplotlib 332 | densityBandwidth l h maybeStartEnd = 333 | plotMapLinear f (case maybeStartEnd of 334 | Nothing -> minimum l 335 | (Just (start, _)) -> start) 336 | (case maybeStartEnd of 337 | Nothing -> maximum l 338 | (Just (_, end)) -> end) 339 | 100 340 | where f x = sum (map (\xi -> gaussianPdf x xi h) l) / ((fromIntegral $ length l) * h) 341 | gaussianPdf x mu sigma = exp (- sqr (x - mu) / (2 * sigma)) / sqrt (2 * pi * sigma) 342 | sqr x = x * x 343 | 344 | -- | Plot cross-correlation 345 | xcorr x y = readData (x, y) % mp # "ax.xcorr(data[0], data[1]" ## ")" 346 | 347 | -- | Plot auto-correlation 348 | acorr x = readData x % mp # "ax.acorr(data" ## ")" 349 | 350 | -- | A quiver plot; color is optional and can be nothing 351 | quiver x y u v Nothing = readData(x,y,u,v) 352 | % mp # "q = ax.quiver(data[0], data[1], data[2], data[3]" ## ")" 353 | quiver x y u v (Just c) = readData(x,y,u,v,c) 354 | % mp # "q = ax.quiver(data[0], data[1], data[2], data[3], data[4]" ## ")" 355 | 356 | -- | A key of a given size with a label for a quiver plot 357 | quiverKey x y u label = mp # "ax.quiverkey(q, "#x#", "#y#", "#u#", "#label##")" 358 | 359 | -- | Plot text at a specified location 360 | text x y s = mp # "ax.text(" # x # "," # y # "," # raw s ## ")" 361 | 362 | -- | Add a text to a figure instead of a particular plot 363 | figText x y s = mp # "plot.figtext(" # x # "," # y # "," # raw s ## ")" 364 | 365 | -- | Add an annotation 366 | annotate s = mp # "ax.annotate(" # str s ## ")" 367 | 368 | -- * Layout, axes, and legends 369 | 370 | -- | Square up the aspect ratio of a plot. 371 | setAspect :: Matplotlib 372 | setAspect = mp # "ax.set_aspect(" ## ")" 373 | 374 | -- | Square up the aspect ratio of a plot. 375 | squareAxes :: Matplotlib 376 | squareAxes = mp # "ax.set_aspect('equal')" 377 | 378 | -- | Set the rotation of the labels on the x axis to the given number of degrees 379 | roateAxesLabels :: MplotValue val => val -> Matplotlib 380 | roateAxesLabels degrees = mp # "labels = ax.get_xticklabels()" 381 | % mp # "for label in labels:" 382 | % mp # " label.set_rotation("#degrees#")" 383 | 384 | -- | Set the x labels to be vertical 385 | verticalAxes :: Matplotlib 386 | verticalAxes = mp # "labels = ax.get_xticklabels()" 387 | % mp # "for label in labels:" 388 | % mp # " label.set_rotation('vertical')" 389 | 390 | -- | Set the x scale to be logarithmic 391 | logX :: Matplotlib 392 | logX = mp # "ax.set_xscale('log')" 393 | 394 | -- | Set the y scale to be logarithmic 395 | logY :: Matplotlib 396 | logY = mp # "ax.set_yscale('log')" 397 | 398 | -- | Set limits on the x axis 399 | xlim :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 400 | xlim l u = mp # "ax.set_xlim(" # l # "," # u # ")" 401 | 402 | -- | Set limits on the y axis 403 | ylim :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 404 | ylim l u = mp # "ax.set_ylim(" # l # "," # u # ")" 405 | 406 | -- | Add a horizontal line across the axis 407 | axhline y = mp # "ax.axhline(" # y ## ")" 408 | 409 | -- | Add a vertical line across the axis 410 | axvline x = mp # "ax.axvline(" # x ## ")" 411 | 412 | -- | Insert a legend 413 | legend = mp # "ax.legend(" ## ")" 414 | 415 | -- | Insert a color bar 416 | -- TODO This refers to the plot and not an axis. Might cause trouble with subplots 417 | colorbar = mp # "plot.colorbar(" ## ")" 418 | 419 | -- | Add a title 420 | title :: String -> Matplotlib 421 | title s = mp # "ax.set_title(" # raw s ## ")" 422 | 423 | -- | Show/hide grid lines 424 | grid :: Bool -> Matplotlib 425 | grid t = mp # "ax.grid(" # t # ")" 426 | 427 | -- | Enable 3D projection 428 | axis3DProjection :: Matplotlib 429 | axis3DProjection = mp # "ax = plot.gca() if plot.gca().name == '3d' else plot.subplot(projection='3d')" 430 | 431 | -- | Label and set limits of a set of 3D axis 432 | -- TODO This is a mess, does both more and less than it claims. 433 | axis3DLabels xs ys zs = 434 | mp # "ax.set_xlabel('X')" 435 | % mp # "ax.set_xlim3d(" # minimum2 xs # ", " # maximum2 xs # ")" 436 | % mp # "ax.set_ylabel('Y')" 437 | % mp # "ax.set_ylim3d(" # minimum2 ys # ", " # maximum2 ys # ")" 438 | % mp # "ax.set_zlabel('Z')" 439 | % mp # "ax.set_zlim3d(" # minimum2 zs # ", " # maximum2 zs # ")" 440 | 441 | -- | Add a label to the x axis 442 | xlabel :: String -> Matplotlib 443 | xlabel label = mp # "ax.set_xlabel(" # raw label ## ")" 444 | 445 | -- | Add a label to the y axis 446 | ylabel :: String -> Matplotlib 447 | ylabel label = mp # "ax.set_ylabel(" # raw label ## ")" 448 | 449 | -- | Add a label to the z axis 450 | zlabel :: String -> Matplotlib 451 | zlabel label = mp # "ax.set_zlabel(" # raw label ## ")" 452 | 453 | setSizeInches w h = mp # "fig.set_size_inches(" # w # "," # h # ", forward=True)" 454 | 455 | tightLayout = mp # "fig.tight_layout(" ## ")" 456 | 457 | xkcd = mp # "plot.xkcd()" 458 | 459 | -- * Ticks 460 | 461 | xticks l = mp # "ax.set_xticks(" # l # ")" 462 | yticks l = mp # "ax.set_yticks(" # l # ")" 463 | zticks l = mp # "ax.set_zticks(" # l # ")" 464 | 465 | xtickLabels l = mp # "ax.set_xticklabels(" # l # ")" 466 | ytickLabels l = mp # "ax.set_yticklabels(" # l # ")" 467 | ztickLabels l = mp # "ax.set_zticklabels(" # l # ")" 468 | 469 | -- | Set the spacing of ticks on the x axis 470 | axisXTickSpacing :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 471 | axisXTickSpacing nr width = mp # "ax.set_xticks(np.arange(" # nr # ")+" # width ## ")" 472 | 473 | -- | Set the labels on the x axis 474 | axisXTickLabels :: MplotValue val => val -> Matplotlib 475 | axisXTickLabels labels = mp # "ax.set_xticklabels( (" # labels # ") " ## " )" 476 | 477 | -- | Set the spacing of ticks on the y axis 478 | axisYTickSpacing :: (MplotValue val1, MplotValue val) => val1 -> val -> Matplotlib 479 | axisYTickSpacing nr width = mp # "ax.set_yticks(np.arange(" # nr # ")+" # width ## ")" 480 | 481 | -- | Set the labels on the y axis 482 | axisYTickLabels :: MplotValue val => val -> Matplotlib 483 | axisYTickLabels labels = mp # "ax.set_yticklabels( (" # labels # ") " ## " )" 484 | 485 | axisXTicksPosition p = mp # "ax.xaxis.set_ticks_position('" # p # "')" 486 | axisYTicksPosition p = mp # "ax.yaxis.set_ticks_position('" # p # "')" 487 | 488 | -- * Spines 489 | 490 | spine s = mp # "spine = ax.spines['" # s # "']" 491 | 492 | spineSetBounds l h = mp # "spine.set_bounds(" # l # "," # h # ")" 493 | 494 | spineSetVisible b = mp # "spine.set_visible(" # b # ")" 495 | 496 | spineSetPosition s n = mp # "spine.set_position((" # s # "," # n # "))" 497 | 498 | -- * Subplots 499 | 500 | setAx = mp # "plot.sca(ax) " 501 | 502 | -- | Create a subplot with the coordinates (r,c,f) 503 | addSubplot r c f = mp # "ax = plot.gcf().add_subplot(" # r # c # f ## ")" % updateAxes % setAx 504 | 505 | -- | Access a subplot with the coordinates (r,c,f) 506 | getSubplot r c f = mp # "ax = plot.subplot(" # r # "," # c # "," # f ## ")" % updateAxes % setAx 507 | 508 | -- | Creates subplots and stores them in an internal variable 509 | subplots = mp # "fig, axes = plot.subplots(" ## ")" 510 | % mp # "axes = np.asarray(axes)" 511 | % mp # "axes = axes.flatten()" 512 | % updateAxes % setAx 513 | 514 | -- | Access a subplot 515 | setSubplot s = mp # "ax = axes[" # s # "]" % setAx 516 | 517 | -- | Add axes to a plot 518 | axes = mp # "ax = plot.axes(" ## ")" % updateAxes % setAx 519 | 520 | -- | Add axes to a figure 521 | addAxes = mp # "ax = fig.add_axes(" ## ")" % updateAxes % setAx 522 | 523 | -- | Creates a new figure with the given id. If the Id is already in use it 524 | -- switches to that figure. 525 | figure = mp # "plot.figure(" ## ")" % updateFigure 526 | -------------------------------------------------------------------------------- /src/Graphics/Matplotlib/Internal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances, ScopedTypeVariables, FlexibleContexts, ExtendedDefaultRules, ExistentialQuantification, CPP #-} 2 | -- | 3 | -- Internal representations of the Matplotlib data. These are not API-stable 4 | -- and may change. You can easily extend the provided bindings without relying 5 | -- on the internals exposed here but they are provided just in case. 6 | module Graphics.Matplotlib.Internal where 7 | import System.IO.Temp 8 | import System.Process 9 | import Data.Aeson 10 | import Control.Monad 11 | import Control.DeepSeq 12 | import System.IO 13 | import qualified Data.ByteString.Lazy as B 14 | import Data.List 15 | import Control.Exception 16 | import qualified Data.Sequence as S 17 | import Data.Sequence (Seq, (|>), (><)) 18 | import Data.Maybe 19 | import Data.Monoid 20 | import GHC.Exts(toList) 21 | 22 | -- | A handy miscellaneous function to linearly map over a range of numbers in a given number of steps 23 | mapLinear :: (Double -> b) -> Double -> Double -> Double -> [b] 24 | mapLinear f s e n = map (\v -> f $ s + (v * (e - s) / n)) [0..n] 25 | 26 | -- $ Basics 27 | 28 | -- | The wrapper type for a matplotlib computation. 29 | data Matplotlib = Matplotlib { 30 | mpCommands :: Seq MplotCommand -- ^ Resolved computations that have been transformed to commands 31 | , mpPendingOption :: Maybe ([Option] -> MplotCommand) -- ^ A pending computation that is affected by applied options 32 | , mpRest :: Seq MplotCommand -- ^ Computations that follow the one that is pending 33 | } 34 | 35 | -- | Monoid instance for Matplotlib type 36 | instance Monoid Matplotlib where 37 | mempty = mp 38 | #if !MIN_VERSION_base(4,11,0) 39 | mappend = (%) 40 | #else 41 | instance Semigroup Matplotlib where 42 | (<>) = (%) 43 | #endif 44 | 45 | instance NFData Matplotlib where 46 | rnf (Matplotlib cs po re) = rnf cs `seq` rnf po `seq` rnf re 47 | 48 | -- | A maplotlib command, right now we have a very shallow embedding essentially 49 | -- dealing in strings containing python code as well as the ability to load 50 | -- data. The loaded data should be a json object. 51 | data MplotCommand = 52 | LoadData B.ByteString 53 | | forall x. MplotImage x => LoadImage x 54 | | Exec { es :: String } 55 | 56 | instance NFData MplotCommand where 57 | rnf (LoadData b) = rnf b 58 | rnf (Exec es) = rnf es 59 | -- don't care too much about the LoadImage 60 | 61 | -- | Throughout the API we need to accept options in order to expose 62 | -- matplotlib's many configuration options. 63 | data Option = 64 | -- | results in a=b 65 | K String String 66 | -- | just inserts the option verbatim as an argument at the end of the function 67 | | P String 68 | deriving (Show, Eq, Ord) 69 | 70 | -- | Convert an 'MplotCommand' to python code, doesn't do much right now 71 | toPy :: MplotCommand -> String 72 | toPy (LoadData _) = error "withMplot needed to load data" 73 | toPy (LoadImage _) = error "withMplot needed to load images" 74 | toPy (Exec str) = str 75 | 76 | -- | Resolve the pending command with no options provided. 77 | resolvePending :: Matplotlib -> Matplotlib 78 | resolvePending m = m { mpCommands = 79 | (maybe (mpCommands m) 80 | (\pendingCommand -> (mpCommands m |> pendingCommand [])) 81 | $ mpPendingOption m) >< mpRest m 82 | , mpPendingOption = Nothing 83 | , mpRest = S.empty} 84 | 85 | -- | The io action is given a list of python commands to execute (note that 86 | -- these are commands in the sense of lines of python code; each inidivudal line 87 | -- may not be parseable on its own) 88 | withMplot :: Matplotlib -> ([String] -> IO a) -> IO a 89 | withMplot m f = preload cs [] 90 | where 91 | cs = toList $ mpCommands $ resolvePending m 92 | preload [] cmds = f $ map toPy $ reverse cmds 93 | preload ((LoadData obj):l) cmds = 94 | withSystemTempFile "data.json" 95 | (\dataFile dataHandle -> do 96 | B.hPutStr dataHandle obj 97 | hClose dataHandle 98 | preload l $ ((map Exec $ pyReadData dataFile) ++ cmds)) 99 | preload ((LoadImage img):l) cmds = do 100 | withSystemTempFile "data.json" $ 101 | (\dataFile dataHandle -> do 102 | hClose dataHandle 103 | obj <- saveHaskellImage img dataFile 104 | preload l $ ([Exec $ "img = " ++ (loadPythonImage img obj dataFile)] ++ cmds)) 105 | preload (c:l) cmds = preload l (c:cmds) 106 | 107 | -- | Create a plot that executes the string as python code 108 | mplotString :: String -> Matplotlib 109 | mplotString s = Matplotlib S.empty Nothing (S.singleton $ Exec s) 110 | 111 | -- | Create an empty plot. This the beginning of most plotting commands. 112 | mp :: Matplotlib 113 | mp = Matplotlib S.empty Nothing S.empty 114 | 115 | -- | Load the given data into the python "data" array 116 | readData :: ToJSON a => a -> Matplotlib 117 | readData d = Matplotlib (S.singleton $ LoadData $ encode d) Nothing S.empty 118 | 119 | -- | Load the given image into python "img" variable 120 | readImage :: MplotImage i => i -> Matplotlib 121 | readImage i = Matplotlib (S.singleton $ LoadImage i) Nothing S.empty 122 | 123 | infixl 5 % 124 | -- | Combine two matplotlib commands 125 | (%) :: Matplotlib -> Matplotlib -> Matplotlib 126 | a % b | isJust $ mpPendingOption b = b { mpCommands = mpCommands (resolvePending a) >< mpCommands b } 127 | | otherwise = a { mpRest = mpRest a >< mpCommands b >< mpRest b } 128 | 129 | infixl 6 # 130 | -- | Add Python code to the last matplotlib command 131 | (#) :: (MplotValue val) => Matplotlib -> val -> Matplotlib 132 | m # v | S.null $ mpRest m = 133 | case mpPendingOption m of 134 | Nothing -> m { mpRest = S.singleton $ Exec $ toPython v } 135 | (Just f) -> m { mpPendingOption = Just (\o -> Exec $ es (f o) ++ toPython v)} 136 | | otherwise = m { mpRest = S.adjust (\(Exec s) -> Exec $ s ++ toPython v) (S.length (mpRest m) - 1) (mpRest m) } 137 | 138 | -- | A string to be rendered in python as a string. In other words it is 139 | -- rendered as 'str'. 140 | data S = S String 141 | deriving (Show, Eq, Ord) 142 | 143 | -- | A string to be rendered in python as a raw string. In other words it is 144 | -- rendered as r'str'. 145 | data R = R String 146 | deriving (Show, Eq, Ord) 147 | 148 | -- | A string to be rendered in python as a raw literal/code. In other words it is 149 | -- inserted directly as is into the code. 150 | data L = L String 151 | deriving (Show, Eq, Ord) 152 | 153 | -- | Values which can be combined together to form a matplotlib command. These 154 | -- specify how values are rendered in Python code. 155 | class MplotValue val where 156 | -- | Render a value inline in Python code 157 | toPython :: val -> String 158 | -- | Render a value as an optional parameter in Python code 159 | toPythonOpt :: val -> String 160 | toPythonOpt = toPython 161 | 162 | instance MplotValue S where 163 | toPython (S s) = "'" ++ s ++ "'" 164 | instance MplotValue R where 165 | toPython (R s) = "r'" ++ s ++ "'" 166 | instance MplotValue L where 167 | toPython (L s) = s 168 | instance MplotValue String where 169 | -- | A string is just a literal when used in code 170 | toPython s = s 171 | -- | A string is a real quoted python string when used as an option 172 | toPythonOpt s = toPythonOpt $ S s 173 | instance MplotValue [String] where 174 | toPython [] = "" 175 | toPython (x:xs) = toPython x ++ "," ++ toPython xs 176 | -- | A list of strings is a list of python strings, not literals 177 | toPythonOpt s = "[" ++ f s ++ "]" 178 | where f [] = "" 179 | f (x:xs) = toPythonOpt (str x) ++ "," ++ f xs 180 | instance MplotValue Double where 181 | toPython s = show s 182 | instance MplotValue [Double] where 183 | toPython s = "[" ++ f s ++ "]" 184 | where f [] = "" 185 | f (x:xs) = toPython x ++ "," ++ f xs 186 | instance MplotValue Integer where 187 | toPython s = show s 188 | instance MplotValue [Integer] where 189 | toPython s = "[" ++ f s ++ "]" 190 | where f [] = "" 191 | f (x:xs) = toPython x ++ "," ++ f xs 192 | instance MplotValue Int where 193 | toPython s = show s 194 | instance MplotValue [Int] where 195 | toPython s = "[" ++ f s ++ "]" 196 | where f [] = "" 197 | f (x:xs) = toPython x ++ "," ++ f xs 198 | instance MplotValue [R] where 199 | toPython s = "[" ++ f s ++ "]" 200 | where f [] = "" 201 | f (x:xs) = toPython x ++ "," ++ f xs 202 | instance MplotValue [S] where 203 | toPython s = "[" ++ f s ++ "]" 204 | where f [] = "" 205 | f (x:xs) = toPython x ++ "," ++ f xs 206 | instance MplotValue [L] where 207 | toPython s = "[" ++ f s ++ "]" 208 | where f [] = "" 209 | f (x:xs) = toPython x ++ "," ++ f xs 210 | instance MplotValue Bool where 211 | toPython s = show s 212 | instance (MplotValue x) => MplotValue (x, x) where 213 | toPython (k, v) = "(" ++ toPython k ++ ", " ++ toPython v ++ ")" 214 | instance (MplotValue (x, y)) => MplotValue [(x, y)] where 215 | toPython s = "[" ++ f s ++ "]" 216 | where f [] = "" 217 | f (x:xs) = toPython x ++ "," ++ f xs 218 | instance MplotValue x => MplotValue (Maybe x) where 219 | toPython Nothing = "None" 220 | toPython (Just x) = toPython x 221 | instance MplotValue [[Double]] where 222 | toPython s = "np.asarray([" ++ f s ++ "])" 223 | where f [] = "" 224 | f (x:xs) = toPython x ++ "," ++ f xs 225 | 226 | default (Integer, Int, Double) 227 | 228 | -- | The class of Haskell images or references to imagese which can be 229 | -- transferred to matplotlib. 230 | class MplotImage a where 231 | saveHaskellImage :: a -> FilePath -> IO String 232 | loadPythonImage :: a -> String -> FilePath -> String 233 | 234 | -- | An image that is a string is a file path. 235 | instance MplotImage String where 236 | saveHaskellImage _ _ = return "" 237 | loadPythonImage s _ _ = "mpimg.imread('" ++ toPython s ++ "')" 238 | 239 | instance ToJSON a => MplotImage [[a]] where 240 | saveHaskellImage d fp = (B.writeFile fp $ encode d) >> return "" 241 | loadPythonImage s _ fp = unlines $ pyReadData fp 242 | 243 | -- $ Options 244 | 245 | -- | Add an option to the last matplotlib command. Commands can have only one option! 246 | -- optFn :: Matplotlib -> Matplotlib 247 | optFn :: ([Option] -> String) -> Matplotlib -> Matplotlib 248 | optFn f l | isJust $ mpPendingOption l = error "Commands can have only open option. TODO Enforce this through the type system or relax it!" 249 | | otherwise = l' { mpPendingOption = Just (\os -> Exec (sl `combine` f os)) } 250 | where (l', (Exec sl)) = removeLast l 251 | removeLast x@(Matplotlib _ Nothing s) = (x { mpRest = sdeleteAt (S.length s - 1) s } 252 | , fromMaybe (Exec "") (slookup (S.length s - 1) s)) 253 | removeLast _ = error "TODO complex options" 254 | -- TODO When containers is >0.5.8 replace these 255 | slookup i s | i < S.length s = Just $ S.index s i 256 | | otherwise = Nothing 257 | sdeleteAt i s | i < S.length s = S.take i s >< S.drop (i + 1) s 258 | | otherwise = s 259 | combine [] r = r 260 | combine l [] = l 261 | combine l r | [last l] == "(" && [head r] == "," = l ++ tail r 262 | | otherwise = l ++ r 263 | 264 | -- | Merge two commands with options between 265 | options :: Matplotlib -> Matplotlib 266 | options l = optFn (\o -> renderOptions o) l 267 | 268 | infixl 6 ## 269 | -- | A combinator like '#' that also inserts an option 270 | (##) :: MplotValue val => Matplotlib -> val -> Matplotlib 271 | m ## v = options m # v 272 | 273 | -- | An internal helper to convert a list of options to the python code that 274 | -- applies those options in a call. 275 | renderOptions :: [Option] -> [Char] 276 | renderOptions [] = "" 277 | renderOptions xs = f xs 278 | where f (P a:l) = "," ++ a ++ f l 279 | f (K a b:l) = "," ++ a ++ "=" ++ b ++ f l 280 | f [] = "" 281 | 282 | -- | An internal helper that modifies the options of a plot. 283 | optionFn :: ([Option] -> [Option]) -> Matplotlib -> Matplotlib 284 | optionFn f m = case mpPendingOption m of 285 | (Just cmd) -> m { mpPendingOption = Just (\os -> cmd $ f os) } 286 | Nothing -> error "Can't apply an option to a non-option command" 287 | 288 | -- | Apply a list of options to a plot resolving any pending options. 289 | option :: Matplotlib -> [Option] -> Matplotlib 290 | option m os = resolvePending $ optionFn (\os' -> os ++ os') m 291 | 292 | infixl 6 @@ 293 | -- | A combinator for 'option' that applies a list of options to a plot 294 | (@@) :: Matplotlib -> [Option] -> Matplotlib 295 | m @@ os = option m os 296 | 297 | -- | Bind a list of default options to a plot. Positional options are kept in 298 | -- order and default that way as well. Keyword arguments are also handled 299 | bindDefault :: Matplotlib -> [Option] -> Matplotlib 300 | bindDefault m os = optionFn (bindDefaultFn os) m 301 | 302 | -- | Merge two sets of options 303 | bindDefaultFn :: [Option] -> [Option] -> [Option] 304 | bindDefaultFn os os' = merge ps' ps ++ (nub $ ks' ++ ks) 305 | where isK (K _ _) = True 306 | isK _ = False 307 | isP (P _) = True 308 | isP _ = False 309 | ps = filter isP os 310 | ps' = filter isP os' 311 | ks = filter isK os 312 | ks' = filter isK os' 313 | merge l [] = l 314 | merge [] l' = l' 315 | merge (x:l) (_:l') = (x : merge l l') 316 | 317 | -- $ Python operations 318 | 319 | -- | Run python given a code string. 320 | python :: Foldable t => t String -> IO (Either String String) 321 | python codeStr = 322 | catch (withSystemTempFile "code.py" 323 | (\codeFile codeHandle -> do 324 | forM_ codeStr (hPutStrLn codeHandle) 325 | hClose codeHandle 326 | Right <$> readProcess "env" ["python3", codeFile] "")) 327 | (\e -> return $ Left $ show (e :: IOException)) 328 | 329 | pyBackend backend = "matplotlib.use('" ++ backend ++ "')" 330 | 331 | -- | The standard python includes of every plot 332 | pyIncludes :: String -> [[Char]] 333 | pyIncludes backend = ["import matplotlib" 334 | ,backend 335 | ,"import matplotlib.path as mpath" 336 | ,"import matplotlib.patches as mpatches" 337 | ,"import matplotlib.pyplot as plot" 338 | ,"import matplotlib.cm as cm" 339 | ,"import matplotlib.colors as mcolors" 340 | ,"import matplotlib.collections as mcollections" 341 | ,"import matplotlib.ticker as mticker" 342 | ,"import matplotlib.image as mpimg" 343 | ,"from mpl_toolkits.mplot3d import axes3d" 344 | ,"import numpy as np" 345 | ,"from scipy import interpolate" 346 | ,"import os" 347 | ,"import io" 348 | ,"import sys" 349 | ,"import json" 350 | ,"import random, datetime" 351 | ,"from matplotlib.dates import DateFormatter, WeekdayLocator" 352 | -- We set this rcParams due to: 353 | -- bivariateNormal: /run/user/1000/code12548-89.py:30: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later. 354 | -- plot.sci(ax.pcolor(np.array(data[0]),np.array(data[1]),np.array(data[2]),cmap=r'PuBu_r')) 355 | -- /run/user/1000/code12548-89.py:36: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3. Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading']. This will become an error two minor releases later. 356 | -- plot.sci(ax.pcolor(np.array(data[0]),np.array(data[1]),np.array(data[2]),norm=mcolors.LogNorm(vmin=1.964128034639681e-6, vmax=7.963602137747198),cmap=r'PuBu_r')) 357 | ,"plot.rcParams['pcolor.shading'] ='auto'" 358 | ,"fig = plot.gcf()" 359 | ,"axes = [plot.gca()]" 360 | ,"ax = axes[0]"] 361 | 362 | -- | These will be Python strings and slashes would cause unwanted control characters. 363 | escapeSlashes ('\\':cs) = '\\':'\\':escapeSlashes cs 364 | escapeSlashes (c:cs) = c:escapeSlashes cs 365 | escapeSlashes [] = [] 366 | 367 | -- | The python command that reads external data into the python data array 368 | pyReadData :: [Char] -> [[Char]] 369 | pyReadData filename = ["data = json.loads(open('" ++ escapeSlashes filename ++ "').read())"] 370 | 371 | -- | The python command that reads an image into the img variable 372 | pyReadImage :: [Char] -> [[Char]] 373 | pyReadImage filename = ["img = mpimg.imread('" ++ escapeSlashes filename ++ "')"] 374 | 375 | -- | Detach python so we don't block (TODO This isn't working reliably) 376 | pyDetach :: [[Char]] 377 | pyDetach = ["pid = os.fork()" 378 | ,"if(pid != 0):" 379 | ," exit(0)"] 380 | 381 | -- | Python code to show a plot 382 | pyOnscreen :: [[Char]] 383 | pyOnscreen = ["plot.draw()" 384 | ,"plot.show()"] 385 | 386 | -- | Python code that saves a figure 387 | pyFigure :: [Char] -> [[Char]] 388 | pyFigure output = ["plot.savefig('" ++ escapeSlashes output ++ "')"] 389 | 390 | -- | Python code that returns SVG for a figure 391 | pySVG :: [[Char]] 392 | pySVG = 393 | ["i = io.StringIO()" 394 | ,"plot.savefig(i, format='svg')" 395 | ,"print(i.getvalue())"] 396 | 397 | -- | Create a positional option 398 | o1 x = P $ toPythonOpt x 399 | 400 | -- | Create a keyword option 401 | o2 x = K x . toPythonOpt 402 | 403 | -- | Create a string that will be rendered as a python string 404 | str = S 405 | 406 | -- | Create a string that will be rendered as a raw python string 407 | raw = R 408 | 409 | -- | Create a literal that will inserted into the python code directly 410 | lit = L 411 | 412 | -- | Update axes. Should be called any time the state is changed. 413 | updateAxes = mp # "axes = plot.gcf().get_axes()" 414 | 415 | -- | Update the figure and the axes. Should be called any time the state is changed. 416 | updateFigure = mp # "fig = plot.gcf()" 417 | % mp # "axes = plot.gcf().get_axes()" 418 | % mp # "ax = axes[0] if len(axes) > 0 else None" 419 | 420 | -- | Smallest element of a list of lists 421 | minimum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a 422 | minimum2 l = minimum $ minimum l 423 | 424 | -- | Largest element of a list of lists 425 | maximum2 :: (Ord (t a), Ord a, Foldable t1, Foldable t) => t1 (t a) -> a 426 | maximum2 l = maximum $ maximum l 427 | 428 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.13 2 | packages: 3 | - '.' 4 | extra-deps: 5 | flags: {} 6 | extra-package-dbs: [] 7 | nix: 8 | enable: false 9 | shell-file: shell.nix 10 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 586268 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/13.yaml 11 | sha256: d9e658a22cfe8d87a64fdf219885f942fef5fe2bcb156a9800174911c5da2443 12 | original: lts-18.13 13 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# language ExtendedDefaultRules, ScopedTypeVariables, QuasiQuotes, ParallelListComp #-} 2 | 3 | import Test.Tasty 4 | import Test.Tasty.Runners 5 | import Test.Tasty.HUnit 6 | import Test.Tasty.ExpectedFailure 7 | import System.IO.Unsafe 8 | import Graphics.Matplotlib 9 | import System.IO.Temp 10 | import System.Random 11 | import Text.RawString.QQ 12 | import Data.List 13 | import Data.List.Split 14 | import Control.Monad 15 | import Test.Tasty.Golden.Advanced 16 | import qualified Data.ByteString as BS 17 | import System.Process 18 | import System.IO 19 | import Numeric.AD 20 | import System.Directory 21 | 22 | -- * Random values for testing 23 | 24 | uniforms :: (Random a, Num a) => [a] 25 | uniforms = randoms (mkStdGen 42) 26 | 27 | uniforms' lo hi = randomRs (lo,hi) (mkStdGen 42) 28 | 29 | -- * Not so random values to enable some fully-reproducible tests 30 | 31 | xs = [-0.54571992, 1.48409716, -0.57545561, 2.13058156, -0.75740497, 32 | -1.27879086, -0.96008858, -1.65482373, -1.69086194, -1.41925464, 33 | 0.68144401, 1.44847131, 1.12124327, 1.32056244, -0.4555279 , 34 | 1.96002923, -1.34810771, 0.01513306, 1.25298883, -1.07541677, 35 | 0.60920278, -0.13978163, 0.3975209 , -0.15211044, 0.0534633 , 36 | -0.39498474, -1.15320186, 0.6795936 , 0.50704333, 1.52951037, 37 | 0.90867393, -0.24833115, 1.39816295, -0.28642814, 0.96951915, 38 | -1.20438266, -0.32642955, -0.62982941, 0.7245042 , -1.03169685, 39 | -0.00542761, 0.54247125, -1.11559132, -2.6829048 , -0.13370841, 40 | -0.74111166, 0.59198725, 2.73711931, 1.82122485, -0.73915212, 41 | 0.88290489, -1.17307876, 0.06753304, 0.40150672, 1.54455801, 42 | -0.31133243, 1.66844302, -0.1290924 , 0.89657699, 0.41181393, 43 | 2.13382656, 1.58577659, -1.02449069, -1.10245954, -0.59691196, 44 | -0.63040161, -0.51541836, 0.04139408, 0.54203055, -2.09082082, 45 | -0.41295376, -0.77509336, 0.47612065, -1.69680096, 0.90195265, 46 | 0.23798858, -0.05112783, 1.00645056, -0.67012513, 0.52017487, 47 | -0.42251472, 0.96513844, 1.00298933, 0.18257527, 0.54599979, 48 | -1.50321042, 0.03949817, 0.35286613, 1.86994544, 1.16249707, 49 | 0.57421291, 1.21151469, 1.74863421, 0.42287859, -1.22785845, 50 | -0.61650528, 1.76743253, -0.45818694, -1.16560907, 0.0677502 ] 51 | ys = [ 1.28231455, 1.13480471, 0.57738712, 0.10268954, 1.00162163, 52 | -0.85453571, -1.61049343, 1.33194242, 0.12054943, -0.56568776, 53 | 2.11550009, 0.03663454, 0.24889313, 0.85458325, 0.77326592, 54 | 0.58815223, -0.79997005, 0.54979418, 0.47711544, 0.73004143, 55 | -0.65704545, 1.1946521 , -0.31119444, -0.0958055 , 0.37838453, 56 | 1.01281301, -0.53364162, 2.84609607, 0.09363483, -0.14821451, 57 | -0.0481863 , -3.58277731, -1.7168244 , -0.87526525, -0.65430073, 58 | 1.0284506 , -0.81397895, 0.34868379, -0.51671293, 0.92879285, 59 | 0.04099886, 1.0828335 , 1.25991492, -1.48901447, 0.43657503, 60 | 0.78191509, 0.16633587, 1.99411663, -0.25542794, -0.43377353, 61 | -0.82871869, -0.0402321 , -0.06278027, 0.28066445, 0.01185443, 62 | 1.42640101, -0.16627931, 0.82021257, -0.66684095, -0.21289723, 63 | -1.25974667, -0.28681327, -2.11039334, -0.2722768 , -0.51622958, 64 | 0.01324637, -0.29277708, 1.35916036, -0.09089638, -1.00619256, 65 | 0.62707331, 1.17105748, -0.85636353, -0.6243519 , 0.1720141 , 66 | -0.15715394, 1.13488465, 2.43996937, 2.08224839, -0.23676918, 67 | -0.24924999, 1.21629376, -0.12748227, 0.78319565, -0.10528614, 68 | 0.60177749, -1.03490762, -0.59163218, 0.16414076, 2.22783012, 69 | -0.55178235, -0.69915414, 1.35454045, 0.42931902, -1.33656935, 70 | -0.8023867 , -2.81354854, 0.39553427, -0.22235586, -1.34302011] 71 | 72 | -- * Generate normally distributed random values; taken from normaldistribution==1.1.0.3 73 | 74 | -- | Box-Muller method for generating two normally distributed 75 | -- independent random values from two uniformly distributed 76 | -- independent random values. 77 | boxMuller :: Floating a => a -> a -> (a,a) 78 | boxMuller u1 u2 = (r * cos t, r * sin t) where r = sqrt (-2 * log u1) 79 | t = 2 * pi * u2 80 | 81 | -- | Convert a list of uniformly distributed random values into a 82 | -- list of normally distributed random values. The Box-Muller 83 | -- algorithms converts values two at a time, so if the input list 84 | -- has an uneven number of element the last one will be discarded. 85 | boxMullers :: Floating a => [a] -> [a] 86 | boxMullers (u1:u2:us) = n1:n2:boxMullers us where (n1,n2) = boxMuller u1 u2 87 | boxMullers _ = [] 88 | 89 | -- | Plural variant of 'normal', producing an infinite list of 90 | -- random values instead of returning a new generator. This function 91 | -- is analogous to 'Random.randoms'. 92 | normals = boxMullers $ randoms (mkStdGen 42) 93 | 94 | -- | Analogous to 'normals' but uses the supplied (mean, standard 95 | -- deviation). 96 | normals' (mean, sigma) g = map (\x -> x * sigma + mean) $ normals 97 | 98 | pdfBivariateNormal x y sigmax sigmay mux muy sigmaxy = 99 | 1/(2*pi*sigmax*sigmay*(sqrt(1-rho^2)))*exp(-z/(2*(1-rho^2))) 100 | where rho = sigmaxy/(sigmax*sigmay) 101 | z = (x-mux)^2/sigmax^2-(2*rho*(x-mux)*(y-muy))/(sigmax*sigmay)+(y-muy)^2/sigmay^2 102 | 103 | -- * Tests 104 | 105 | main = do 106 | createDirectoryIfMissing False "/tmp/imgs/" 107 | defaultMain $ tests "All tests" testPlot 108 | 109 | main' = do 110 | createDirectoryIfMissing False "/tmp/imgs/" 111 | defaultMain $ tests "Golden tests" testPlotGolden 112 | 113 | main'' = do 114 | createDirectoryIfMissing False "/tmp/imgs/" 115 | defaultMain $ testGroup "All tests" [tests "Execution tests" testPlot 116 | , toneDownTests "Unreliable across machines" $ tests "Golden tests" testPlotGolden] 117 | 118 | tests name f = testGroup name [basicTests f, 119 | toneDownTests "Can fail with old matplotlib or data" $ fragileTests f, 120 | ignoreTest $ failingTests f] 121 | 122 | toneDownTests reason tests = wrapTest (liftM (\x -> x { resultOutcome = Success 123 | , resultShortDescription = 124 | case resultOutcome x of 125 | Success -> resultShortDescription x 126 | _ -> reason ++ ": " ++ resultShortDescription x 127 | })) tests 128 | 129 | testPlotGolden name fn = 130 | unsafePerformIO $ tmp (\filename -> 131 | return $ goldenTest 132 | name 133 | (BS.readFile ref) 134 | (file filename fn >> BS.readFile filename) 135 | (\g n -> 136 | tmp (\gfile -> 137 | tmp (\nfile -> do 138 | BS.writeFile gfile g 139 | BS.writeFile nfile n 140 | (code, stdout, stderr) <- 141 | readProcessWithExitCode "/usr/bin/compare" ["-metric" 142 | ,"PSNR" 143 | ,gfile 144 | ,nfile 145 | ,"null"] "" 146 | case (stderr, reads stderr) of 147 | ("inf", _) -> return Nothing 148 | (_, [(x :: Double, _)]) -> 149 | if x < 25 then 150 | return $ Just $ "Images very different; PSNR too low " ++ show x else 151 | return Nothing))) 152 | (BS.writeFile ref)) 153 | where ref = "imgs/" ++ name ++ ".png" 154 | tmp f = withSystemTempFile "a.png" (\filename h -> hClose h >> f filename) 155 | 156 | -- | Test one plot; right now we just test that the command executed without 157 | -- errors. We should visually compare plots somehow. 158 | testPlot' name fn = testCase name $ tryit fn @?= Right "" 159 | where tryit fn = unsafePerformIO $ withSystemTempFile "a.png" (\filename _ -> file filename fn) 160 | 161 | -- | This generates examples from the test cases 162 | testPlot name fn = testCase name $ tryit fn name @?= Right "" 163 | where tryit fn name = unsafePerformIO $ do 164 | file ("/tmp/imgs/" ++ name ++ ".png") fn 165 | 166 | basicTests f = testGroup "Basic tests" 167 | [ f "histogram" mhistogram 168 | , f "cumulative" mcumulative 169 | , f "scatter" mscatter 170 | , f "contour" mcontour 171 | , f "labelled-histogram" mlabelledHistogram 172 | , f "density-bandwidth" mdensityBandwidth 173 | , f "density" mdensity 174 | , f "line-function" mlineFunction 175 | , f "quadratic" mQuadratic 176 | , f "projections" mProjections 177 | , f "line-options" mlineOptions 178 | , f "corr" mxcorr 179 | , f "show-matrix" mmat 180 | , f "legend" mlegend 181 | , f "hist2DLog" mhist2DLog 182 | , f "eventplot" meventplot 183 | , f "errorbar" merrorbar 184 | , f "scatterhist" mscatterHist 185 | , f "histMulti" mhistMulti 186 | , f "spines" mspines 187 | , f "hists" mhists 188 | , f "hinton" mhinton 189 | , f "integral" mintegral 190 | , f "quiver" mquiver 191 | , f "quiver-fancy" mquiverFancy 192 | , f "polar" mpolar 193 | , f "bivariateNormal" mbivariateNormal 194 | , f "pcolorlog" mpcolorlog 195 | , f "pie" mpie 196 | , f "stacked" mstacked 197 | , f "annotation" mannotation 198 | , f "streamplot" mstreamplot 199 | ] 200 | 201 | fragileTests f = testGroup "Fragile tests" 202 | [ f "tex" mtex -- TODO Fails on circle ci (with latex) 203 | -- TODO Fails on circle ci (labels is not valid; matplotlib too old) 204 | , f "boxplot" mboxplot 205 | -- TODO Fails on circle ci (no violin plots; matplotlib too old) 206 | , f "violinplot" mviolinplot 207 | -- TODO Needs a fairly recent matplotlib; too old for circleci 208 | , f "griddata" mgriddata 209 | -- TODO Needs access to a data file 210 | , f "images" mimages 211 | ] 212 | 213 | failingTests f = testGroup "Failing tests" 214 | [ 215 | -- TODO This test case is broken 216 | f "sub-bars" msubBars 217 | ] 218 | 219 | -- * These tests are fully-reproducible, the output must be identical every time 220 | 221 | mhistogram :: Matplotlib 222 | mhistogram = histogram xs 8 223 | 224 | mcumulative = histogram xs 8 @@ [o2 "cumulative" True] 225 | 226 | mscatter = scatter xs ys 227 | 228 | degreesRadians a = a * pi / 180.0 229 | radiansDegrees a = a * 180.0 / pi 230 | 231 | mcontour = contourF (\a b -> sin (degreesRadians a) + cos (degreesRadians b)) (-100) 100 (-200) 200 10 232 | 233 | mlabelledHistogram = histogram xs 7 234 | % ylabel "number of queries" 235 | % xlabel "true positives" 236 | 237 | -- TODO Broken test 238 | msubBars = subplotBarsLabelled 239 | [[40, 50, 20, 50], [10, 20, 30, 40], [40, 50, 20, 50]] 240 | ["a", "b", "c", "d"] [] 241 | % title "Wee a title" 242 | % xlabel "X" 243 | % ylabel "Y" 244 | 245 | mdensityBandwidth = densityBandwidth [2.1, 1.3, 0.4, 1.9, 5.1, 6.2] 1.5 (Just (-6, 11)) 246 | % ylim 0 0.2 247 | 248 | mdensity = density [2.1, 1.3, 0.4, 1.9, 5.1, 6.2] (Just (-6, 11)) 249 | 250 | mlineFunction = lineF (\x -> x**2) [0,0.01..1] 251 | 252 | mQuadratic = plotMapLinear (\x -> x**2) (-2) 2 100 @@ [o1 "."] % title "Quadratic function" 253 | 254 | mProjections = projectionsF (\a b -> cos (degreesRadians a) + sin (degreesRadians b)) (-100) 100 (-200) 200 10 255 | 256 | mlineOptions = plot [1,2,3,4,5,6] [1,3,2,5,2,4] @@ [o1 "go-", o2 "linewidth" 2] 257 | 258 | -- * These tests can be random and may not be exactly the same every time 259 | 260 | -- | http://matplotlib.org/examples/pylab_examples/xcorr_demo.html 261 | mxcorr = xacorr xs ys [o2 "usevlines" True, o2 "maxlags" 50, o2 "normed" True, o2 "lw" 2] 262 | where (xs :: [Double]) = take 100 normals 263 | (ys :: [Double]) = take 100 normals 264 | 265 | -- | http://matplotlib.org/examples/pylab_examples/tex_unicode_demo.html 266 | mtex = setTeX True 267 | % figure 268 | % addSubplot 1 1 1 269 | % plotMapLinear cos 0 1 100 270 | % xlabel [r|\textbf{time (s)}|] 271 | % ylabel "\\textit{Velocity (°/sec)}" @@ [o2 "fontsize" 16] 272 | % title [r|\TeX\ is Number $\displaystyle\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}$!"|] @@ [o2 "fontsize" 16, o2 "color" "r"] 273 | % grid True 274 | 275 | mmat = pcolor (take 10 $ chunksOf 8 uniforms) @@ [o2 "edgecolors" "k", o2 "linewidth" 1] 276 | 277 | -- | http://matplotlib.org/examples/pylab_examples/legend_demo3.html 278 | mlegend = plotMapLinear (\x -> x ** 2) 0 1 100 @@ [o2 "label" "x^2"] 279 | % plotMapLinear (\x -> x ** 3) 0 1 100 @@ [o2 "label" "x^3"] 280 | % legend @@ [o2 "fancybox" True, o2 "shadow" True, o2 "title" "Legend", o2 "loc" "upper left"] 281 | 282 | -- | http://matplotlib.org/examples/pylab_examples/hist2d_log_demo.html 283 | mhist2DLog = histogram2D x y @@ [o2 "bins" 40, o2 "norm" $ lit "mcolors.LogNorm()"] 284 | % setAx 285 | % colorbar 286 | where (x:y:_) = chunksOf 10000 normals 287 | 288 | meventplot = plot xs ys 289 | % mp # "ax.add_collection(mcollections.EventCollection(data[0], linelength=0.05))" 290 | % mp # "ax.add_collection(mcollections.EventCollection(data[1], orientation='vertical', linelength=0.05))" 291 | % text 0.1 0.6 "Ticks mark the actual data points" 292 | where xs = sort $ take 10 uniforms 293 | ys = map (\x -> x ** 2) xs 294 | 295 | merrorbar = errorbar xs ys (Nothing :: Maybe [Double]) (Just errs) @@ [o2 "errorevery" 2] 296 | where xs = [0.1,0.2..4] 297 | ys = map (\x -> exp $ -x) xs 298 | errs = [map (\x -> 0.1 + 0.1 * sqrt x) xs, map (\x -> 0.1 + 0.1 * sqrt x) ys] 299 | 300 | mboxplot = subplots @@ [o2 "ncols" 2, o2 "sharey" True] 301 | % setSubplot "0" 302 | % boxplot (take 3 $ chunksOf 10 $ map (* 2) $ normals) @@ [o2 "labels" ["X", "Y", "Z"]] 303 | % setSubplot "1" 304 | % boxplot (take 3 $ chunksOf 10 $ map (* 2) $ normals) @@ [o2 "labels" ["A", "B", "C"], o2 "showbox" False, o2 "showcaps" False] 305 | 306 | mviolinplot = subplots @@ [o2 "ncols" 2, o2 "sharey" True] 307 | % setSubplot "0" 308 | % violinplot (take 3 $ chunksOf 100 $ map (* 2) $ normals) 309 | % setSubplot "1" 310 | % violinplot (take 3 $ chunksOf 100 $ map (* 2) $ normals) @@ [o2 "showmeans" True, o2 "showmedians" True, o2 "vert" False] 311 | 312 | -- | http://matplotlib.org/examples/pylab_examples/scatter_hist.html 313 | mscatterHist = figure @@ [o1 0] 314 | % setSizeInches 8 8 315 | -- The scatter plot 316 | % axes @@ [o1 ([left, bottom', width, height] :: [Double])] 317 | % scatter x y 318 | % xlim (-lim) lim 319 | % ylim (-lim) lim 320 | -- The histogram on the right (x) 321 | % axes @@ [o1 [left, bottom_h, width, 0.2]] 322 | % mp # "ax.xaxis.set_major_formatter(mticker.NullFormatter())" 323 | % histogram x bins 324 | % xlim (-lim) lim 325 | -- The histogram on top (y) 326 | % axes @@ [o1 [left_h, bottom', 0.2, height]] 327 | % mp # "ax.yaxis.set_major_formatter(mticker.NullFormatter())" 328 | % histogram y bins @@ [o2 "orientation" "horizontal"] 329 | % ylim (-lim) lim 330 | where left = 0.1 331 | width = 0.65 332 | bottom' = 0.1 333 | height = 0.65 334 | bottom_h = left + width + 0.02 335 | left_h = left + width + 0.02 336 | [x, y] = take 2 $ chunksOf 1000 $ map (* 2) $ normals 337 | binwidth = 0.25 338 | xymax = maximum [maximum $ map abs x, maximum $ map abs y] 339 | lim = ((fromIntegral $ round $ xymax / binwidth) + 1) * binwidth 340 | bins = [-lim,-lim+binwidth..(lim + binwidth)] 341 | 342 | mhistMulti = subplots @@ [o2 "nrows" 2, o2 "ncols" 2] 343 | % setSubplot 0 344 | % histogram x nrBins @@ [o2 "density" 1, o2 "histtype" "bar", o2 "color" ["red", "tan", "lime"], o2 "label" ["red", "tan", "lime"]] 345 | % legend @@ [o2 "prop" $ lit "{'size': 10}"] 346 | % title "bars with legend" 347 | % setSubplot 1 348 | % histogram x nrBins @@ [o2 "density" 1, o2 "histtype" "bar", o2 "stacked" True] 349 | % title "stacked bar" 350 | % setSubplot 2 351 | % histogram x nrBins @@ [o2 "histtype" "step", o2 "stacked" True, o2 "fill" False] 352 | % title "stacked bar" 353 | % setSubplot 3 354 | % histogram (map (\x -> take x normals) [2000, 5000, 10000]) nrBins @@ [o2 "histtype" "bar"] 355 | % title "different sample sizes" 356 | % tightLayout 357 | where nrBins = 10 358 | x = take 3 $ chunksOf 1000 $ normals 359 | 360 | mspines = plot x y @@ [o1 "k--"] 361 | % plot x y' @@ [o1 "ro"] 362 | % xlim 0 (2 * pi) 363 | % xticks [0 :: Double, pi, 2*pi] 364 | % xtickLabels (map raw ["0", "$\\pi$", "2$\\pi$"]) 365 | % ylim (-1.5) 1.5 366 | % yticks [-1 :: Double, 0, 1] 367 | % spine "left" 368 | % spineSetBounds (-1) 1 369 | % spine "right" 370 | % spineSetVisible False 371 | % spine "top" 372 | % spineSetVisible False 373 | % axisYTicksPosition "left" 374 | % axisXTicksPosition "bottom" 375 | where x = mapLinear (\x -> x) 0 (2 * pi) 50 376 | y = map sin x 377 | y' = zipWith (\a b -> a + 0.1*b) y normals 378 | 379 | mhists = h 10 1.5 380 | % h 4 1 381 | % h 15 2 382 | % h 6 0.5 383 | where ns mu var = map (\x -> mu + x * var) $ take 1000 normals 384 | h mu var = histogram (ns mu var) 25 @@ [o2 "histtype" "stepfilled" 385 | ,o2 "alpha" 0.8 386 | ,o2 "density" True] 387 | 388 | mhinton = mp # "ax.patch.set_facecolor('gray')" 389 | % setAspect @@ [o1 "equal", o1 "box"] 390 | % mp # "ax.xaxis.set_major_locator(plot.NullLocator())" 391 | % mp # "ax.yaxis.set_major_locator(plot.NullLocator())" 392 | % foldl (\a (x,y,w) -> a % f x y w) mp m 393 | % mp # "ax.autoscale_view()" 394 | % mp # "ax.invert_yaxis()" 395 | where m = [ (x,y,w) | x <- [0..19], y <- [0..19] | w <- (map (\x -> x - 0.5) normals) ] 396 | maxWeight = maximum $ map (\(_,_,v) -> abs v) m 397 | f x y w = mp # "ax.add_patch(plot.Rectangle(" 398 | # "[" # (x - size / 2) # "," # (y - size / 2) # "]" 399 | # ", " # size # ", " # size 400 | # ", facecolor='" # color # "', edgecolor='" # color # "'))" 401 | where color = if w > 0 then "white" else "black" 402 | size = sqrt $ abs w / maxWeight 403 | 404 | mintegral = subplots 405 | % plot x y @@ [o1 "r", o2 "linewidth" 2] 406 | % ylim 0 (maximum y) 407 | % mp # "ax.add_patch(plot.Polygon(" # ([(a, 0)] ++ zip ix iy ++ [(b,0)]) ## "))" 408 | @@ [o2 "facecolor" "0.9", o2 "edgecolor" "0.5"] 409 | % text (0.5 * (a + b)) 30 [r|$\int_a^b f(x)\mathrm{d}x$|] 410 | @@ [o2 "horizontalalignment" "center", o2 "fontsize" 20] 411 | % figText 0.9 0.05 "$x$" 412 | % figText 0.1 0.9 "$y$" 413 | % spine "right" 414 | % spineSetVisible False 415 | % spine "top" 416 | % spineSetVisible False 417 | % axisXTicksPosition "bottom" 418 | % xticks (a, b) 419 | % xtickLabels (raw "$a$", raw "$b$") 420 | % yticks ([] :: [Double]) 421 | where func x = (x - 3) * (x - 5) * (x - 7) + 85 422 | -- integral limits 423 | a = 2 :: Double 424 | b = 9 :: Double 425 | (x :: [Double]) = mapLinear (\x -> x) 0 10 100 426 | y = map func x 427 | -- shaded region 428 | (ix :: [Double]) = mapLinear (\x -> x) a b 100 429 | iy = map func ix 430 | 431 | mquiver = quiver x y u v (Nothing :: Maybe [Double]) @@ [o2 "units" "width"] 432 | % quiverKey 0.9 0.93 2 (raw [r|$2 \frac{m}{s}$|]) 433 | @@ [o2 "labelpos" "E", o2 "coordinates" "figure"] 434 | % xlim (-0.2) 6.4 435 | % ylim (-0.2) 6.4 436 | where m = [ (x,y,cos x,sin y) | x <- [0,0.2..2*pi], y <- [0,0.2..2*pi] ] 437 | x = map (\(x,_,_,_) -> x) m 438 | y = map (\(_,x,_,_) -> x) m 439 | u = map (\(_,_,x,_) -> x) m 440 | v = map (\(_,_,_,x) -> x) m 441 | 442 | mquiverFancy = quiver x y u v (Just mag) @@ [o2 "units" "x" 443 | ,o2 "pivot" "tip" 444 | ,o2 "width" 0.022 445 | ,o2 "scale" (1 / 0.15)] 446 | % quiverKey 0.9 0.93 1 (raw [r|$2 \frac{m}{s}$|]) 447 | @@ [o2 "labelpos" "E", o2 "coordinates" "figure"] 448 | % scatter x y @@ [o2 "color" "k", o2 "s" 5] 449 | % xlim (-0.2) 6.4 450 | % ylim (-0.2) 6.4 451 | where m = [ (x,y,cos x,sin y) | x <- [0,0.2..2*pi], y <- [0,0.2..2*pi] ] 452 | x = map (\(x,_,_,_) -> x) m 453 | y = map (\(_,x,_,_) -> x) m 454 | u = map (\(_,_,x,_) -> x) m 455 | v = map (\(_,_,_,x) -> x) m 456 | mag = zipWith (\x x' -> sqrt(x**2 + x'**2)) u v 457 | 458 | mpolar = rc "grid" @@ [o2 "color" "#316931", o2 "linewidth" 1, o2 "linestyle" "-"] 459 | % rc "xtick" @@ [o2 "labelsize" 15] 460 | % rc "ytick" @@ [o2 "labelsize" 15] 461 | % figure @@ [o2 "figsize" (8::Int,8::Int)] 462 | % addAxes @@ [o1 [0.1, 0.1, 0.8, 0.8::Double], o2 "projection" "polar" 463 | -- TODO My matplotlib doesn't seem to have this property 464 | -- , o2 "facecolor" "#d5de9c" 465 | ] 466 | % plot theta r @@ [o2 "color" "#ee8d18", o2 "lw" 3, o2 "label" "a line"] 467 | % plot (map (\x -> 0.5*x) theta) r 468 | @@ [o2 "color" "blue", o2 "ls" "--", o2 "lw" 3, o2 "label" "another line"] 469 | % legend 470 | where r = [0,0.01..3.0] 471 | theta = map (\x -> 2*pi*x) r 472 | 473 | mbivariateNormal = 474 | imshow vs @@ [o2 "interpolation" "bilinear" 475 | ,o2 "cmap" $ raw "RdYlGn" 476 | ,o2 "origin" "lower" 477 | ,o2 "extent" [-3::Double, 3, -3, 3] 478 | ,o2 "vmin" $ (0-) $ maximum $ map abs vs' 479 | ,o2 "vmax" $ maximum $ map abs vs'] 480 | where delta = 0.025::Double 481 | xs = [-3.0,-3.0+delta..3.0] 482 | ys = [-3.0,-3.0+delta..3.0] 483 | vs = [[pdfBivariateNormal x y 1.5 0.5 1.0 1.0 0.0 484 | - pdfBivariateNormal x y 1.0 1.0 0.0 0.0 0.0 485 | | x <- xs] 486 | | y <- ys] 487 | vs' = foldl' (++) [] vs 488 | 489 | -- TODO This is subtly broken 490 | mimages = -- figure @@ [o2 "figsize" (10::Int,10::Int)] 491 | -- % 492 | subplots @@ [o2 "nrows" 1, o2 "ncols" 2] 493 | % setSubplot 0 494 | % imshow "data/heightmap.png" @@ [o2 "interpolation" "nearest"] 495 | % setSubplot 1 496 | % mp # "ls = mcolors.LightSource(azdeg=315, altdeg=45)" 497 | % mp # "ax.imshow(ls.shade(img, cmap=cm.gist_earth))" 498 | -- TODO This doesn't work on my matplab version 499 | -- vert_exag=0.05, 500 | --, blend_mode='overlay' 501 | % xlabel "overlay blend mode" 502 | 503 | mpcolorlog = figure 504 | % addSubplot 2 1 1 505 | % pcolor3 xs' ys' vs @@ [o2 "cmap" $ raw "PuBu_r"] 506 | % colorbar 507 | % addSubplot 2 1 2 508 | % pcolor3 xs' ys' vs @@ [o2 "norm" 509 | (lit $ "mcolors.LogNorm(vmin="++(show $ minimum vs')++ 510 | ", vmax="++(show $ maximum vs')++")") 511 | ,o2 "cmap" $ raw "PuBu_r"] 512 | % colorbar 513 | where delta = 0.1::Double 514 | xs = [-3.0,-3.0+delta..3.0] 515 | ys = [-3.0,-3.0+delta..3.0] 516 | vs = [[pdfBivariateNormal x y 0.1 0.2 1.0 1.0 0.0 517 | + 0.1 * pdfBivariateNormal x y 1.0 1.0 0.0 0.0 0.0 518 | | x <- xs] 519 | | y <- ys] 520 | xs' = [[ x | x <- xs]| y <- ys] 521 | ys' = [[ y | x <- xs]| y <- ys] 522 | vs' = foldl' (++) [] vs 523 | 524 | mpie = pie [15, 30, 45, 10 :: Double] 525 | @@ [o2 "explode" [0, 0.05, 0, 0 :: Double] 526 | ,o2 "labels" ["Frogs", "Hogs", "Dogs", "Logs"] 527 | ,o2 "autopct" "%.0f%%" 528 | ,o2 "shadow" True] 529 | 530 | -- | http://matplotlib.org/examples/pylab_examples/bar_stacked.html 531 | mstacked = 532 | -- TODO The locations of the bars is off 533 | bar [0..4] ms @@ [o1 width, o2 "color" "#d62728", o2 "yerr" mStd, o2 "label" "ms"] 534 | % bar [0..4] ws @@ [o1 width, o2 "bottom" ms, o2 "yerr" wStd, o2 "label" "ws"] 535 | % xticks [0..4 :: Int] 536 | % xtickLabels "['G1', 'G2', 'G3', 'G4', 'G5']" 537 | % title "Scores" 538 | % ylabel "Score" 539 | % yticks [0,10..80 :: Int] 540 | % legend 541 | where ms = [20 :: Double, 35, 30, 35, 27] 542 | ws = [25 :: Double, 32, 34, 20, 25] 543 | mStd = [2 :: Double, 3, 4, 1, 2] 544 | wStd = [3 :: Double, 5, 2, 3, 3] 545 | width = 0.35 :: Double 546 | 547 | mannotation = -- figure @@ [o2 "figsize" (10::Int,10::Int)] 548 | -- TODO This is subtly broken 549 | -- TODO Dictionaries 550 | plot t s @@ [o2 "lw" 3] 551 | % xlim (-1) 5 552 | % ylim (-4) 3 553 | % annotate "straight" @@ [o2 "xy" [0, 1::Double], o2 "xycoords" "data", o2 "xytext" [-50, 30 :: Double] 554 | ,o2 "textcoords" "offset points", o2 "arrowprops" (lit "dict(arrowstyle='->')")] 555 | % annotate "arc3,\\nrad 0.2" @@ [o2 "xy" [0.5, -1::Double], o2 "xycoords" "data", o2 "xytext" [-80, -60 :: Double] 556 | ,o2 "textcoords" "offset points" 557 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='arc3,rad=.2')")] 558 | % annotate "arc,\\nangle 50" @@ [o2 "xy" [1, 1::Double], o2 "xycoords" "data", o2 "xytext" [-90, 50 :: Double] 559 | ,o2 "textcoords" "offset points" 560 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='arc,angleA=0,armA=50,rad=10')")] 561 | % annotate "arc,\\narms" @@ [o2 "xy" [1.5, -1::Double], o2 "xycoords" "data", o2 "xytext" [-80, -60 :: Double] 562 | ,o2 "textcoords" "offset points" 563 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='arc,angleA=0,armA=40,angleB=-90,armB=30,rad=7')")] 564 | % annotate "angle,\\nangle 90" @@ [o2 "xy" [2, 1::Double], o2 "xycoords" "data", o2 "xytext" [-70, 30 :: Double] 565 | ,o2 "textcoords" "offset points" 566 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='angle,angleA=0,angleB=90,rad=10')")] 567 | % annotate "angle3,\\nangle -90" @@ [o2 "xy" [2.5, -1::Double], o2 "xycoords" "data", o2 "xytext" [-80, -60 :: Double] 568 | ,o2 "textcoords" "offset points" 569 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='angle3,angleA=0,angleB=-90')")] 570 | % annotate "angle,\\nround" @@ [o2 "xy" [3, 1::Double], o2 "xycoords" "data", o2 "xytext" [-60, 30 :: Double] 571 | ,o2 "textcoords" "offset points" 572 | ,o2 "bbox" (lit "dict(boxstyle='round', fc='0.8')") 573 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='angle,angleA=0,angleB=90,rad=10')")] 574 | % annotate "angle,\\nround4" @@ [o2 "xy" [3.5, -1::Double], o2 "xycoords" "data", o2 "xytext" [-70, -80 :: Double] 575 | ,o2 "textcoords" "offset points" 576 | ,o2 "bbox" (lit "dict(boxstyle='round4,pad=.5', fc='0.8')") 577 | ,o2 "size" 20 578 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='angle,angleA=0,angleB=-90,rad=10')")] 579 | % annotate "angle,\\nshrink" @@ [o2 "xy" [4, 1::Double], o2 "xycoords" "data", o2 "xytext" [-60, 30 :: Double] 580 | ,o2 "textcoords" "offset points" 581 | ,o2 "bbox" (lit "dict(boxstyle='round', fc='0.8')") 582 | ,o2 "arrowprops" (lit "dict(arrowstyle='->', connectionstyle='angle,angleA=0,angleB=90,rad=10')")] 583 | -- TODO This annotation doesn't render correctly on my matplotlib version 584 | % annotate "" @@ [o2 "xy" [4, 1::Double], o2 "xycoords" "data", o2 "xytext" [4.5, -1 :: Double] 585 | ,o2 "textcoords" "offset points" 586 | ,o2 "arrowprops" (lit "dict(arrowstyle='<->', connectionstyle='bar', ec='k', shrinkA=5, shrinkB=5)")] 587 | where t = [0, 0.01 .. 5.0 :: Double] 588 | s = map (\x -> cos $ 2*pi*x) t 589 | 590 | mstreamplot = streamplot xs ys xs' ys' @@ [o2 "linewidth" mag'] 591 | -- useful for seeing the energy landscape 592 | -- pcolor3 xmat ymat vs 593 | where delta = 0.05::Double 594 | xs = [-3.0,-3.0+delta..3.0] 595 | ys = [-3.0,-3.0+delta..3.0] 596 | xmat = [[ x | x <- xs]| y <- ys] 597 | ymat = [[ y | x <- xs]| y <- ys] 598 | ms = [[grad' (\[xv,yv] -> 599 | pdfBivariateNormal xv yv 0.4 0.7 1.0 1.0 0.0 600 | + pdfBivariateNormal xv yv 1.0 1.0 (-1.0) (-1.0) 0.0) 601 | [x,y] 602 | | x <- xs] 603 | | y <- ys] :: [[(Double, [Double])]] 604 | vs = map2 (\(v, _) -> v) ms 605 | xs' = map2 (\(_, [x, _]) -> x) ms 606 | ys' = map2 (\(_, [_, y]) -> y) ms 607 | mag' = zipWith (\lx ly -> zipWith (\x y -> 5 * (log $ 1 + (sqrt $ x*x + y*y))) lx ly) xs' ys' 608 | map2 f l = map (\r -> map f r) l 609 | 610 | mgriddata = readData (x, y, z, xi, yi) 611 | -- TODO This requires a lot of manual indexing. Next big API change will be to 612 | -- have references to loaded data. 613 | % mp # "data.append(interpolate.griddata((data[0], data[1]), data[2], tuple(np.meshgrid(data[3], data[4])), method='linear', rescale=True))" 614 | % mp # "plot.contour(data[3], data[4], data[5], 15, linewidths=0.5, colors='k')" 615 | % mp # "plot.contourf(data[3], data[4], data[5], 15, extend='both')" 616 | % colorbar 617 | % scatter x y @@ [o2 "marker" "o", o2 "s" 5, o2 "zorder" 10] 618 | % xlim (-2) 2 619 | % ylim (-2) 2 620 | % title "Grid interpolation" 621 | where [x, y] = take 2 $ chunksOf 200 $ map (\x -> 4 * (x - 0.5)) $ uniforms 622 | z = zipWith (\x y -> x*(exp $ -(x**2) - y**2)) x y 623 | xi = mapLinear (\x -> x) (-2.1) 2.1 300 624 | yi = mapLinear (\x -> x) (-2.1) 2.1 300 625 | --------------------------------------------------------------------------------