chiark / gitweb /
plugins: new m.plots plugin for rendering inline SVG plots.
authorVladimír Vondruš <mosra@centrum.cz>
Tue, 15 May 2018 21:52:58 +0000 (23:52 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Wed, 16 May 2018 10:20:49 +0000 (12:20 +0200)
20 files changed:
CONTRIBUTING.rst
css/m-components.css
css/m-dark+doxygen.compiled.css
css/m-dark.compiled.css
css/m-dark.css
css/m-light+doxygen.compiled.css
css/m-light.compiled.css
css/m-light.css
doc/css/components-plot.svg [new file with mode: 0644]
doc/css/components.rst
doc/plugins.rst
doc/plugins/links.rst
doc/plugins/math-and-code.rst
doc/plugins/plots-test.rst [new file with mode: 0644]
doc/plugins/plots.rst [new file with mode: 0644]
pelican-plugins/m/plots.py [new file with mode: 0644]
pelican-plugins/m/test/plots/page.html [new file with mode: 0644]
pelican-plugins/m/test/plots/page.rst [new file with mode: 0644]
pelican-plugins/m/test/test_plots.py [new file with mode: 0644]
site/pelicanconf.py

index 01dc38b392669b7b5621a2dfaae5b2224f050f3c..5751a2d39f0d298be174843265cd4181f21cc070 100644 (file)
@@ -53,14 +53,14 @@ On ArchLinux:
 
 .. code:: sh
 
-    sudo pacman -S texlive-most pelican python-pillow
+    sudo pacman -S texlive-most pelican python-pillow python-matplotlib
     cower -d python-pyphen # Build the python-pyphen package from AUR
 
 On Ubuntu you need these:
 
 .. code:: sh
 
-    sudo apt-get install texlive-base texlive-latex-extra texlive-fonts-extra
+    sudo apt-get install texlive-base texlive-latex-extra texlive-fonts-extra python3-matplotlib
     pip3 install pelican Pyphen Pillow
 
 Once you have all the dependencies, simply go to the ``site/`` subdirectory and
index 928e159b62b489dbdb8fada3c25bace5e6fff253..11316542af7d1a8e49e1d479848b82eca6b0f0ed 100644 (file)
@@ -1434,15 +1434,50 @@ div.m-math {
   text-align: center;
 }
 
-/* Colored math block, inline math */
+/* Plots */
+div.m-plot svg {
+  text-align: center;
+}
+div.m-plot .m-background {
+  fill: var(--plot-background-color);
+}
+/* Font sizes are hardcoded in m.plots, change there first! */
+div.m-plot svg .m-label { font-size: 11px; }
+div.m-plot svg .m-title { font-size: 13px; }
+div.m-plot svg .m-label, div.m-plot svg .m-title { fill: var(--color); }
+div.m-plot svg .m-line {
+  stroke: var(--color);
+  stroke-width: 0.8;
+}
+div.m-plot svg .m-error {
+  stroke: var(--plot-error-color);
+  stroke-width: 1.5;
+}
+div.m-plot svg .m-label.m-dim { fill: var(--dim-color); }
+
+/* Colored math block, inline math, plots */
 div.m-math svg, svg.m-math { fill: var(--color); }
-div.m-math.m-default svg, svg.m-math.m-default { fill: var(--default-color); }
-div.m-math.m-primary svg, svg.m-math.m-primary { fill: var(--primary-color); }
-div.m-math.m-success svg, svg.m-math.m-success { fill: var(--success-color); }
-div.m-math.m-warning svg, svg.m-math.m-warning { fill: var(--warning-color); }
-div.m-math.m-danger svg, svg.m-math.m-danger { fill: var(--danger-color); }
-div.m-math.m-info svg, svg.m-math.m-info { fill: var(--info-color); }
-div.m-math.m-dim svg, svg.m-math.m-dim { fill: var(--dim-color); }
+div.m-math.m-default svg, svg.m-math.m-default, div.m-plot svg .m-bar.m-default {
+  fill: var(--default-color);
+}
+div.m-math.m-primary svg, svg.m-math.m-primary, div.m-plot svg .m-bar.m-primary {
+  fill: var(--primary-color);
+}
+div.m-math.m-success svg, svg.m-math.m-success, div.m-plot svg .m-bar.m-success {
+  fill: var(--success-color);
+}
+div.m-math.m-warning svg, svg.m-math.m-warning, div.m-plot svg .m-bar.m-warning {
+  fill: var(--warning-color);
+}
+div.m-math.m-danger svg, svg.m-math.m-danger, div.m-plot svg .m-bar.m-danger {
+  fill: var(--danger-color);
+}
+div.m-math.m-info svg, svg.m-math.m-info, div.m-plot svg .m-bar.m-info {
+  fill: var(--info-color);
+}
+div.m-math.m-dim svg, svg.m-math.m-dim, div.m-plot svg .m-bar.m-dim {
+  fill: var(--dim-color);
+}
 
 /* Spacing after every block element, but not after the last */
 p, ul, ol, dl, blockquote, pre, figure.m-code-figure, figure.m-console-figure,
index 9d8436ed22d4c915edfc875b2f6d2843f6a4dadd..c99320abbbc159c19d496b3bfc038caa52f187e9 100644 (file)
@@ -1709,14 +1709,46 @@ div.m-math {
   overflow-y: hidden;
   text-align: center;
 }
+div.m-plot svg {
+  text-align: center;
+}
+div.m-plot .m-background {
+  fill: #34424d;
+}
+div.m-plot svg .m-label { font-size: 11px; }
+div.m-plot svg .m-title { font-size: 13px; }
+div.m-plot svg .m-label, div.m-plot svg .m-title { fill: #dcdcdc; }
+div.m-plot svg .m-line {
+  stroke: #dcdcdc;
+  stroke-width: 0.8;
+}
+div.m-plot svg .m-error {
+  stroke: #ffffff;
+  stroke-width: 1.5;
+}
+div.m-plot svg .m-label.m-dim { fill: #747474; }
 div.m-math svg, svg.m-math { fill: #dcdcdc; }
-div.m-math.m-default svg, svg.m-math.m-default { fill: #dcdcdc; }
-div.m-math.m-primary svg, svg.m-math.m-primary { fill: #a5c9ea; }
-div.m-math.m-success svg, svg.m-math.m-success { fill: #3bd267; }
-div.m-math.m-warning svg, svg.m-math.m-warning { fill: #c7cf2f; }
-div.m-math.m-danger svg, svg.m-math.m-danger { fill: #cd3431; }
-div.m-math.m-info svg, svg.m-math.m-info { fill: #2f83cc; }
-div.m-math.m-dim svg, svg.m-math.m-dim { fill: #747474; }
+div.m-math.m-default svg, svg.m-math.m-default, div.m-plot svg .m-bar.m-default {
+  fill: #dcdcdc;
+}
+div.m-math.m-primary svg, svg.m-math.m-primary, div.m-plot svg .m-bar.m-primary {
+  fill: #a5c9ea;
+}
+div.m-math.m-success svg, svg.m-math.m-success, div.m-plot svg .m-bar.m-success {
+  fill: #3bd267;
+}
+div.m-math.m-warning svg, svg.m-math.m-warning, div.m-plot svg .m-bar.m-warning {
+  fill: #c7cf2f;
+}
+div.m-math.m-danger svg, svg.m-math.m-danger, div.m-plot svg .m-bar.m-danger {
+  fill: #cd3431;
+}
+div.m-math.m-info svg, svg.m-math.m-info, div.m-plot svg .m-bar.m-info {
+  fill: #2f83cc;
+}
+div.m-math.m-dim svg, svg.m-math.m-dim, div.m-plot svg .m-bar.m-dim {
+  fill: #747474;
+}
 p, ul, ol, dl, blockquote, pre, figure.m-code-figure, figure.m-console-figure,
 hr, article, article > header, article section,
 .m-note, .m-frame, .m-block, div.m-button,
index 8dd12f7ea91c419419b0252f7e4d71582b2910bd..bece910274484ca526c4b2697773432b66848966 100644 (file)
@@ -1709,14 +1709,46 @@ div.m-math {
   overflow-y: hidden;
   text-align: center;
 }
+div.m-plot svg {
+  text-align: center;
+}
+div.m-plot .m-background {
+  fill: #34424d;
+}
+div.m-plot svg .m-label { font-size: 11px; }
+div.m-plot svg .m-title { font-size: 13px; }
+div.m-plot svg .m-label, div.m-plot svg .m-title { fill: #dcdcdc; }
+div.m-plot svg .m-line {
+  stroke: #dcdcdc;
+  stroke-width: 0.8;
+}
+div.m-plot svg .m-error {
+  stroke: #ffffff;
+  stroke-width: 1.5;
+}
+div.m-plot svg .m-label.m-dim { fill: #747474; }
 div.m-math svg, svg.m-math { fill: #dcdcdc; }
-div.m-math.m-default svg, svg.m-math.m-default { fill: #dcdcdc; }
-div.m-math.m-primary svg, svg.m-math.m-primary { fill: #a5c9ea; }
-div.m-math.m-success svg, svg.m-math.m-success { fill: #3bd267; }
-div.m-math.m-warning svg, svg.m-math.m-warning { fill: #c7cf2f; }
-div.m-math.m-danger svg, svg.m-math.m-danger { fill: #cd3431; }
-div.m-math.m-info svg, svg.m-math.m-info { fill: #2f83cc; }
-div.m-math.m-dim svg, svg.m-math.m-dim { fill: #747474; }
+div.m-math.m-default svg, svg.m-math.m-default, div.m-plot svg .m-bar.m-default {
+  fill: #dcdcdc;
+}
+div.m-math.m-primary svg, svg.m-math.m-primary, div.m-plot svg .m-bar.m-primary {
+  fill: #a5c9ea;
+}
+div.m-math.m-success svg, svg.m-math.m-success, div.m-plot svg .m-bar.m-success {
+  fill: #3bd267;
+}
+div.m-math.m-warning svg, svg.m-math.m-warning, div.m-plot svg .m-bar.m-warning {
+  fill: #c7cf2f;
+}
+div.m-math.m-danger svg, svg.m-math.m-danger, div.m-plot svg .m-bar.m-danger {
+  fill: #cd3431;
+}
+div.m-math.m-info svg, svg.m-math.m-info, div.m-plot svg .m-bar.m-info {
+  fill: #2f83cc;
+}
+div.m-math.m-dim svg, svg.m-math.m-dim, div.m-plot svg .m-bar.m-dim {
+  fill: #747474;
+}
 p, ul, ol, dl, blockquote, pre, figure.m-code-figure, figure.m-console-figure,
 hr, article, article > header, article section,
 .m-note, .m-frame, .m-block, div.m-button,
index 6ca682f9c0246d03e7b2e631e280307d9e136dac..5f9baceab4fd060bac451ff4a13a4d78b4a7e3b7 100644 (file)
   --navpanel-link-color: #ffffff;
   --navpanel-link-active-color: #a5c9ea;
 
+  /* Plots */
+  --plot-background-color: #34424d;
+  --plot-error-color: #ffffff;
+
   /* Colored components */
   --default-color: #dcdcdc;
   --default-filled-color: #dcdcdc;
index 36626c4367dbb974f7bd4d33f52ef0327d00cbfd..fa022b3f780a6326e32b38337af254e4b5a53476 100644 (file)
@@ -1709,14 +1709,46 @@ div.m-math {
   overflow-y: hidden;
   text-align: center;
 }
+div.m-plot svg {
+  text-align: center;
+}
+div.m-plot .m-background {
+  fill: #fbf0ec;
+}
+div.m-plot svg .m-label { font-size: 11px; }
+div.m-plot svg .m-title { font-size: 13px; }
+div.m-plot svg .m-label, div.m-plot svg .m-title { fill: #000000; }
+div.m-plot svg .m-line {
+  stroke: #000000;
+  stroke-width: 0.8;
+}
+div.m-plot svg .m-error {
+  stroke: #000000;
+  stroke-width: 1.5;
+}
+div.m-plot svg .m-label.m-dim { fill: #bdbdbd; }
 div.m-math svg, svg.m-math { fill: #000000; }
-div.m-math.m-default svg, svg.m-math.m-default { fill: #000000; }
-div.m-math.m-primary svg, svg.m-math.m-primary { fill: #cb4b16; }
-div.m-math.m-success svg, svg.m-math.m-success { fill: #31c25d; }
-div.m-math.m-warning svg, svg.m-math.m-warning { fill: #c7cf2f; }
-div.m-math.m-danger svg, svg.m-math.m-danger { fill: #f60000; }
-div.m-math.m-info svg, svg.m-math.m-info { fill: #2e7dc5; }
-div.m-math.m-dim svg, svg.m-math.m-dim { fill: #bdbdbd; }
+div.m-math.m-default svg, svg.m-math.m-default, div.m-plot svg .m-bar.m-default {
+  fill: #000000;
+}
+div.m-math.m-primary svg, svg.m-math.m-primary, div.m-plot svg .m-bar.m-primary {
+  fill: #cb4b16;
+}
+div.m-math.m-success svg, svg.m-math.m-success, div.m-plot svg .m-bar.m-success {
+  fill: #31c25d;
+}
+div.m-math.m-warning svg, svg.m-math.m-warning, div.m-plot svg .m-bar.m-warning {
+  fill: #c7cf2f;
+}
+div.m-math.m-danger svg, svg.m-math.m-danger, div.m-plot svg .m-bar.m-danger {
+  fill: #f60000;
+}
+div.m-math.m-info svg, svg.m-math.m-info, div.m-plot svg .m-bar.m-info {
+  fill: #2e7dc5;
+}
+div.m-math.m-dim svg, svg.m-math.m-dim, div.m-plot svg .m-bar.m-dim {
+  fill: #bdbdbd;
+}
 p, ul, ol, dl, blockquote, pre, figure.m-code-figure, figure.m-console-figure,
 hr, article, article > header, article section,
 .m-note, .m-frame, .m-block, div.m-button,
index 5c57695658d8c7943d3855c6177b3e447283f40a..9ed3bd3c3591a7cd213571b540b0b61967d876a5 100644 (file)
@@ -1709,14 +1709,46 @@ div.m-math {
   overflow-y: hidden;
   text-align: center;
 }
+div.m-plot svg {
+  text-align: center;
+}
+div.m-plot .m-background {
+  fill: #fbf0ec;
+}
+div.m-plot svg .m-label { font-size: 11px; }
+div.m-plot svg .m-title { font-size: 13px; }
+div.m-plot svg .m-label, div.m-plot svg .m-title { fill: #000000; }
+div.m-plot svg .m-line {
+  stroke: #000000;
+  stroke-width: 0.8;
+}
+div.m-plot svg .m-error {
+  stroke: #000000;
+  stroke-width: 1.5;
+}
+div.m-plot svg .m-label.m-dim { fill: #bdbdbd; }
 div.m-math svg, svg.m-math { fill: #000000; }
-div.m-math.m-default svg, svg.m-math.m-default { fill: #000000; }
-div.m-math.m-primary svg, svg.m-math.m-primary { fill: #cb4b16; }
-div.m-math.m-success svg, svg.m-math.m-success { fill: #31c25d; }
-div.m-math.m-warning svg, svg.m-math.m-warning { fill: #c7cf2f; }
-div.m-math.m-danger svg, svg.m-math.m-danger { fill: #f60000; }
-div.m-math.m-info svg, svg.m-math.m-info { fill: #2e7dc5; }
-div.m-math.m-dim svg, svg.m-math.m-dim { fill: #bdbdbd; }
+div.m-math.m-default svg, svg.m-math.m-default, div.m-plot svg .m-bar.m-default {
+  fill: #000000;
+}
+div.m-math.m-primary svg, svg.m-math.m-primary, div.m-plot svg .m-bar.m-primary {
+  fill: #cb4b16;
+}
+div.m-math.m-success svg, svg.m-math.m-success, div.m-plot svg .m-bar.m-success {
+  fill: #31c25d;
+}
+div.m-math.m-warning svg, svg.m-math.m-warning, div.m-plot svg .m-bar.m-warning {
+  fill: #c7cf2f;
+}
+div.m-math.m-danger svg, svg.m-math.m-danger, div.m-plot svg .m-bar.m-danger {
+  fill: #f60000;
+}
+div.m-math.m-info svg, svg.m-math.m-info, div.m-plot svg .m-bar.m-info {
+  fill: #2e7dc5;
+}
+div.m-math.m-dim svg, svg.m-math.m-dim, div.m-plot svg .m-bar.m-dim {
+  fill: #bdbdbd;
+}
 p, ul, ol, dl, blockquote, pre, figure.m-code-figure, figure.m-console-figure,
 hr, article, article > header, article section,
 .m-note, .m-frame, .m-block, div.m-button,
index d1e742dd11a9eb2dbac9f4a0040a8993e24ec6c0..5790811654fc89fa178f6ffe27027919da90a69b 100644 (file)
   --navpanel-link-color: #292929;
   --navpanel-link-active-color: #cb4b16;
 
+  /* Plots */
+  --plot-background-color: #fbf0ec;
+  --plot-error-color: #000000;
+
   /* Colored components */
   --default-color: #000000;
   --default-filled-color: #000000;
diff --git a/doc/css/components-plot.svg b/doc/css/components-plot.svg
new file mode 100644 (file)
index 0000000..f9cc68e
--- /dev/null
@@ -0,0 +1,40 @@
+<svg viewBox="0 0 576 113.76">
+  <path d="M 68.22875 70.705312 L 560.62 70.705312 L 560.62 27.136406 L 68.22875 27.136406 z" class="m-background"/>
+
+  <path d="M 68.22875 29.116811 L 482.89476 29.116811 L 482.89476 46.72041 L 68.22875 46.72041 z" class="m-bar m-warning"/>
+  <path d="M 68.22875 51.121309 L 403.676117 51.121309 L 403.676117 68.724908 L 68.22875 68.724908 z" class="m-bar m-primary"/>
+
+  <defs><path d="M 0 0 L 0 3.5" id="m15f0a304df" class="m-line"/></defs>
+  <use x="68.22875" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 68.22875, 85.537656)" x="68.22875" y="85.537656">0</text>
+  <use x="144.036065" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 144.036065, 85.537656)" x="144.036065" y="85.537656">20</text>
+  <use x="219.843379" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 219.843379, 85.537656)" x="219.843379" y="85.537656">40</text>
+  <use x="295.650694" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 295.650694, 85.537656)" x="295.650694" y="85.537656">60</text>
+  <use x="371.458008" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 371.458008, 85.537656)" x="371.458008" y="85.537656">80</text>
+  <use x="447.265323" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 447.265323, 85.537656)" x="447.265323" y="85.537656">100</text>
+  <use x="523.072637" xlink:href="#m15f0a304df" y="70.705312"/>
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 523.072637, 85.537656)" x="523.072637" y="85.537656">120</text>
+
+  <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 314.424375, 99.625)" x="314.424375" y="99.625">km/h</text>
+
+  <defs><path d="M 0 0 L -3.5 0" id="mba4ce04b6c" class="m-line"/></defs>
+  <use x="68.22875" xlink:href="#mba4ce04b6c" y="37.91861"/>
+  <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 61.22875, 41.834782)" x="61.22875" y="41.834782">Cheetah</text>
+  <use x="68.22875" xlink:href="#mba4ce04b6c" y="59.923108"/>
+  <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 61.22875, 63.83928)" x="61.22875" y="63.83928">Pronghorn</text>
+
+  <path d="M 428.616723 37.91861 L 537.172798 37.91861" class="m-error"/>
+  <path d="M 382.829105 59.923108 L 424.523128 59.923108" class="m-error"/>
+  <defs><path d="M 0 5 L 0 -5" id="m468f9279d6" class="m-error"/></defs>
+  <use x="428.616723" xlink:href="#m468f9279d6" y="37.91861"/>
+  <use x="382.829105" xlink:href="#m468f9279d6" y="59.923108"/>
+  <use x="537.172798" xlink:href="#m468f9279d6" y="37.91861"/>
+  <use x="424.523128" xlink:href="#m468f9279d6" y="59.923108"/>
+
+  <text class="m-title" style="text-anchor:middle;" transform="rotate(-0, 314.424375, 21.136406)" x="314.424375" y="21.136406">Fastest animals</text>
+</svg>
index 25f8637a7bf6e3689bfa7fd8a5e3252601715da2..fc340fecc2f680cb5530217e3782ec67c8048b80 100644 (file)
@@ -1064,6 +1064,47 @@ the ``depth`` value returned on stderr can be taken as a base for the
     integrates LaTeX math directly into your :abbr:`reST <reStructuredText>`
     markup for convenient content authoring.
 
+`Plots`_
+========
+
+Wrap a :html:`<svg>` element in a :html:`<div class="m-plot">` to make it
+centered and occupying full width. Mark plot axes background with
+:css:`.m-background`, bars can be styled using :css:`.m-bar` and a
+corresponding `CSS color class <#colors>`_. Mark ticks and various other lines
+with :css:`.m-line`, error bars with :css:`.m-error`. Use
+:html:`<text class="m-label">` for tick and axes labels and
+:html:`<text class="m-title">` for graph title.
+
+.. code-figure::
+
+    .. code:: html
+
+        <div class="m-plot"><svg>
+          <path d="M 68.22875 70.705312 ..." class="m-background"/>
+          <path d="M 68.22875 29.116811 ..." class="m-bar m-warning"/>
+          <path d="M 68.22875 51.121309 ..." class="m-bar m-primary"/>
+          ...
+          <defs><path d="..." id="mba4ce04b6c" class="m-line"/></defs>
+          <use x="68.22875" xlink:href="#mba4ce04b6c" y="37.91861"/>
+          <text class="m-label" style="text-anchor:end;" ...>Cheetah</text>
+          ...
+          <path d="M 428.616723 37.91861 ..." class="m-error"/>
+          ...
+          <text class="m-title" style="text-anchor:middle;" ...>Fastest animals</text>
+        </svg></div>
+
+    .. container:: m-plot
+
+        .. raw:: html
+            :file: components-plot.svg
+
+.. note-success::
+
+    Plot styling is designed to be used with extenal tools, for example Python
+    Matplotlib. If you use Pelican, m.css has a `Plots <{filename}/plugins/plots.rst>`__
+    plugin that allows you to produce plots using :rst:`.. plot::` directives
+    directly in your :abbr:`reST <reStructuredText>` markup.
+
 `Padding`_
 ==========
 
index b216fad34288906fa2e474a39865d4c89c552abd..afb54bbd5e1a0ff5dc3380045c0dc341d1aec708 100644 (file)
@@ -45,6 +45,7 @@ and restart Pelican. Download the plugins below or
 -   :gh:`m.images <mosra/m.css$master/pelican-plugins/m/images.py>`
 -   :gh:`m.math  <mosra/m.css$master/pelican-plugins/m/math.py>` (needs also :gh:`latex2svg <mosra/m.css$master/pelican-plugins/latex2svg.py>`),
     :gh:`m.code <mosra/m.css$master/pelican-plugins/m/code.py>` (needs also :gh:`ansilexer <mosra/m.css$master/pelican-plugins/ansilexer.py>`)
+-   :gh:`m.plots <mosra/m.css$master/pelican-plugins/m/plots.py>`
 -   :gh:`m.gh <mosra/m.css$master/pelican-plugins/m/gh.py>`,
     :gh:`m.dox <mosra/m.css$master/pelican-plugins/m/dox.py>`,
     :gh:`m.gl <mosra/m.css$master/pelican-plugins/m/gl.py>`,
@@ -86,6 +87,13 @@ rendering and syntax highlighting, so they are provided as separate packages
 that you can but don't have to use. With these, math and code snippets can be
 entered directly in your :abbr:`reST <reStructuredText>` sources.
 
+`Plots » <{filename}/plugins/plots.rst>`_
+===================================================
+
+With :py:`m.plots` you can render various graphs and charts directly from
+values in your :abbr:`reST <reStructuredText>` sources. The result is embedded
+as an inline SVG and can be styled using CSS like everything else.
+
 `Links and other » <{filename}/plugins/links.rst>`_
 ===================================================
 
index a9fb8899e3fd4ff7760ccf34d541342f3b2447e9..eb57a94dcbceeeb776bebed3dcbd28585ec64f04 100644 (file)
@@ -30,7 +30,7 @@ Links and other
     .. note-dim::
         :class: m-text-center
 
-        `« Math and code <{filename}/plugins/math-and-code.rst>`_ | `Pelican plugins <{filename}/plugins.rst>`_ | `Metadata » <{filename}/plugins/metadata.rst>`_
+        `« Plots <{filename}/plugins/plots.rst>`_ | `Pelican plugins <{filename}/plugins.rst>`_ | `Metadata » <{filename}/plugins/metadata.rst>`_
 
 .. role:: py(code)
     :language: py
index a3210ea650c6e8da928862c1ec077e93c83feda5..09b4e56846598f5d79e0d1cdebc393466208d547 100644 (file)
@@ -30,7 +30,7 @@ Math and code
     .. note-dim::
         :class: m-text-center
 
-        `« Images <{filename}/plugins/images.rst>`_ | `Pelican plugins <{filename}/plugins.rst>`_ | `Links and other » <{filename}/plugins/links.rst>`_
+        `« Images <{filename}/plugins/images.rst>`_ | `Pelican plugins <{filename}/plugins.rst>`_ | `Plots » <{filename}/plugins/plots.rst>`_
 
 .. role:: css(code)
     :language: css
diff --git a/doc/plugins/plots-test.rst b/doc/plugins/plots-test.rst
new file mode 100644 (file)
index 0000000..a99935e
--- /dev/null
@@ -0,0 +1,57 @@
+..
+    This file is part of m.css.
+
+    Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included
+    in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+..
+
+Test
+####
+
+:save_as: plugins/plots/test/index.html
+:breadcrumb: {filename}/plugins.rst Pelican plugins
+             {filename}/plugins/plots.rst Plots
+
+.. container:: m-row
+
+    .. container:: m-col-m-6
+
+        .. plot:: Fastest animals
+            :type: barh
+            :labels:
+                Cheetah
+                Pronghorn
+            :units: km/h
+            :values: 109.4 88.5
+            :colors: warning primary
+            :errors: 14.32 5.5
+
+    .. container:: m-col-m-6
+
+        .. plot:: Fastest animals
+            :type: barh
+            :labels:
+                Cheetah
+                Pronghorn
+                Springbok
+                Wildebeest
+            :units: km/h
+            :values: 109.4 88.5 88 80.5
+            :colors: warning primary danger info
diff --git a/doc/plugins/plots.rst b/doc/plugins/plots.rst
new file mode 100644 (file)
index 0000000..1da954a
--- /dev/null
@@ -0,0 +1,144 @@
+..
+    This file is part of m.css.
+
+    Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included
+    in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+..
+
+Plots
+#####
+
+:breadcrumb: {filename}/plugins.rst Pelican plugins
+:footer:
+    .. note-dim::
+        :class: m-text-center
+
+        `« Math and code <{filename}/plugins/math-and-code.rst>`_ | `Pelican plugins <{filename}/plugins.rst>`_ | `Links and other » <{filename}/plugins/math-and-code.rst>`_
+
+Allows you to render plots directly from data specified inline in the page
+source. Similarly to `math rendering <{filename}/admire/math.rst>`_, the plots
+are rendered to a SVG that's embedded directly in the HTML markup.
+
+.. contents::
+    :class: m-block m-default
+
+`How to use`_
+=============
+
+Download the `m/plots.py <{filename}/plugins.rst>`_ file, put it including the
+``m/`` directory into one of your :py:`PLUGIN_PATHS` and add ``m.plots``
+package to your :py:`PLUGINS` in ``pelicanconf.py``.
+
+.. code:: python
+
+    PLUGINS += ['m.plots']
+
+In addition you need the `Matplotlib <https://matplotlib.org/>`_ library
+installed. Get it via ``pip`` or your distribution package manager:
+
+.. code:: sh
+
+    pip3 install matplotlib
+
+The plugin produces SVG plots that make use of the
+`CSS plot styling <{filename}/css/components.rst#plots>`_.
+
+`Bar graphs`_
+=============
+
+Currently the only supported plot type is a horizontal bar graph, denoted by
+:rst:`:type: hbar`:
+
+.. code-figure::
+
+    .. code:: rst
+
+        .. plot:: Fastest animals
+            :type: barh
+            :labels:
+                Cheetah
+                Pronghorn
+                Springbok
+                Wildebeest
+            :units: km/h
+            :values: 109.4 88.5 88 80.5
+
+    .. plot:: Fastest animals
+        :type: barh
+        :labels:
+            Cheetah
+            Pronghorn
+            Springbok
+            Wildebeest
+        :units: km/h
+        :values: 109.4 88.5 88 80.5
+
+The multi-line :rst:`:labels:` option contain value labels, one per line. You
+can specify unit label using :rst:`:units:`, particular values go into
+:rst:`:values:` separated by whitespace, there should me as many values as
+labels. Hovering over the bars will show the concrete value in a title.
+
+It's also optionally possible to add error bars using :rst:`:error:` and
+configure bar colors using :rst:`:colors:`. The colors correspond to m.css
+`color classes <{filename}/css/components.rst#colors>`_ and you can either
+use one color for all or one for each value, separated by whitespace. It's
+possible to add an extra line of labels using :rst:`:labels_extra:`. Bar graph
+height is calculated automatically based on amount of values, you can adjust
+the bar height using :rst:`:bar_height:`. Default value is :py:`0.4`.
+
+.. code-figure::
+
+    .. code:: rst
+
+        .. plot:: Runtime cost
+            :type: barh
+            :labels:
+                Ours minimal
+                Ours default
+                3rd party
+                Full setup
+            :labels_extra:
+                15 modules
+                60 modules
+                200 modules
+                for comparison
+            :units: µs
+            :values: 15.09 84.98 197.13 934.27
+            :errors: 0.74 3.65 9.45 25.66
+            :colors: success info danger dim
+            :bar_height: 0.6
+
+    .. plot:: Runtime cost
+        :type: barh
+        :labels:
+            Ours minimal
+            Ours default
+            3rd party
+            Full setup
+        :labels_extra:
+            15 modules
+            60 modules
+            200 modules
+            for comparison
+        :units: µs
+        :values: 15.09 84.98 197.13 934.27
+        :errors: 0.74 3.65 9.45 25.66
+        :colors: success info danger dim
+        :bar_height: 0.6
diff --git a/pelican-plugins/m/plots.py b/pelican-plugins/m/plots.py
new file mode 100644 (file)
index 0000000..e724a7a
--- /dev/null
@@ -0,0 +1,244 @@
+#
+#   This file is part of m.css.
+#
+#   Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+#
+#   Permission is hereby granted, free of charge, to any person obtaining a
+#   copy of this software and associated documentation files (the "Software"),
+#   to deal in the Software without restriction, including without limitation
+#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+#   and/or sell copies of the Software, and to permit persons to whom the
+#   Software is furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included
+#   in all copies or substantial portions of the Software.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#   DEALINGS IN THE SOFTWARE.
+#
+
+import re
+
+from docutils import nodes, utils
+from docutils.parsers import rst
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.roles import set_classes
+
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+import io
+
+import pelican.signals
+
+mpl.rcParams['font.family'] = 'Source Sans Pro'
+mpl.rcParams['font.size'] = '11'
+mpl.rcParams['axes.titlesize'] = '13'
+
+# Plot background. Replaced with .m-plot .m-background later, equivalent to
+# --default-filled-background-color
+mpl.rcParams['axes.facecolor'] = '#cafe01'
+
+# All of these should match --color, replaced with .m-plot .m-text
+mpl.rcParams['text.color'] = '#cafe02'
+mpl.rcParams['axes.labelcolor'] = '#cafe02'
+mpl.rcParams['xtick.color'] = '#cafe02'
+mpl.rcParams['ytick.color'] = '#cafe02'
+
+# no need to have a border around the plot
+mpl.rcParams['axes.spines.left'] = False
+mpl.rcParams['axes.spines.right'] = False
+mpl.rcParams['axes.spines.top'] = False
+mpl.rcParams['axes.spines.bottom'] = False
+
+mpl.rcParams['svg.fonttype'] = 'none' # otherwise it renders text to paths
+mpl.rcParams['figure.autolayout'] = True # so it relayouts everything to fit
+
+# Gets increased for every graph on a page to (hopefully) ensure unique SVG IDs
+mpl.rcParams['svg.hashsalt'] = 0
+
+# Color codes for bars
+style_mapping = {
+    'default': '#cafe03',
+    'primary': '#cafe04',
+    'success': '#cafe05',
+    'warning': '#cafe06',
+    'danger': '#cafe07',
+    'info': '#cafe08',
+    'dim': '#cafe09'
+}
+
+# Patch to remove preamble and hardcoded sizes
+_patch_src = re.compile(r"""<\?xml version="1\.0" encoding="utf-8" standalone="no"\?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1\.1//EN"
+  "http://www\.w3\.org/Graphics/SVG/1\.1/DTD/svg11\.dtd">
+<!-- Created with matplotlib \(http://matplotlib.org/\) -->
+<svg height="\d+(\.\d+)?pt" version="1.1" (?P<viewBox>viewBox="0 0 \d+ \d+(\.\d+)?") width="\d+(\.\d+)?pt" xmlns="http://www\.w3\.org/2000/svg" xmlns:xlink="http://www\.w3\.org/1999/xlink">
+""")
+_patch_dst = r"""<svg \g<viewBox>>
+"""
+
+# Remove needless newlines and trailing space in path data
+_path_patch_src = re.compile('(?P<prev>[\\dz]) ?\n(?P<next>[LMz])', re.MULTILINE)
+_path_patch_dst = '\\g<prev> \\g<next>'
+_path_patch2_src = re.compile(' ?\n"')
+_path_patch2_dst = '"'
+
+# Mapping from color codes to CSS classes
+_class_mapping = [
+    # Graph background
+    ('style="fill:#cafe01;"', 'class="m-background"'),
+
+    # Tick <path> definition in <defs>
+    ('style="stroke:#cafe02;stroke-width:0.8;"', 'class="m-line"'),
+    # <use>, everything is defined in <defs>, no need to repeat
+    ('<use style="fill:#cafe02;stroke:#cafe02;stroke-width:0.8;"', '<use'),
+
+    # Label text on left
+    ('style="fill:#cafe02;font-family:Source Sans Pro;font-size:11px;font-style:normal;font-weight:normal;"', 'class="m-label"'),
+    # Label text on bottom (has extra style params)
+    ('style="fill:#cafe02;font-family:Source Sans Pro;font-size:11px;font-style:normal;font-weight:normal;', 'class="m-label" style="'),
+    # Secondary label text
+    ('style="fill:#cafe0b;font-family:Source Sans Pro;font-size:11px;font-style:normal;font-weight:normal;"', 'class="m-label m-dim"'),
+    # Title text
+    ('style="fill:#cafe02;font-family:Source Sans Pro;font-size:13px;font-style:normal;font-weight:normal;', 'class="m-title" style="'),
+
+    # Bar colors
+    ('style="fill:#cafe03;"', 'class="m-bar m-default"'),
+    ('style="fill:#cafe04;"', 'class="m-bar m-primary"'),
+    ('style="fill:#cafe05;"', 'class="m-bar m-success"'),
+    ('style="fill:#cafe06;"', 'class="m-bar m-warning"'),
+    ('style="fill:#cafe07;"', 'class="m-bar m-danger"'),
+    ('style="fill:#cafe08;"', 'class="m-bar m-info"'),
+    ('style="fill:#cafe09;"', 'class="m-bar m-dim"'),
+
+    # Error bar line
+    ('style="fill:none;stroke:#cafe0a;stroke-width:1.5;"', 'class="m-error"'),
+    # Error bar <path> definition in <defs>
+    ('style="stroke:#cafe0a;"', 'class="m-error"'),
+    # <use>, everything is defined in <defs>, no need to repeat
+    ('<use style="fill:#cafe0a;stroke:#cafe0a;"', '<use'),
+]
+
+# Titles for bars
+_bar_titles_src = '<g id="plot{}-value{}">'
+_bar_titles_dst = '<g id="plot{}-value{}"><title>{} {}</title>'
+_bar_titles_dst_error = '<g id="plot{}-value{}"><title>{} ± {} {}</title>'
+
+class Plot(rst.Directive):
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = True
+    option_spec = {'class': directives.class_option,
+                   'name': directives.unchanged,
+                   'type': directives.unchanged_required,
+                   'labels': directives.unchanged_required,
+                   'labels_extra': directives.unchanged,
+                   'units': directives.unchanged_required,
+                   'values': directives.unchanged_required,
+                   'errors': directives.unchanged,
+                   'colors': directives.unchanged,
+                   'bar_height': directives.unchanged}
+    has_content = False
+
+    def run(self):
+        set_classes(self.options)
+
+        # Type
+        assert self.options['type'] == 'barh'
+
+        # Graph title and axis labels. Value labels are one per line.
+        title = self.arguments[0]
+        units = self.options['units']
+        labels = self.options['labels'].split('\n')
+
+        # Optional extra labels
+        if 'labels_extra' in self.options:
+            labels_extra = self.options['labels_extra'].split('\n')
+            assert len(labels_extra) == len(labels)
+        else:
+            labels_extra = None
+
+        # Values. Should be one for each label.
+        values = [float(v) for v in self.options['values'].split()]
+        assert len(values) == len(labels)
+
+        # Optional errors
+        if 'errors' in self.options:
+            errors = [float(e) for e in self.options['errors'].split()]
+        else:
+            errors = None
+
+        # Colors. Should be either one for all or one for every value
+        colors = [style_mapping[c] for c in self.options.get('colors', 'default').split()]
+        if len(colors) == 1: colors = colors[0]
+        else: assert len(colors) == len(labels)
+
+        # Bar height
+        bar_height = float(self.options.get('bar_height', '0.4'))
+
+        # Increase hashsalt for every plot to ensure (hopefully) unique SVG IDs
+        mpl.rcParams['svg.hashsalt'] = int(mpl.rcParams['svg.hashsalt']) + 1
+
+        # Setup the graph
+        fig, ax = plt.subplots()
+        # TODO: let matplotlib calculate the height somehow
+        fig.set_size_inches(8, 0.78 + len(values)*bar_height)
+        yticks = np.arange(len(labels))
+        plot = ax.barh(yticks, values, xerr=errors,
+                       align='center', color=colors, ecolor='#cafe0a', capsize=5*bar_height/0.4)
+        for i, v in enumerate(plot):
+            v.set_gid('plot{}-value{}'.format(mpl.rcParams['svg.hashsalt'], i))
+        ax.set_yticks(yticks)
+        ax.invert_yaxis() # top-to-bottom
+        ax.set_xlabel(units)
+        ax.set_title(title)
+
+        # Value labels. If extra label is specified, create two multiline texts
+        # with first having the second line empty and second having the first
+        # line empty.
+        if labels_extra:
+            ax.set_yticklabels([y + '\n' for y in labels])
+            for i, label in enumerate(ax.get_yticklabels()):
+                ax.text(0, i + 0.05, '\n' + labels_extra[i],
+                        va='center', ha='right',
+                        transform=label.get_transform(), color='#cafe0b')
+        else: ax.set_yticklabels(labels)
+
+        # Export to SVG
+        fig.patch.set_visible(False) # hide the white background
+        imgdata = io.StringIO()
+        fig.savefig(imgdata, format='svg')
+
+        # Patch the rendered output: remove preable and hardcoded size
+        imgdata = _patch_src.sub(_patch_dst, imgdata.getvalue())
+        # Remove needless newlines and trailing whitespace in path data
+        imgdata = _path_patch2_src.sub(_path_patch2_dst, _path_patch_src.sub(_path_patch_dst, imgdata))
+        # Replace color codes with CSS classes
+        for src, dst in _class_mapping: imgdata = imgdata.replace(src, dst)
+        # Add titles for bars
+        for i in range(len(values)):
+            if errors: imgdata = imgdata.replace(
+                _bar_titles_src.format(mpl.rcParams['svg.hashsalt'], i),
+                _bar_titles_dst_error.format(mpl.rcParams['svg.hashsalt'], i, values[i], errors[i], units))
+            else: imgdata = imgdata.replace(
+                _bar_titles_src.format(mpl.rcParams['svg.hashsalt'], i),
+                _bar_titles_dst.format(mpl.rcParams['svg.hashsalt'], i, values[i], units))
+
+        container = nodes.container(**self.options)
+        container['classes'] += ['m-plot']
+        node = nodes.raw('', imgdata, format='html')
+        container.append(node)
+        return [container]
+
+def new_page(content):
+    mpl.rcParams['svg.hashsalt'] = 0
+
+def register():
+    pelican.signals.content_object_init.connect(new_page)
+    rst.directives.register_directive('plot', Plot)
diff --git a/pelican-plugins/m/test/plots/page.html b/pelican-plugins/m/test/plots/page.html
new file mode 100644 (file)
index 0000000..833ee8e
--- /dev/null
@@ -0,0 +1,349 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>m.plots | A Pelican Blog</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i" />
+  <link rel="stylesheet" href="static/m-dark.css" />
+  <link rel="canonical" href="page.html" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="./" id="m-navbar-brand" class="m-col-t-9 m-col-m-none m-left-m">A Pelican Blog</a>
+    </div>
+  </div>
+</nav></header>
+<main>
+<article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>m.plots</h1>
+<!-- content -->
+<div class="m-plot">
+<svg viewBox="0 0 576 113.76">
+ <defs>
+  <style type="text/css">
+*{stroke-linecap:butt;stroke-linejoin:round;}
+  </style>
+ </defs>
+ <g id="figure_1">
+  <g id="axes_1">
+   <g id="patch_1">
+    <path d="M 53.31 70.705312 L 560.62 70.705312 L 560.62 27.136406 L 53.31 27.136406 z" class="m-background"/>
+   </g>
+   <g id="plot1-value0"><title>15.0 meters, i guess?</title>
+    <path clip-path="url(#p87df164df2)" d="M 53.31 29.116811 L 294.88619 29.116811 L 294.88619 46.72041 L 53.31 46.72041 z" class="m-bar m-success"/>
+   </g>
+   <g id="plot1-value1"><title>30.0 meters, i guess?</title>
+    <path clip-path="url(#p87df164df2)" d="M 53.31 51.121309 L 536.462381 51.121309 L 536.462381 68.724908 L 53.31 68.724908 z" class="m-bar m-success"/>
+   </g>
+   <g id="matplotlib.axis_1">
+    <g id="xtick_1">
+     <g id="line2d_1">
+      <defs>
+       <path d="M 0 0 L 0 3.5" id="m15f0a304df" class="m-line"/>
+      </defs>
+      <g>
+       <use x="53.31" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_1">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 53.31, 85.537656)" x="53.31" y="85.537656">0</text>
+     </g>
+    </g>
+    <g id="xtick_2">
+     <g id="line2d_2">
+      <g>
+       <use x="133.835397" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_2">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 133.835397, 85.537656)" x="133.835397" y="85.537656">5</text>
+     </g>
+    </g>
+    <g id="xtick_3">
+     <g id="line2d_3">
+      <g>
+       <use x="214.360794" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_3">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 214.360794, 85.537656)" x="214.360794" y="85.537656">10</text>
+     </g>
+    </g>
+    <g id="xtick_4">
+     <g id="line2d_4">
+      <g>
+       <use x="294.88619" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_4">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 294.88619, 85.537656)" x="294.88619" y="85.537656">15</text>
+     </g>
+    </g>
+    <g id="xtick_5">
+     <g id="line2d_5">
+      <g>
+       <use x="375.411587" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_5">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 375.411587, 85.537656)" x="375.411587" y="85.537656">20</text>
+     </g>
+    </g>
+    <g id="xtick_6">
+     <g id="line2d_6">
+      <g>
+       <use x="455.936984" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_6">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 455.936984, 85.537656)" x="455.936984" y="85.537656">25</text>
+     </g>
+    </g>
+    <g id="xtick_7">
+     <g id="line2d_7">
+      <g>
+       <use x="536.462381" xlink:href="#m15f0a304df" y="70.705312"/>
+      </g>
+     </g>
+     <g id="text_7">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 536.462381, 85.537656)" x="536.462381" y="85.537656">30</text>
+     </g>
+    </g>
+    <g id="text_8">
+     <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 306.965, 99.415312)" x="306.965" y="99.415312">meters, i guess?</text>
+    </g>
+   </g>
+   <g id="matplotlib.axis_2">
+    <g id="ytick_1">
+     <g id="line2d_8">
+      <defs>
+       <path d="M 0 0 L -3.5 0" id="mba4ce04b6c" class="m-line"/>
+      </defs>
+      <g>
+       <use x="53.31" xlink:href="#mba4ce04b6c" y="37.91861"/>
+      </g>
+     </g>
+     <g id="text_9">
+      <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 46.31, 41.834782)" x="46.31" y="41.834782">First</text>
+     </g>
+    </g>
+    <g id="ytick_2">
+     <g id="line2d_9">
+      <g>
+       <use x="53.31" xlink:href="#mba4ce04b6c" y="59.923108"/>
+      </g>
+     </g>
+     <g id="text_10">
+      <text class="m-label" style="text-anchor:end;" transform="rotate(-0, 46.31, 63.83928)" x="46.31" y="63.83928">Second</text>
+     </g>
+    </g>
+   </g>
+   <g id="text_11">
+    <text class="m-title" style="text-anchor:middle;" transform="rotate(-0, 306.965, 21.136406)" x="306.965" y="21.136406">A plot with a single color</text>
+   </g>
+  </g>
+ </g>
+ <defs>
+  <clipPath id="p87df164df2">
+   <rect height="43.568906" width="507.31" x="53.31" y="27.136406"/>
+  </clipPath>
+ </defs>
+</svg>
+</div>
+<div class="m-plot">
+<svg viewBox="0 0 576 218.16">
+ <defs>
+  <style type="text/css">
+*{stroke-linecap:butt;stroke-linejoin:round;}
+  </style>
+ </defs>
+ <g id="figure_1">
+  <g id="axes_1">
+   <g id="patch_1">
+    <path d="M 60.154063 175.060625 L 560.62 175.060625 L 560.62 27.136406 L 60.154063 27.136406 z" class="m-background"/>
+   </g>
+   <g id="plot2-value0"><title>3.0 ± 0.1 Mondays</title>
+    <path clip-path="url(#p0d12d49cf2)" d="M 60.154063 33.860234 L 294.564338 33.860234 L 294.564338 72.282109 L 60.154063 72.282109 z" class="m-bar m-success"/>
+   </g>
+   <g id="plot2-value1"><title>4.0 ± 2.1 Mondays</title>
+    <path clip-path="url(#p0d12d49cf2)" d="M 60.154063 81.887578 L 372.701096 81.887578 L 372.701096 120.309453 L 60.154063 120.309453 z" class="m-bar m-info"/>
+   </g>
+   <g id="plot2-value2"><title>5.0 ± 1.0 Mondays</title>
+    <path clip-path="url(#p0d12d49cf2)" d="M 60.154063 129.914922 L 450.837854 129.914922 L 450.837854 168.336797 L 60.154063 168.336797 z" class="m-bar m-danger"/>
+   </g>
+   <g id="matplotlib.axis_1">
+    <g id="xtick_1">
+     <g id="line2d_1">
+      <defs>
+       <path d="M 0 0 L 0 3.5" id="m7a9c636c50" class="m-line"/>
+      </defs>
+      <g>
+       <use x="60.154063" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_1">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 60.154063, 189.892969)" x="60.154063" y="189.892969">0</text>
+     </g>
+    </g>
+    <g id="xtick_2">
+     <g id="line2d_2">
+      <g>
+       <use x="138.290821" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_2">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 138.290821, 189.892969)" x="138.290821" y="189.892969">1</text>
+     </g>
+    </g>
+    <g id="xtick_3">
+     <g id="line2d_3">
+      <g>
+       <use x="216.427579" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_3">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 216.427579, 189.892969)" x="216.427579" y="189.892969">2</text>
+     </g>
+    </g>
+    <g id="xtick_4">
+     <g id="line2d_4">
+      <g>
+       <use x="294.564338" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_4">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 294.564338, 189.892969)" x="294.564338" y="189.892969">3</text>
+     </g>
+    </g>
+    <g id="xtick_5">
+     <g id="line2d_5">
+      <g>
+       <use x="372.701096" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_5">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 372.701096, 189.892969)" x="372.701096" y="189.892969">4</text>
+     </g>
+    </g>
+    <g id="xtick_6">
+     <g id="line2d_6">
+      <g>
+       <use x="450.837854" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_6">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 450.837854, 189.892969)" x="450.837854" y="189.892969">5</text>
+     </g>
+    </g>
+    <g id="xtick_7">
+     <g id="line2d_7">
+      <g>
+       <use x="528.974613" xlink:href="#m7a9c636c50" y="175.060625"/>
+      </g>
+     </g>
+     <g id="text_7">
+      <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 528.974613, 189.892969)" x="528.974613" y="189.892969">6</text>
+     </g>
+    </g>
+    <g id="text_8">
+     <text class="m-label" style="text-anchor:middle;" transform="rotate(-0, 310.387031, 203.980313)" x="310.387031" y="203.980313">Mondays</text>
+    </g>
+   </g>
+   <g id="matplotlib.axis_2">
+    <g id="ytick_1">
+     <g id="line2d_8">
+      <defs>
+       <path d="M 0 0 L -3.5 0" id="m03b20617ea" class="m-line"/>
+      </defs>
+      <g>
+       <use x="60.154063" xlink:href="#m03b20617ea" y="53.071172"/>
+      </g>
+     </g>
+     <g id="text_9">
+      <text class="m-label" transform="translate(15.827969 51.138094)">January</text>
+      <text class="m-label" transform="translate(53.154063 62.836594)"/>
+     </g>
+    </g>
+    <g id="ytick_2">
+     <g id="line2d_9">
+      <g>
+       <use x="60.154063" xlink:href="#m03b20617ea" y="101.098516"/>
+      </g>
+     </g>
+     <g id="text_10">
+      <text class="m-label" transform="translate(11.88 99.210125)">February</text>
+      <text class="m-label" transform="translate(53.154063 110.908625)"/>
+     </g>
+    </g>
+    <g id="ytick_3">
+     <g id="line2d_10">
+      <g>
+       <use x="60.154063" xlink:href="#m03b20617ea" y="149.125859"/>
+      </g>
+     </g>
+     <g id="text_11">
+      <text class="m-label" transform="translate(24.794688 147.215125)">March</text>
+      <text class="m-label" transform="translate(53.154063 158.868938)"/>
+     </g>
+    </g>
+   </g>
+   <g id="LineCollection_1">
+    <path clip-path="url(#p0d12d49cf2)" d="M 286.750662 53.071172 L 302.378014 53.071172" class="m-error"/>
+    <path clip-path="url(#p0d12d49cf2)" d="M 208.613903 101.098516 L 536.788289 101.098516" class="m-error"/>
+    <path clip-path="url(#p0d12d49cf2)" d="M 372.701096 149.125859 L 528.974613 149.125859" class="m-error"/>
+   </g>
+   <g id="line2d_11">
+    <defs>
+     <path d="M 0 9.375 L 0 -9.375" id="m63fa77ade3" class="m-error"/>
+    </defs>
+    <g clip-path="url(#p0d12d49cf2)">
+     <use x="286.750662" xlink:href="#m63fa77ade3" y="53.071172"/>
+     <use x="208.613903" xlink:href="#m63fa77ade3" y="101.098516"/>
+     <use x="372.701096" xlink:href="#m63fa77ade3" y="149.125859"/>
+    </g>
+   </g>
+   <g id="line2d_12">
+    <g clip-path="url(#p0d12d49cf2)">
+     <use x="302.378014" xlink:href="#m63fa77ade3" y="53.071172"/>
+     <use x="536.788289" xlink:href="#m63fa77ade3" y="101.098516"/>
+     <use x="528.974613" xlink:href="#m63fa77ade3" y="149.125859"/>
+    </g>
+   </g>
+   <g id="text_12">
+    <text class="m-label m-dim" transform="translate(53.154063 52.434305)"/>
+    <text class="m-label m-dim" transform="translate(5.522344 64.088117)">a paradise</text>
+   </g>
+   <g id="text_13">
+    <text class="m-label m-dim" transform="translate(53.154063 100.416961)"/>
+    <text class="m-label m-dim" transform="translate(31.064688 112.070773)">okay</text>
+   </g>
+   <g id="text_14">
+    <text class="m-label m-dim" transform="translate(53.154063 148.488992)"/>
+    <text class="m-label m-dim" transform="translate(32.924375 160.142805)">hell!</text>
+   </g>
+   <g id="text_15">
+    <text class="m-title" style="text-anchor:middle;" transform="rotate(-0, 310.387031, 21.136406)" x="310.387031" y="21.136406">A plot with separate colors, extra labels, error bars and custom height</text>
+   </g>
+  </g>
+ </g>
+ <defs>
+  <clipPath id="p0d12d49cf2">
+   <rect height="147.924219" width="500.465938" x="60.154063" y="27.136406"/>
+  </clipPath>
+ </defs>
+</svg>
+</div>
+<!-- /content -->
+      </div>
+    </div>
+  </div>
+</article>
+</main>
+</body>
+</html>
diff --git a/pelican-plugins/m/test/plots/page.rst b/pelican-plugins/m/test/plots/page.rst
new file mode 100644 (file)
index 0000000..805c81a
--- /dev/null
@@ -0,0 +1,27 @@
+m.plots
+#######
+
+.. plot:: A plot with a single color
+    :type: barh
+    :labels:
+        First
+        Second
+    :units: meters, i guess?
+    :values: 15 30
+    :colors: success
+
+.. plot:: A plot with separate colors, extra labels, error bars and custom height
+    :type: barh
+    :labels:
+        January
+        February
+        March
+    :labels_extra:
+        a paradise
+        okay
+        hell!
+    :units: Mondays
+    :values: 3 4 5
+    :errors: 0.1 2.1 1.0
+    :colors: success info danger
+    :bar_height: 0.75
diff --git a/pelican-plugins/m/test/test_plots.py b/pelican-plugins/m/test/test_plots.py
new file mode 100644 (file)
index 0000000..a7d011a
--- /dev/null
@@ -0,0 +1,36 @@
+#
+#   This file is part of m.css.
+#
+#   Copyright © 2017, 2018 Vladimír Vondruš <mosra@centrum.cz>
+#
+#   Permission is hereby granted, free of charge, to any person obtaining a
+#   copy of this software and associated documentation files (the "Software"),
+#   to deal in the Software without restriction, including without limitation
+#   the rights to use, copy, modify, merge, publish, distribute, sublicense,
+#   and/or sell copies of the Software, and to permit persons to whom the
+#   Software is furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included
+#   in all copies or substantial portions of the Software.
+#
+#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+#   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+#   DEALINGS IN THE SOFTWARE.
+#
+
+from . import PluginTestCase
+
+class Plots(PluginTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, '', *args, **kwargs)
+
+    def test(self):
+        self.run_pelican({
+            'PLUGINS': ['m.htmlsanity', 'm.plots']
+        })
+
+        self.assertEqual(*self.actual_expected_contents('page.html'))
index 226f3614833b8bdd348e322dae9357e98f6d5089..55b845bd42626d17b8d77e532d937a1b7653b1d0 100644 (file)
@@ -88,6 +88,7 @@ M_LINKS_NAVBAR2 = [('Pelican plugins', 'plugins/', 'plugins', [
                         ('Images', 'plugins/images/', 'plugins/images'),
                         ('Math and code', 'plugins/math-and-code/', 'plugins/math-and-code'),
                         ('Links and other', 'plugins/links/', 'plugins/links'),
+                        ('Plots', 'plugins/plots/', 'plugins/plots'),
                         ('Metadata', 'plugins/metadata/', 'plugins/metadata')]),
                    ('Doxygen theme', 'doxygen/', 'doxygen', []),
                    ('GitHub', 'https://github.com/mosra/m.css', '', [])]
@@ -118,6 +119,7 @@ M_LINKS_FOOTER4 = [('Pelican plugins', 'plugins/'),
                    ('Components', 'plugins/components/'),
                    ('Images', 'plugins/images/'),
                    ('Math and code', 'plugins/math-and-code/'),
+                   ('Plots', 'plugins/plots/'),
                    ('Links and other', 'plugins/links/'),
                    ('Metadata', 'plugins/metadata/')]
 
@@ -145,7 +147,8 @@ PLUGINS = ['m.abbr',
            'm.htmlsanity',
            'm.images',
            'm.math',
-           'm.metadata']
+           'm.metadata',
+           'm.plots']
 
 THEME = '../pelican-theme'
 THEME_STATIC_DIR = 'static'