Matplotlib-Simple
view release on metacpan or search on metacpan
plt(
'output.file' => '/tmp/gospel.word.counts.png',
'plot.type' => 'bar',
data => {
Matthew => 18345,
Mark => 11304,
Luke => 19482,
John => 15635,
}
);
<img width="651" height="491" alt="gospel word counts" src="https://github.com/user-attachments/assets/a008dece-2e34-47bf-af0f-8603709f7d52" />
# Multiple Plots
Having a `plots` argument as an array lets the module know to create subplots:
use Matplotlib::Simple 'plt';
plt(
'output.file' => 'svg/pies.png',
plots => [
{
data => {
Russian => 106_000_000, # Primarily European Russia
German => 95_000_000, # Germany, Austria, Switzerland, etc.
},
'plot.type' => 'pie',
title => 'Top Languages in Europe',
suptitle => 'Pie in subplots',
},
{
data => {
Russian => 106_000_000, # Primarily European Russia
German => 95_000_000, # Germany, Austria, Switzerland, etc.
},
'plot.type' => 'pie',
title => 'Top Languages in Europe',
},
],
ncols => 2,
);
which produces the following subplots image:
<img width="651" height="424" alt="pies" src="https://github.com/user-attachments/assets/49d3e28b-f897-4b01-9e72-38afa12fa538" />
`bar`, `barh`, `boxplot`, `hexbin`, `hist`, `hist2d`, `imshow`, `pie`, `plot`, `scatter`, and `violinplot` all match the methods in matplotlib itself.
## The `p` argument
`p` is a single, uniform way to describe one *or* many subplots, so you no
longer need a top-level `plot.type` (or the older `plots` array). Each plot is a
hash, exactly like a single-plot call, and `p` collects the subplots into one
array.
The rule is simple: **one element of `p` is one subplot.**
- A **hash** element is a subplot containing a single plot.
- An **array of hashes** element is a single subplot whose plots are drawn on
the **same** axes: the first hash is the base plot and the rest are
*additions* (overlays), exactly like `add`.
The two forms may be **mixed freely** in the same `p`. The first (or only) hash
of a subplot supplies that subplot's axes-level options (`title`, `xlabel`,
`ylabel`, `legend`, â¦).
If you don't give a grid, the subplots are laid out automatically on a
near-square grid. Give `ncol`/`nrow` (aliases for `ncols`/`nrows`) to control
it; supplying only one dimension derives the other (so `ncols => 1` stacks the
subplots in a single column), and supplying both is honored as given.
`p` cannot be combined with `plot.type`, `data`, `plots`, or `add`.
> Arguments are now passed as a plain list â `plt( ... )` â though the older
> `plt({ ... })` form still works.
### One subplot, several plots overlaid
Wrap the plots in an inner array and they all land on a single subplot (the
first is the base plot, the rest are additions):
plt(
p => [
[
{
data => {
E => [ 55, @{$x}, 160 ],
B => [ @{$y}, 140 ],
},
'plot.type' => 'boxplot',
title => 'Single Box Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple' },
},
{
data => {
A => [ 55, @{$z} ],
E => [ @{$y} ],
B => [ 122, @{$z} ],
},
'plot.type' => 'violinplot',
title => 'Single Violin Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple', A => 'green' },
},
],
],
'output.file' => '1plot.svg', # note: no `plot.type` needed
);
### Multiple subplots
Give each subplot as its own element. A bare hash is a one-plot subplot, so two
hashes make two subplots; with `ncol => 2` they sit side by side:
plt(
p => [
{
data => {
E => [ 55, @{$x}, 160 ],
B => [ @{$y}, 140 ],
},
'plot.type' => 'boxplot',
title => 'Box Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple' },
},
{
data => {
A => [ 55, @{$z} ],
E => [ @{$y} ],
B => [ 122, @{$z} ],
},
'plot.type' => 'violinplot',
title => 'Violin Plot: Specified Colors',
colors => { E => 'yellow', B => 'purple', A => 'green' },
},
],
ncol => 2,
'output.file' => '2plots.svg',
);
To overlay extra plots on any one subplot, make that element an array of hashes
instead of a bare hash (the first is the plot, the rest are additions). Bare
hashes and inner arrays may be intermixed in the same `p`, for example
`p => [ \%single, [ \%base, \%overlay ], \%another ]`.
## Options
`sharex` and `sharey` are both implemented at the plot, rather than subplot, level. See Matplotlib's documentation for more clarity.
# Color Bars (colorbars)
Colarbar args attempt to match matplotlib closely
| Option | Description | Example |
| -------- | ------- | -------
|`cbdrawedges` | Whether to draw lines at color boundaries | `cbdrawedges => 1`|
|`cblabel` | The label on the colorbar's long axis | `cblabel => 1` |
|`cblocation` | of the colorbar None or {'left', 'right', 'top', 'bottom'} | |
|`cborientation` | # None or {`vertical`, `horizontal`} |
|`cbpad` | pad : float, default: 0.05 if vertical, 0.15 if horizontal; Fraction of original Axes between colorbar and new image Axes
|`cb_logscale` | Perl true (anything but 0) or false (0)| |
|`shared.colorbar` | share colorbar between different plots: specify plot indices | `'shared.colorbar' => [0,1]`|
# Size/Dimensions of output file
| Option | Description | Example |
| -------- | ------- | ------- |
|`scale` | scale/multiply the size of the output figure | `scale => 2.4`|
|`scalex` | scale/multiply the x-axis only | `scalex => 2.4` |
|`scaley` | scale/multiply the y-axis only | `scalex => 1.4` |
# Examples/Plot Types
Consider the following helper subroutines to generate data to plot:
sub linspace { # mostly written by Grok
my ($start, $stop, $num, $endpoint) = @_; # endpoint means include $stop
$num = defined $num ? int($num) : 50; # Default to 50 points
$endpoint = defined $endpoint ? $endpoint : 1; # Default to include endpoint
return () if $num < 0; # Return empty array for invalid num
return ($start) if $num == 1; # Return single value if num is 1
my (@result, $step);
if ($endpoint) {
$step = ($stop - $start) / ($num - 1) if $num > 1;
for my $i (0 .. $num - 1) {
$result[$i] = $start + $i * $step;
}
} else {
$step = ($stop - $start) / $num;
for my $i (0 .. $num - 1) {
$result[$i] = $start + $i * $step;
}
}
return @result;
}
sub generate_normal_dist {
my ($mean, $std_dev, $size) = @_;
$size = defined $size ? int $size : 100; # default to 100 points
my @numbers;
for (1 .. int($size / 2) + 1) {# Box-Muller transform
my $u1 = rand();
my $u2 = rand();
[ [5..9], [1..5] ],
],
'set.options' => 'linewidth = 2', # both lines
}
**An array sets options per line (positional).** With array data, give one
string per line; entry `i` styles line `i`. You may supply fewer entries than
lines, but not more:
{
'plot.type' => 'plot',
data => [
[ [5..9], [5..9] ],
[ [5..9], [1..5] ],
],
'set.options' => [
'color = "red"',
'color = "blue", linestyle = "--"',
],
}
**A hash sets options per key.** With hash data, key the options by the same
data keys (any key may be omitted):
{
'plot.type' => 'plot',
data => {
A => [ [5..9], [5..9] ],
B => [ [5..9], [1..5] ],
},
'set.options' => {
A => 'color = "red"',
B => 'color = "blue", marker = "o"',
},
}
Note the pairing rule: a scalar `set.options` goes with **any** data shape; an
**array** `set.options` goes with **array** data; a **hash** `set.options` goes
with **hash** data. Mismatches (for example a hash of options with array data)
are rejected with an explanatory error.
### Other options
- `show.legend` â on by default (`1`); set to `0` to suppress labels. Only the
hash form produces labels in the first place.
- `key.order` â array of keys (hash form) fixing the draw/legend order; defaults
to the keys sorted alphabetically.
- `logscale` â array of axes to put on a log scale, e.g. `[ 'x', 'y' ]`.
- `twinx` â draw selected series against a secondary y-axis.
- hash data: a single key, or a hash whose keys are the series to twin;
- array data: an integer index, or an array of indices.
- `twinx.args` â a hash keyed by data key (hash form) or index (array form);
each value is a hash of axis options (e.g. `ylabel`, `set_ylim`) applied to
that twin axis.
Common axes options such as `title`, `xlabel`, `ylabel`, and `legend` are
accepted here too, exactly as for the other plot types.
A `plot` spec is an ordinary plot hash, so it can be dropped straight into the
[`p`](#the-p-argument) argument â on its own for a single subplot, or alongside
other hashes to overlay or to fill a grid of subplots.
### single, simple
data can be given as a hash, where the hash key is the label:
plt(
fh => $fh,
execute => 0,
'output.file' => 'output.images/plot.single.png',
data => {
'sin(x)' => [
[@x], # x
[ map { sin($_) } @x ] # y
],
'cos(x)' => [
[@x], # x
[ map { cos($_) } @x ] # y
],
},
'plot.type' => 'plot',
title => 'simple plot',
set_xticks =>
"[-2 * $pi, -3 * $pi / 2, -$pi, -$pi / 2, 0, $pi / 2, $pi, 3 * $pi / 2, 2 * $pi"
. '], [r\'$-2\pi$\', r\'$-3\pi/2$\', r\'$-\pi$\', r\'$-\pi/2$\', r\'$0$\', r\'$\pi/2$\', r\'$\pi$\', r\'$3\pi/2$\', r\'$2\pi$\']',
'set.options' => { # set options overrides global settings
'sin(x)' => 'color="blue", linewidth=2',
'cos(x)' => 'color="red", linewidth=2'
}
);
or as an array of arrays:
plt(
fh => $fh,
execute => 0,
'output.file' => 'output.images/plot.single.arr.png',
data => [
[
[@x], # x
[ map { sin($_) } @x ] # y
],
[
[@x], # x
[ map { cos($_) } @x ] # y
],
],
'plot.type' => 'plot',
title => 'simple plot',
set_xticks =>
"[-2 * $pi, -3 * $pi / 2, -$pi, -$pi / 2, 0, $pi / 2, $pi, 3 * $pi / 2, 2 * $pi"
. '], [r\'$-2\pi$\', r\'$-3\pi/2$\', r\'$-\pi$\', r\'$-\pi/2$\', r\'$0$\', r\'$\pi/2$\', r\'$\pi$\', r\'$3\pi/2$\', r\'$2\pi$\']',
'set.options' => [ # set options overrides global settings; indices match data array
'color="blue", linewidth=2, label = "sin(x)"', # labels aren't added automatically when using array here
'color="red", linewidth=2, label = "cos(x)"'
],
);
both of which make the following "plot" plot:
<img width="651" height="491" alt="plot single" src="https://github.com/user-attachments/assets/6cbd6aad-c464-4703-b962-b420ec08bb66" />
Better warning when color key isn't defined for `scatter`
When giving two hash of hashes for a barplot, if one second key is defined in one subplot, but not the other, that subkey is initialized to 0.
### Cross-platform support
The module now should run on Windows in addition to Linux and macOS.
The generated Python script is written to the system temporary directory (via `File::Spec->tmpdir()`) instead of a hard-coded `/tmp`, which does not exist on Windows.
The Python interpreter is now discovered automatically by probing, in order, `python3`, `python`, and the Windows `py` launcher, accepting the first that reports Python 3. This fixes Windows, where the interpreter is typically named `python` (not `py...
The Python script is now executed with the list form of `system` rather than a single shell string, so script paths containing spaces (common on Windows, e.g. `C:\Users\First Last\AppData\Local\Temp`) no longer break execution.
The `Creator` metadata embedded in the output file is now passed through `write_data` (base64), so Windows paths containing backslashes no longer produce invalid escape sequences (e.g. `\U` in `C:\Users`) in the generated Python string literal.
On Windows, `Win32::Console::ANSI` is loaded if available (it is optional, not a hard dependency) so colored status messages render on legacy consoles.
### Crashes / generated-code fixes
`violinplot` is now a callable wrapper; it was exported and dispatched but never defined, so calling it died with "Undefined subroutine".
`hist` with an array of `bins` no longer emits a stray double-quote (e.g. `[0,2,4"]`) that caused a Python `SyntaxError`.
`hexbin` and `hist2d` no longer pass `cblabel` twice (once inside the option string and again as `label => ...`), which previously caused a duplicate-keyword `SyntaxError`.
`scatter` with a scalar `set.options` no longer emits a doubled comma (`scatter(x, y, , ...)`), which was a `SyntaxError`.
Stacked `barh` now uses the `left` keyword for stacking instead of `bottom`, which collided with `barh`'s own `bottom` (y-position) parameter and raised "got multiple values for keyword argument 'bottom'".
`colored_table` with `cb_logscale` together with `cb_min`/`cb_max` no longer emits `LogNorm(, vmin=...)` with a leading comma (a `SyntaxError`).
`plot` with a hash of data and a scalar `set.options` no longer crashes by dereferencing a string as a hash under `strict refs`.
`plot` with a hash of data now accepts a scalar `twinx` naming a data key (e.g. `twinx => 'pressure'`); previously the value was wrongly required to be a digit string, making key-named `twinx` impossible.
Grouped bar plots with a single scalar `color` (e.g. `color => 'green'`) no longer crash trying to dereference the string as an array; the color is applied to all series.
### Incorrect-output fixes
`colored_table` no longer clobbers asymmetric data: filling undefined cells with `np.nan` previously also overwrote the mirror cell, destroying defined values (if `A->B` was defined but `B->A` was not, both became `NaN`).
`colored_table` now honors `cb_min` and `cb_max`; they were read from the wrong hash (`$args` instead of the plot options) and so were silently ignored.
`colored_table` now honors the `cmap` option; the color map and `set_bad` color were hard-coded to `gist_rainbow` regardless of the `cmap` given. The colormap is copied before calling `set_bad`, as registered colormaps are immutable in current matplo...
`colored_table` default row labels now mirror the column labels, matching the matrix that is actually built; with asymmetric data the old default could produce a row-label count mismatch ("'rowLabels' must be of length N").
`scatter` (single set, three keys) now honors the `cmap` option instead of always using `gist_rainbow`.
`scatter` now validates undefined values in *both* coordinate keys; the undefined-data check previously inspected only the first key.
Grouped, non-stacked bar widths are now divided by the number of bar series (plus one), not by a constant; the old divisor came from a hash that always held exactly one key, so groups with more than a few series overlapped their neighbors.
The `wide` plot no longer clamps the upper standard-deviation band at `1`; that clamp assumed data in the range `[0, 1]` and clipped ordinary data (the documented example reaches roughly `1.9`).
Numeric arguments to `plt` methods (e.g. `margins => 0.2`) are no longer quoted into strings; `print_type` now recognizes numbers.
`plt.show()` is now emitted after `plt.savefig()` (and only once), so using `show` no longer writes the file only after the interactive window is closed; `output.file` is no longer required when `show` is requested.
The `add` overlay's `plot.type` now correctly falls back to the parent plot's type when omitted, in both single- and multi-plot calls; the fallback was previously unreachable dead code, and an undefined type could be dispatched on.
### Cleanups
Removed corrupted entries from the method whitelists (`'set_mouseover( '` and a leading-space `' FixedFormatter'`) that made those options unusable.
Removed a stray default applied to the wrong hash in `violin`, two empty dead `if` blocks, and a duplicated `die`.
## 0.27
Better warnings for undefined data in `scatter`
`color_key` didn't work properly for multiple sets of data in `scatter`, which has now been fixed
## 0.26
`ncol` & `nrow` are synonymous with `ncols` and `nrows` respectively; testing now reflects these two specifically numeric options
no longer exports Data::Printer and Devel::Confess with the module, but is still used inside the module
'show.legend' option added to "hist", which is automatically turned off if there is only 1 group
"add" group is no longer deleted
"boxplot", "hist", and "violin" can take a single array, simplifying calls without requiring useless single keys when calling a single distribution
`cb_min` and `cb_max` now work for colored_table
"write_data" is no longer used in hist, as it prints numbers as strings (python3's types are a headache)
Instead, all values are checked in hist for being numeric before being sent to "write_data"
re-use undefined error array in hist_helper (slightly less RAM use)
## 0.25
re-used error array in scatter_helper
better warnings for undefined values in multiple-set scatterplots
fixed bug in scatterplot, where different sets would have the same label
"logscale" now available with "boxplot, "hist", "plot", "scatter"
$VERSION now prints with metadata for SVG output files, which required minor changes to testing
slightly better warnings in plot_helper
removed duplicate check from hist2d_helper
better warnings if wrong data types are given to "add"
Fixed bug in scatterplot, where color key could repeat on axes
colorbar can now be in logscale for colored_table
---
## 0.24
---
Newlines are now possible in key names for barplot and pie; other characters may be fixed too
@prop_cycle is only now taking RAM/valid where it's needed
( run in 0.958 second using v1.01-cache-2.11-cpan-7fcb06a456a )