pandoc-pyplot 2.1.3.0 → 2.1.4.0
raw patch · 7 files changed
+196/−186 lines, 7 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Text.Pandoc.Filter.Pyplot.Internal: parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec)
Files
- CHANGELOG.md +14/−0
- README.md +101/−45
- pandoc-pyplot.cabal +1/−1
- src/Text/Pandoc/Filter/Pyplot.hs +6/−114
- src/Text/Pandoc/Filter/Pyplot/Configuration.hs +1/−1
- src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs +68/−23
- src/Text/Pandoc/Filter/Pyplot/Types.hs +5/−2
CHANGELOG.md view
@@ -2,6 +2,20 @@ pandoc-pyplot uses [Semantic Versioning](http://semver.org/spec/v2.0.0.html) +Release 2.1.4.0 +--------------- + +* Added examples and documentation on how to use `pandoc-pyplot` on LaTeX documents. +* Allowed raw LaTeX macros in figure captions. This is required to label figures in LaTeX. E.g.: + + ```latex + \begin{minted}[caption=myCaption\label{myfig}]{pyplot} + + \end{minted} + ``` + +* `with-links` key changed to `links`. I'm sorry. Pandoc doesn't support LaTeX tokens with `-`. + Release 2.1.3.0 ---------------
README.md view
@@ -6,6 +6,9 @@ `pandoc-pyplot` turns Python code present in your documents into embedded Matplotlib figures. * [Usage](#usage) + * [Markdown](#Markdown) + * [LaTeX](#latex) + * [Examples](#examples) * [Features](#features) * [Installation](#installation) * [Running the filter](#running-the-filter) @@ -13,19 +16,21 @@ ## Usage -The filter recognizes code blocks with the `.pyplot` class present. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. +### Markdown +The filter recognizes code blocks with the `.pyplot` class present in Markdown documents. It will run the script in the associated code block in a Python interpreter and capture the generated Matplotlib figure. + Here is a basic example using the scripting `matplotlib.pyplot` API: -```markdown - ```{.pyplot} - import matplotlib.pyplot as plt +~~~markdown +```{.pyplot} +import matplotlib.pyplot as plt - plt.figure() - plt.plot([0,1,2,3,4], [1,2,3,4,5]) - plt.title('This is an example figure') - ``` +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') ``` +~~~ Putting the above in `input.md`, we can then generate the plot and embed it: @@ -41,38 +46,80 @@ or any other output format you want. +### LaTeX + +The filter works slightly differently in LaTeX documents. In LaTeX, the `minted` environment must be used, with the `pyplot` class. + +```latex +\begin{minted}{pyplot} +import matplotlib.pyplot as plt + +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') +\end{minted} +``` + +Note that __you do not need to have `minted` installed__. + +### Examples + There are more examples in the [source repository](https://github.com/LaurentRDC/pandoc-pyplot), in the `\examples` directory. ## Features ### Captions -You can also specify a caption for your image. This is done using the optional `caption` parameter: +You can also specify a caption for your image. This is done using the optional `caption` parameter. -```markdown - ```{.pyplot caption="This is a simple figure"} - import matplotlib.pyplot as plt +__Markdown__: - plt.figure() - plt.plot([0,1,2,3,4], [1,2,3,4,5]) - plt.title('This is an example figure') - ``` +~~~markdown +```{.pyplot caption="This is a simple figure"} +import matplotlib.pyplot as plt + +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') ``` +~~~ +__LaTex__: + +```latex +\begin{minted}[caption=This is a simple figure]{pyplot} +import matplotlib.pyplot as plt + +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') +\end{minted} +``` + Caption formatting is either plain text or Markdown. LaTeX-style math is also support in captions (using dollar signs $...$). ### Link to source code and high-resolution figure In case of an output format that supports links (e.g. HTML), the embedded image generated by `pandoc-pyplot` will be a link to the source code which was used to generate the file. Therefore, other people can see what Python code was used to create your figures. A high resolution image will be made available in a caption link. -(*New in version 2.1.3.0*) For cleaner output (e.g. PDF), you can turn this off via the `with-links=false` key: +(*New in version 2.1.3.0*) For cleaner output (e.g. PDF), you can turn this off via the `links=false` key: -```markdown - ```{.pyplot with-links=false} - ... - ``` +__Markdown__: + +~~~markdown +```{.pyplot links=false} +... ``` +~~~ +__LaTex__: + +```latex +\begin{minted}[links=false]{pyplot} +... +\end{minted} +``` + or via a [configuration file](#Configurable). ### Including scripts @@ -86,27 +133,35 @@ and include it in your document as follows: -```markdown - ```{.pyplot include=style.py} - plt.figure() - plt.plot([0,1,2,3,4], [1,2,3,4,5]) - plt.title('This is an example figure') - ``` +~~~markdown +```{.pyplot include=style.py} +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') ``` +~~~ Which is equivalent to writing the following markdown: -```markdown - ```{.pyplot} - import matplotlib.pyplot as plt - plt.style.use('ggplot') +~~~markdown +```{.pyplot} +import matplotlib.pyplot as plt +plt.style.use('ggplot') - plt.figure() - plt.plot([0,1,2,3,4], [1,2,3,4,5]) - plt.title('This is an example figure') - ``` +plt.figure() +plt.plot([0,1,2,3,4], [1,2,3,4,5]) +plt.title('This is an example figure') ``` +~~~ +The equivalent LaTeX usage is as follows: + +```latex +\begin{minted}[include=style.py]{pyplot} + +\end{minted} +``` + This `include` parameter is perfect for longer documents with many plots. Simply define the style you want in a separate script! You can also import packages this way, or define functions you often use. ### No wasted work @@ -115,18 +170,18 @@ ### Compatibility with pandoc-crossref -[`pandoc-crossref`](https://github.com/lierdakil/pandoc-crossref) is a pandoc filter that makes it effortless to cross-reference objects on documents. +[`pandoc-crossref`](https://github.com/lierdakil/pandoc-crossref) is a pandoc filter that makes it effortless to cross-reference objects in Markdown documents. You can use `pandoc-crossref` in conjunction with `pandoc-pyplot` for the ultimate figure-making pipeline. You can combine both in a figure like so: -```markdown - ```{#fig:myexample .pyplot caption="This is a caption"} - # Insert figure script here - ``` - - As you can see in @fig:myexample, ... +~~~markdown +```{#fig:myexample .pyplot caption="This is a caption"} +# Insert figure script here ``` +As you can see in @fig:myexample, ... +~~~ + If the above source is located in file `myfile.md`, you can render the figure and references by applying `pandoc-pyplot` **first**, and then `pandoc-crossref`. For example: ```bash @@ -143,7 +198,7 @@ directory: mydirectory/ include: mystyle.py format: jpeg -with-links: false +links: false dpi: 150 flags: [-O, -Wignore] ``` @@ -152,12 +207,13 @@ ```yaml # Defaults if no configuration is provided. -# Note that the default interpreter name on MacOS and Unix is python3 +# Note that the default interpreter name on MacOS and Unix is 'python3' +# and 'python' on Windows. interpreter: python flags: [] directory: generated/ format: png -with-links: true +links: true dpi: 80 ```
pandoc-pyplot.cabal view
@@ -1,5 +1,5 @@ name: pandoc-pyplot -version: 2.1.3.0 +version: 2.1.4.0 cabal-version: >= 1.12 synopsis: A Pandoc filter to include figures generated from Python code blocks description: A Pandoc filter to include figures generated from Python code blocks. Keep the document and Python code in the same location. Output from Matplotlib is captured and included as a figure.
src/Text/Pandoc/Filter/Pyplot.hs view
@@ -10,8 +10,8 @@ Stability : stable Portability : portable -This module defines a Pandoc filter @makePlot@ that can be -used to walk over a Pandoc document and generate figures from +This module defines a Pandoc filter @makePlot@ and related functions +that can be used to walk over a Pandoc document and generate figures from Python code blocks. The syntax for code blocks is simple, Code blocks with the @.pyplot@ @@ -30,68 +30,9 @@ * @caption="..."@: Specify a plot caption (or alternate text). Captions support Markdown formatting and LaTeX math (@$...$@). * @dpi=...@: Specify a value for figure resolution, or dots-per-inch. Default is 80DPI. * @include=...@: Path to a Python script to include before the code block. Ideal to avoid repetition over many figures. - -Here are some example blocks in Markdown: - -@ -This is a paragraph - -```{.pyplot caption="This is a caption."} -import matplotlib.pyplot as plt - -plt.figure() -plt.plot([0,1,2,3,4], [1,2,3,4,5]) -plt.title('This is an example figure') -``` - -This is another paragraph - -```{.pyplot dpi=150 format=SVG} -# This example was taken from the Matplotlib gallery -# https://matplotlib.org/examples/pylab_examples/bar_stacked.html - -import numpy as np -import matplotlib.pyplot as plt - -N = 5 -menMeans = (20, 35, 30, 35, 27) -womenMeans = (25, 32, 34, 20, 25) -menStd = (2, 3, 4, 1, 2) -womenStd = (3, 5, 2, 3, 3) -ind = np.arange(N) # the x locations for the groups -width = 0.35 # the width of the bars: can also be len(x) sequence - -p1 = plt.bar(ind, menMeans, width, color='#d62728', yerr=menStd) -p2 = plt.bar(ind, womenMeans, width, - bottom=menMeans, yerr=womenStd) - -plt.ylabel('Scores') -plt.title('Scores by group and gender') -plt.xticks(ind, ('G1', 'G2', 'G3', 'G4', 'G5')) -plt.yticks(np.arange(0, 81, 10)) -plt.legend((p1[0], p2[0]), ('Men', 'Women')) -``` -@ - -This filter was originally designed to be used with [Hakyll](https://jaspervdj.be/hakyll/). -In case you want to use the filter with your own Hakyll setup, you can use a transform -function that works on entire documents: - -@ -import Text.Pandoc.Filter.Pyplot (plotTransform) - -import Hakyll - --- Unsafe compiler is required because of the interaction --- in IO (i.e. running an external Python script). -makePlotPandocCompiler :: Compiler (Item String) -makePlotPandocCompiler = - pandocCompilerWithTransformM - defaultHakyllReaderOptions - defaultHakyllWriterOptions - (unsafeCompiler . plotTransform) -@ - + * @links=true|false@: Add links to source code and high-resolution version of this figure. + This is @true@ by default, but you may wish to disable this for PDF output. + Custom configurations are possible via the @Configuration@ type and the filter functions @plotTransformWithConfig@ and @makePlotWithConfig@. -} @@ -112,63 +53,14 @@ , makePlot' ) where -import Control.Monad ((>=>), join) - -import Data.List (intersperse) +import Control.Monad ((>=>)) import Data.Default.Class (def) -import qualified Data.Map.Strict as Map -import Data.Maybe (fromMaybe) -import Data.Monoid ((<>)) -import qualified Data.Text as T -import qualified Data.Text.IO as T -import Data.Version (showVersion) -import Paths_pandoc_pyplot (version) - -import System.FilePath (makeValid) - import Text.Pandoc.Definition import Text.Pandoc.Walk (walkM) import Text.Pandoc.Filter.Pyplot.Internal - --- | Code block class that will trigger the filter -filterClass :: String -filterClass = "pyplot" - - --- | Flexible boolean parsing -readBool :: String -> Bool -readBool s | s `elem` ["True", "true", "'True'", "'true'", "1"] = True - | s `elem` ["False", "false", "'False'", "'false'", "0"] = False - | otherwise = error $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"] - --- | Determine inclusion specifications from Block attributes. --- Note that the @".pyplot"@ class is required, but all other parameters are optional -parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec) -parseFigureSpec config (CodeBlock (id', cls, attrs) content) - | filterClass `elem` cls = Just <$> figureSpec - | otherwise = return Nothing - where - attrs' = Map.fromList attrs - filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs - includePath = Map.lookup includePathKey attrs' - - figureSpec :: IO FigureSpec - figureSpec = do - includeScript <- fromMaybe (return $ defaultIncludeScript config) $ T.readFile <$> includePath - let header = "# Generated by pandoc-pyplot " <> ((T.pack . showVersion) version) - fullScript = mconcat $ intersperse "\n" [header, includeScript, T.pack content] - caption' = Map.findWithDefault mempty captionKey attrs' - format = fromMaybe (defaultSaveFormat config) $ join $ saveFormatFromString <$> Map.lookup saveFormatKey attrs' - dir = makeValid $ Map.findWithDefault (defaultDirectory config) directoryKey attrs' - dpi' = fromMaybe (defaultDPI config) $ read <$> Map.lookup dpiKey attrs' - withLinks' = fromMaybe (defaultWithLinks config) $ readBool <$> Map.lookup withLinksKey attrs' - blockAttrs' = (id', filter (/= filterClass) cls, filteredAttrs) - return $ FigureSpec caption' withLinks' fullScript format dir dpi' blockAttrs' - -parseFigureSpec _ _ = return Nothing -- | Main routine to include Matplotlib plots. -- Code blocks containing the attributes @.pyplot@ are considered
src/Text/Pandoc/Filter/Pyplot/Configuration.hs view
@@ -41,7 +41,7 @@ dpiKey = "dpi" includePathKey = "include" saveFormatKey = "format" -withLinksKey = "with-links" +withLinksKey = "links" -- | list of all keys related to pandoc-pyplot. inclusionKeys :: [String]
src/Text/Pandoc/Filter/Pyplot/FigureSpec.hs view
@@ -19,53 +19,65 @@ , sourceCodePath , figurePath , addPlotCapture + , parseFigureSpec -- for testing purposes , extension ) where import Control.Monad (join) +import Data.Default.Class (def) import Data.Hashable (hash) +import Data.List (intersperse) +import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import Data.Monoid ((<>)) import qualified Data.Text as T +import qualified Data.Text.IO as T +import Data.Version (showVersion) +import Paths_pandoc_pyplot (version) + import System.FilePath (FilePath, addExtension, - replaceExtension, (</>)) + replaceExtension, (</>), makeValid) import Text.Pandoc.Definition import Text.Pandoc.Builder (imageWith, link, para, fromList, toList) import Text.Pandoc.Class (runPure) import Text.Pandoc.Extensions (extensionsFromList, Extension(..)) -import Text.Pandoc.Options (def, ReaderOptions(..)) +import Text.Pandoc.Options (ReaderOptions(..)) import Text.Pandoc.Readers (readMarkdown) import Text.Pandoc.Filter.Pyplot.Types +import Text.Pandoc.Filter.Pyplot.Configuration -readerOptions :: ReaderOptions -readerOptions = def - {readerExtensions = - extensionsFromList - [ Ext_tex_math_dollars - , Ext_superscript - , Ext_subscript - ] - } --- | Read a figure caption in Markdown format. LaTeX math @$...$@ is supported, --- as are Markdown subscripts and superscripts. -captionReader :: String -> Maybe [Inline] -captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' (T.pack t) - where - readMarkdown' = readMarkdown readerOptions - - extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks +-- | Determine inclusion specifications from Block attributes. +-- Note that the @".pyplot"@ class is required, but all other parameters are optional +parseFigureSpec :: Configuration -> Block -> IO (Maybe FigureSpec) +parseFigureSpec config (CodeBlock (id', cls, attrs) content) + | "pyplot" `elem` cls = Just <$> figureSpec + | otherwise = return Nothing + where + attrs' = Map.fromList attrs + filteredAttrs = filter (\(k, _) -> k `notElem` inclusionKeys) attrs + includePath = Map.lookup includePathKey attrs' - extractInlines (Plain inlines) = inlines - extractInlines (Para inlines) = inlines - extractInlines (LineBlock multiinlines) = join multiinlines - extractInlines _ = [] + figureSpec :: IO FigureSpec + figureSpec = do + includeScript <- fromMaybe (return $ defaultIncludeScript config) $ T.readFile <$> includePath + let header = "# Generated by pandoc-pyplot " <> ((T.pack . showVersion) version) + fullScript = mconcat $ intersperse "\n" [header, includeScript, T.pack content] + caption' = Map.findWithDefault mempty captionKey attrs' + format = fromMaybe (defaultSaveFormat config) $ join $ saveFormatFromString <$> Map.lookup saveFormatKey attrs' + dir = makeValid $ Map.findWithDefault (defaultDirectory config) directoryKey attrs' + dpi' = fromMaybe (defaultDPI config) $ read <$> Map.lookup dpiKey attrs' + withLinks' = fromMaybe (defaultWithLinks config) $ readBool <$> Map.lookup withLinksKey attrs' + blockAttrs' = (id', filter (/= "pyplot") cls, filteredAttrs) + return $ FigureSpec caption' withLinks' fullScript format dir dpi' blockAttrs' + +parseFigureSpec _ _ = return Nothing -- | Convert a FigureSpec to a Pandoc block component toImage :: FigureSpec -> Block @@ -120,3 +132,36 @@ , T.pack $ show dpi' , ")" ] + +-- | Reader options for captions. +readerOptions :: ReaderOptions +readerOptions = def + {readerExtensions = + extensionsFromList + [ Ext_tex_math_dollars + , Ext_superscript + , Ext_subscript + , Ext_raw_tex + ] + } + +-- | Read a figure caption in Markdown format. LaTeX math @$...$@ is supported, +-- as are Markdown subscripts and superscripts. +captionReader :: String -> Maybe [Inline] +captionReader t = either (const Nothing) (Just . extractFromBlocks) $ runPure $ readMarkdown' (T.pack t) + where + readMarkdown' = readMarkdown readerOptions + + extractFromBlocks (Pandoc _ blocks) = mconcat $ extractInlines <$> blocks + + extractInlines (Plain inlines) = inlines + extractInlines (Para inlines) = inlines + extractInlines (LineBlock multiinlines) = join multiinlines + extractInlines _ = [] + + +-- | Flexible boolean parsing +readBool :: String -> Bool +readBool s | s `elem` ["True", "true", "'True'", "'true'", "1"] = True + | s `elem` ["False", "false", "'False'", "'false'", "0"] = False + | otherwise = error $ mconcat ["Could not parse '", s, "' into a boolean. Please use 'True' or 'False'"]
src/Text/Pandoc/Filter/Pyplot/Types.hs view
@@ -147,7 +147,7 @@ -- this is for a filepath object [ "directory" .= dir' , "include" .= ("example.py" :: FilePath) - , "with-links" .= withLinks' + , "links" .= withLinks' , "dpi" .= dpi' , "format" .= (toLower <$> show savefmt') , "interpreter" .= interp' @@ -155,7 +155,10 @@ ] --- | Datatype containing all parameters required to run pandoc-pyplot +-- | Datatype containing all parameters required to run pandoc-pyplot. +-- +-- It is assumed that once a @FigureSpec@ has been created, no configuration +-- can overload it; hence, a @FigureSpec@ completely encodes a particular figure. data FigureSpec = FigureSpec { caption :: String -- ^ Figure caption. , withLinks :: Bool -- ^ Append links to source code and high-dpi figure to caption