# -*- coding: utf-8 -*-
"""Python API for vfd"""
import json
from glob import glob
import os
import subprocess
import logging
import io
import sys
import re
from jsonschema import validate as validate_schema
import xlsxwriter
try:
import matplotlib.pyplot as plt
except ModuleNotFoundError:
plt = None
logging.basicConfig(level=logging.INFO)
logger = logging.Logger("vfd")
default_colors = ['#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B', '#E377C2', '#7F7F7F']
default_lines = ['-', '--', ':', '-.']
default_markers = ['o', 's', '^', 'p', 'v', "d", "P", "*"]
_indentation_size = 4
_float_pattern = '[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?'
schema_style = {
"type": "object",
"properties": {
"lines": {"Description": "List of lines suggested for each index of the plot", "type": "array",
"items": {"type": "string"}},
"colors": {"Description": "List of colors suggested for each index of the plot", "type": "array",
"items": {"type": "string"}},
"markers": {"Description": "List of markers suggested for each index of the plot", "type": "array",
"items": {"type": "string"}}
}
}
schema_added_axis = {
"type": "object",
"range": {"Description": "Range of representation in this axis", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"log": {"Description": "Whether the scale should be logarithmic in this axis", "type": "boolean"},
"label": {"Description": "Label for this added axis", "type": "string"},
"legendlabel": {
"Description": "A title to be placed in the legend of the series related to the axis, when such an attribution "
"makes sense (e.g., when one axis is shared)",
"type": "string"}
}
schema_plot = {
"type": "object",
"properties": {
"type": {"Description": "Reserved keyword. Must be \"plot\"", "type": "string"},
"version": {"Description": "vfd file format version. Reserved for future use", "type": "string"},
"xrange": {"Description": "Range of representation in the x-axis", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"yrange": {"Description": "Range of representation in the y-axis", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"xlog": {"Description": "Whether the scale should be logarithmic in the x-axis", "type": "boolean"},
"ylog": {"Description": "Whether the scale should be logarithmic in the y-axis", "type": "boolean"},
"xlabel": {"Description": "Label for the x-axis", "type": "string"},
"ylabel": {"Description": "Label for the y-axis", "type": "string"},
"xadded": {"Description": "List of added x axes", "type": "array", "items": schema_added_axis},
"yadded": {"Description": "List of added y axes", "type": "array", "items": schema_added_axis},
"title": {"Description": "Title for the plot", "type": "string"},
"legendtitle": {"Description": "A title to be placed in the legend", "type": "string"},
"series": {"description": "Series of data in the plot", "type": "array", "minItems": 1, "items": {
"type": "object", "properties": {
"x": {"description": "x-coordinates of the points of the plot. Assumed integers from 1 if not given",
"type": "array",
"items": {"type": "number"},
},
"y": {"description": "y-coordinates of the points of the plot",
"type": "array",
"items": {"type": "number"},
"minItems": 1, },
"xerr": {"description": "Uncertainty in the x-axis of the points (in each direction)",
"type": "array",
"items": {"type": "number"},
},
"yerr": {"description": "Uncertainty in the y-axis of the points (in each direction)",
"type": "array",
"items": {"type": "number"},
},
"xmin": {"description": "Lower limit of the error bar in the x-axis. Has precedence over xerr",
"type": "array",
"items": {"type": "number"},
},
"xmax": {"description": "Upper limit of the error bar in the x-axis. Has precedence over xerr",
"type": "array",
"items": {"type": "number"},
},
"ymin": {"description": "Lower limit of the error bar in the y-axis. Has precedence over xerr",
"type": "array",
"items": {"type": "number"},
},
"ymax": {"description": "Upper limit of the error bar in the y-axis. Has precedence over xerr",
"type": "array",
"items": {"type": "number"},
},
"xadded": {"description": "Index of the added x axis to use for this series",
"type": "integer"},
"yadded": {"description": "Index of the added y axis to use for this series",
"type": "integer"},
"label": {"description": "Description of the series to be added to the legend if used",
"type": ["string", "number"]},
"color": {"description": "An index representing the color used.",
"type": "integer"},
"line": {"description": "An index representing the line style used. 1 should be a solid line.",
"type": "integer"},
"joined": {
"description": "Whether the data should be joined (representing a continuum) "
"or not (representing the actual given points)",
"type": "boolean"}}}},
"style": schema_style,
},
"required": ["series"]
}
schema_colorplot = {
"type": "object",
"properties": {
"type": {"Description": "Reserved keyword. Must be \"colorplot\"", "type": "string"},
"version": {"Description": "vfd file format version. Reserved for future use", "type": "string"},
"xrange": {"Description": "Range of representation in the x-axis", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"yrange": {"Description": "Range of representation in the y-axis", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"zrange": {"Description": "Range of representation in the color map", "type": "array", "minItems": 2,
"maxItems": 2, "items": {"type": "number"}},
"levels": {"Description": "Levels to plot in a contour plot", "type": "array", "items": {"type": "number"}},
"xlog": {"Description": "Whether the scale should be logarithmic in the x-axis", "type": "boolean"},
"ylog": {"Description": "Whether the scale should be logarithmic in the y-axis", "type": "boolean"},
"zlog": {"Description": "Whether the color scale should be logarithmic", "type": "boolean"},
"xlabel": {"Description": "Label for the x-axis", "type": "string"},
"ylabel": {"Description": "Label for the y-axis", "type": "string"},
"contour": {"Description": "Whether contour should be plotted instead of density", "type": "boolean"},
"fillcontour": {"Description": "When contour is plotted, whether regions should be filled", "type": "boolean"},
"title": {"Description": "Title for the plot", "type": "string"},
"x": {"description": "x-coordinates of the mesh of the plot. Assumed integers from 1 if not given",
"type": "array",
"items": {"type": "number"},
},
"y": {"description": "y-coordinates of the mesh of the plot. Assumed integers from 1 if not given",
"type": "array",
"items": {"type": "number"},
},
"z": {"description": "Matrix of values to plot",
"type": "array",
"items": {"type": "array", "items": {"type": "number"}},
},
"style": schema_style,
},
"required": ["z"]
}
schema_multiplot = {
"type": "object",
"properties": {
"type": {"Description": "Reserved keyword. Must be \"multiplot\"", "type": "string"},
"version": {"Description": "vfd file format version. Reserved for future use", "type": "string"},
"plots": {
"Description": "Bidimensional matrix of plots. Each item of this array is an array with a row of plots",
"type": "array",
"items": {"type": "array", "items": {"anyof": [schema_plot, schema_colorplot]}}},
"title": {"Description": "Title for the plots", "type": "string"},
"xshared": {"Description:": "If x-axis should be shared", "type": "string", "pattern": "^(all|none|row|col)$"},
"yshared": {"Description:": "If y-axis should be shared", "type": "string", "pattern": "^(all|none|row|col)$"},
"joined": {"Description:": "If the subplots should be adjacent in horizontal and vertical respectively",
"type": "array", "minitems": 2, "maxitems": 2, "items": {"type": "boolean"}},
"style": schema_style
},
"required": ["plots"]
}
# TODO: Add epilog to schemas
def _open_write(path):
# io.open seems not to encode properly in windows + python3. Use regular open in py3, just in case.
if sys.version_info < (3, 0):
return io.open(path, "w", encoding='utf8')
else:
return open(path, "w")
def _cycle_property(index, property_list):
return property_list[index % len(property_list)]
def _full_errorbar(values, error_limit, error, positive):
"""
Get the argument needed for plt.errorbar error description.
error_limit has precedence over error. If neither is specified, return a list of zeros.
Args:
values (list): Values of the variable.
error_limit (list or float or None): Value(s) of the upper/lower limit.
error (list or float or None): Value(s) of the uncertainty in the direction.
positive (boolean): Whether it is a positive error.
Returns:
list of float: Uncertainty in the direction for all of the points.
"""
if error_limit is not None:
if isinstance(error_limit, list):
if positive:
return [y2 - y1 for y1, y2 in zip(values, error_limit)]
else:
return [y1 - y2 for y1, y2 in zip(values, error_limit)]
else:
if positive:
return [y1 + error_limit for y1 in values]
else:
return [y1 - error_limit for y1 in values]
elif error is not None:
return error if isinstance(error, list) else [error] * len(values)
else:
return [0] * len(values)
def _get_style(description):
"""
Get the kwargs from a style_schema
Args:
description (dict): A style_schema.
Returns:
dict: A dictionary with the kwargs.
"""
kwargs = {}
if "lines" in description:
kwargs["line_style"] = description["lines"]
if "colors" in description:
kwargs["color_list"] = description["colors"]
if "markers" in description:
kwargs["marker_list"] = description["markers"]
return kwargs
def _create_matplotlib_plot(description, container="plt", current_axes=True, indentation_level=0, marker_list=None,
color_list=None, line_list=None, title_inside=False):
"""
Create code describing a simple plot.
Args:
description (dict): A part of VFD of type "plot", parsed from the JSON.
container (str): The object whose methods are called. E.g., 'plt' for pyplot or an axes.
current_axes (bool): Whether to call the set_* methods of the container or the current axes methods (for 'plt').
indentation_level: Indentation level for the code.
marker_list (list of str): Markers to use cyclically for series which are not joined.
color_list (list): Colors to use when an index requests to do so.
line_list (list of str): Line styles to use when requested.
title_inside (bool): Insert the title as text inside the plot instead as a title. Useful for multiplots.
Returns:
str: Python code which will create the plot.
"""
# Markers will automatically switch always to distinguish the series.
if marker_list is None:
marker_list = default_markers
# The other properties will do under explicit demand (otherwise it's left to matplotlib's style)
if color_list is None:
color_list = default_colors
explicit_colors = False
else:
explicit_colors = True
if line_list is None:
line_list = default_lines
explicit_lines = False
else:
explicit_lines = True
add_legend = False
# Counters for automatic properties:
marker_count = 0
color_count = 0
line_count = 0
code = ""
indentation = " " * (indentation_level * _indentation_size)
# find out twin axes to set
xadded_max = 0
yadded_max = 0
for s in description["series"]:
if "xadded" in s:
xadded_max = max(xadded_max, s["xadded"])
if "yadded" in s:
yadded_max = max(yadded_max, s["yadded"])
if xadded_max == 0 and yadded_max == 0:
# Regular plot
pass
elif xadded_max > 1 or yadded_max > 1:
raise NotImplemented("Two or more added axes are not supported")
else:
if current_axes:
code += indentation + "fig, ax = plt.subplots()\n"
current_axes = False
container = "ax"
if xadded_max == 1:
code += indentation + "twiny = %s.twiny()\n" % container
if yadded_max == 1:
code += indentation + "twinx = %s.twinx()\n" % container
# TODO: Consider the possibility of both axes different
for series_index, s in enumerate(description["series"]):
y = s["y"]
if "x" in s:
args = [s["x"], y]
else:
args = [y]
kwargs = {}
if "label" in s and s["label"]:
kwargs["label"] = s["label"]
add_legend = True
if "color" in s:
kwargs["color"] = _cycle_property(s["color"] - 1, color_list) # User colors start at 1
elif explicit_colors:
kwargs["color"] = _cycle_property(color_count, color_list)
color_count += 1
if "line" in s:
kwargs["linestyle"] = _cycle_property(s["line"] - 1, line_list)
elif explicit_lines:
kwargs["linestyle"] = _cycle_property(line_count, line_list)
line_count += 1
series_container = container
series_xaxis, series_yaxis = 0, 0
if "xadded" in s and s["xadded"] == 1:
series_xaxis = 1
if "yadded" in s and s["yadded"] == 1:
series_yaxis = 1
if series_xaxis and series_yaxis:
raise NotImplemented("Both added axes not yet supported")
elif series_xaxis:
series_container = "twiny"
elif series_yaxis:
series_container = "twinx"
# If a secondary axis is used, color should be specifically set to avoid repetition
if series_xaxis or series_yaxis and "color" not in kwargs:
kwargs["color"] = "C" + str(series_index)
if any([i in s for i in ["xerr", "xmax", "xmin", "yerr", "ymin", "ymax"]]):
# Some kind of error plot
if "joined" in s and s["joined"] and all([i not in s for i in ["xerr", "xmax", "xmin"]]):
# Shadowed region instead of points with error bars
ymin = s["ymin"] if "ymin" in s else [y1 - y2 for y1, y2 in zip(s["y"], s["yerr"])]
ymax = s["ymax"] if "ymax" in s else [y1 + y2 for y1, y2 in zip(s["y"], s["yerr"])]
code += indentation + series_container + '.plot(*%s,%s**%s)\n' % (
args, "\n" + indentation + " " * 12, kwargs)
# X coordinates are needed explicitly
if "x" in s:
x = s["x"]
else:
x = list(range(len(s["y"])))
kwargs["alpha"]=0.5 # Half-transparency seems desirable
code += indentation + series_container + '.fill_between(*%s,%s**%s)\n' % (
[x, ymin, ymax], "\n" + indentation + " " * 12, kwargs)
else:
# Error bar plot
if "ymin" in s or "ymax" in s:
# Custom error bars
ymin = _full_errorbar(y, s["ymin"] if "ymin" in s else None, s["yerr"] if "yerr" in s else None,
False)
ymax = _full_errorbar(y, s["ymax"] if "ymax" in s else None, s["yerr"] if "yerr" in s else None,
True)
kwargs["yerr"] = [ymin, ymax]
elif "yerr" in s:
kwargs["yerr"] = s["yerr"]
if "xmin" in s or "xmax" in s:
# Custom error bars
x = s["x"] if "x" in s else list(range(len(y)))
xmin = _full_errorbar(x, s["xmin"] if "xmin" in s else None, s["xerr"] if "xerr" in s else None,
False)
xmax = _full_errorbar(x, s["xmax"] if "xmax" in s else None, s["xerr"] if "xerr" in s else None,
True)
kwargs["xerr"] = [xmin, xmax]
elif "xerr" in s:
kwargs["xerr"] = s["xerr"]
if "joined" in s:
if not s["joined"]:
kwargs["fmt"] = _cycle_property(marker_count, marker_list)
marker_count += 1
# Add indentation to aid edition
code += indentation + series_container + '.errorbar(*%s,%s**%s)\n' % (
args, "\n" + indentation + " " * 12, kwargs)
else:
# Regular plot
if "joined" in s:
if not s["joined"]:
args.append(_cycle_property(marker_count, marker_list))
marker_count += 1
if kwargs:
# Add indentation to aid edition
code += indentation + series_container + '.plot(*%s,%s**%s)\n' % (
args, "\n" + indentation + " " * 8, kwargs)
else:
code += indentation + series_container + '.plot(*%s)\n' % (args)
if "xrange" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'xlim(%f,%f)\n' % (
description["xrange"][0], description["xrange"][1])
if "yrange" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'ylim(%f,%f)\n' % (
description["yrange"][0], description["yrange"][1])
if "xlog" in description and description["xlog"]:
code += indentation + container + ('.' if current_axes else '.set_') + 'xscale("log")\n'
if "ylog" in description and description["ylog"]:
code += indentation + container + ('.' if current_axes else '.set_') + 'yscale("log")\n'
if add_legend:
# Find out options needed for the main legend, checking added axis and adding their legends before
legend_options = []
if "legendtitle" in description:
legend_options.append("title=" + repr(description["legendtitle"]))
if xadded_max == yadded_max == 0:
pass
elif xadded_max == 1 and yadded_max == 0:
legend_options.append("loc='lower right'")
if "legendtitle" in description["xadded"][0]:
code += indentation + 'twiny.legend(title=%s, loc="upper right")\n' % repr(
description["xadded"][0]["legendtitle"])
else:
code += indentation + 'twiny.legend(loc="upper right")\n'
elif xadded_max == 0 and yadded_max == 1:
legend_options.append("loc='upper left'")
if "legendtitle" in description["yadded"][0]:
code += indentation + 'twinx.legend(title=%s, loc="upper right")\n' % repr(
description["yadded"][0]["legendtitle"])
else:
code += indentation + 'twinx.legend(loc="upper right")\n'
else:
# TODO: Consider the possibility of both axes different
logger.warning("Ignoring not simple twin legend")
code += indentation + container + '.legend(%s)\n' % ", ".join(legend_options)
if "xlabel" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'xlabel(%s)\n' % repr(
description["xlabel"])
if "ylabel" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'ylabel(%s)\n' % repr(
description["ylabel"])
# Added axes
if xadded_max == 1:
if "label" in description["xadded"][0]:
code += indentation + "twiny.set_xlabel(%s)\n" % repr(description["xadded"][0]["label"])
if "range" in description["xadded"][0]:
code += indentation + 'twiny.set_xlim(%f,%f)\n' % tuple(description["xadded"][0]["range"])
if "log" in description["xadded"][0] and description["xadded"][0]["log"]:
code += indentation + 'twiny.set_xscale("log")\n'
if yadded_max == 1:
if "label" in description["yadded"][0]:
code += indentation + "twinx.set_ylabel(%s)\n" % repr(description["yadded"][0]["label"])
if "range" in description["yadded"][0]:
code += indentation + 'twinx.set_ylim(%f,%f)\n' % tuple(description["yadded"][0]["range"])
if "log" in description["yadded"][0] and description["yadded"][0]["log"]:
code += indentation + 'twinx.set_yscale("log")\n'
if "title" in description and description["title"]:
# Title can be requested to go inside the figure as a text.
# This is useful for multiplots, where an upper title can be confusing.
if title_inside:
code += indentation + container + '.text(.5,.95,%s, horizontalalignment="center",' \
'transform=%s.transAxes)\n' % (repr(description["title"]),
container)
else:
code += indentation + container + ('.' if current_axes else '.set_') + 'title(%s)\n' % repr(
description["title"])
if "epilog" in description:
for directive in description["epilog"]:
if directive["type"] == "text":
code += indentation + container + ".text(%f, %f, %s)\n" % (
directive["x"], directive["y"], repr(directive["text"]))
pass
else:
raise ValueError("Unknown epilog directive: " + directive["type"])
return code
def _create_matplotlib_colorplot(description, container="plt", current_axes=True, indentation_level=0, rasterized=True):
"""
Create code describing a simple plot.
Args:
description (dict): A part of VFD of type "colorplot", parsed from the JSON.
container (str): The object whose methods are called. E.g., 'plt' for pyplot or an axes.
current_axes (bool): Whether to call the set_* methods of the container or the current axes methods (for 'plt').
indentation_level: Indentation level for the code.
rasterized (bool): Whether the plot should be rasterized
Returns:
str: Python code which will create the plot.
"""
code = ""
indentation = " " * (indentation_level * _indentation_size)
plot_f = "pcolormesh"
if "contour" in description:
if description["contour"]:
plot_f = "contour"
try:
if description["fillcontour"]:
plot_f = "contourf"
except KeyError:
pass
try:
if description["zlog"]:
code += indentation + "from matplotlib.colors import LogNorm\n"
except KeyError:
pass
# Store the ContourSet to label or rasterize it later
code += indentation
if plot_f in ["contour", "contourf"]:
code += "cs = "
# Leave call open for other args
if "x" and "y" in description:
code += container + '.%s(%s,%s,%s' % (plot_f, description["x"], description["y"], description["z"])
else:
code += container + '.%s(%s' % (plot_f, description["z"])
# Set the scale and range
if "zlog" in description and description["zlog"]:
if "zrange" in description:
code += ", norm=LogNorm(vmin=%f, vmax=%f)" % tuple(description["zrange"])
else:
code += ", norm=LogNorm()"
elif "zrange" in description and plot_f in ["contour", "contourf"]:
code += ",vmin=%f, vmax=%f" % tuple(description["zrange"])
if "levels" in description and plot_f in ["contour", "contourf"]:
code += ", levels=[" + ", ".join(list(map(str, description["levels"]))) + "]"
if rasterized and plot_f == "pcolormesh":
code += ", rasterized=True"
code += ')\n'
if rasterized and plot_f in ["contour", "contourf"]:
code += indentation + "for c in cs.collections:\n"
code += indentation + " " * _indentation_size + "c.set_rasterized(True)\n"
try:
if description["xlog"]:
code += indentation + 'plt.gca().set_xscale("log")\n'
except KeyError:
pass
try:
if description["ylog"]:
code += indentation + 'plt.gca().set_yscale("log")\n'
except KeyError:
pass
if "xrange" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'xlim(%f,%f)\n' % (
description["xrange"][0], description["xrange"][1])
if "yrange" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'ylim(%f,%f)\n' % (
description["yrange"][0], description["yrange"][1])
if "zrange" in description and plot_f not in ["contour", "contourf"]:
code += indentation + container + ('.' if current_axes else '.set_') + 'clim(%f,%f)\n' % (
description["zrange"][0], description["zrange"][1])
if "xlabel" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'xlabel(%s)\n' % repr(
description["xlabel"])
if "ylabel" in description:
code += indentation + container + ('.' if current_axes else '.set_') + 'ylabel(%s)\n' % repr(
description["ylabel"])
if "title" in description and description["title"]:
code += indentation + container + ('.' if current_axes else '.set_') + 'title(%s)\n' % repr(
description["title"])
# If contour lines, label them. Otherwise, add the colorbar.
if plot_f == "contour":
code += indentation + "plt.clabel(cs)\n"
else:
code += indentation + "plt.colorbar()\n"
return code
[docs]def create_matplotlib_script(description, export_name="untitled", context=None, export_format=None,
marker_list=None, color_list=None, line_list=None, tight_layout=None,
scale_multiplot=False):
"""
Create a matplotlib script to plot the VFD with the given description.
Args:
description (dict): Description of the VFD, obtained parsing the JSON.
export_name (str): Name to give to the script file and the plots generated therein.
context (str): Matplotlib context to use in the script.
export_format (str or list of str): Format(s) to export to.
marker_list (list of str): Markers to use cyclically for series which are not joined.
color_list (list): Colors to use when an index requests to do so.
line_list (list of str): Line styles to use when requested.
tight_layout (bool): Use the tight_layout function to fit the plot.
scale_multiplot (bool): Whether to automatically increase the size of multiplots.
Returns:
str: Python code which will create the plot.
"""
# Consider only top level style hinting
if "style" in description:
style_description = description["style"]
if line_list is None and "lines" in style_description:
line_list = style_description["lines"]
if color_list is None and "colors" in style_description:
color_list = style_description["colors"]
if marker_list is None and "markers" in style_description:
marker_list = style_description["markers"]
code = "#!/usr/bin/env python\nimport matplotlib.pyplot as plt\n"
indentation = ""
indentation_level = 0
if context is not None and context:
if isinstance(context, str):
code += "with plt.style.context(%s):\n" % repr(context)
elif isinstance(context, list):
code += "with plt.style.context([%s]):\n" % ", ".join([repr(s) for s in context])
else:
raise TypeError("context must be a str or a list of str")
indentation_level = 1
indentation = " " * _indentation_size
if description["type"] == "plot":
code += _create_matplotlib_plot(description, indentation_level=indentation_level, marker_list=marker_list,
color_list=color_list, line_list=line_list, )
if tight_layout:
code += indentation + "plt.tight_layout()\n"
elif description["type"] == "multiplot":
plots_ver = len(description["plots"])
plots_hor = len(description["plots"][0])
if scale_multiplot:
code += indentation + "size_x, size_y= plt.rcParams.get('figure.figsize')\n"
code += indentation + "fig, axarr = plt.subplots(%d, %d" % (plots_ver, plots_hor) # Note unfinished line
if "xshared" in description:
code += ', sharex="%s"' % description["xshared"]
if "yshared" in description:
code += ', sharey="%s"' % description["yshared"]
if scale_multiplot:
code += ", figsize=(%d * size_x, %d * size_y)" % (plots_hor, plots_ver)
code += ")\n"
# TODO: Add colorplot support
if plots_hor == 1 and plots_ver == 1:
code += _create_matplotlib_plot(description["plots"][0][0], container="axarr", current_axes=False,
indentation_level=indentation_level, marker_list=marker_list,
color_list=color_list, line_list=line_list, title_inside=True)
elif plots_hor == 1:
for i in range(plots_ver):
code += _create_matplotlib_plot(description["plots"][i][0], container="axarr[%d]" % i,
current_axes=False, indentation_level=indentation_level,
marker_list=marker_list, color_list=color_list,
line_list=line_list, title_inside=True)
elif plots_ver == 1:
for j in range(plots_hor):
code += _create_matplotlib_plot(description["plots"][0][j], container="axarr[%d]" % j,
current_axes=False, indentation_level=indentation_level,
marker_list=marker_list, color_list=color_list,
line_list=line_list, title_inside=True)
else:
for i in range(plots_ver):
for j in range(plots_hor):
code += _create_matplotlib_plot(description["plots"][i][j], container="axarr[%d][%d]" % (i, j),
current_axes=False, indentation_level=indentation_level,
marker_list=marker_list, color_list=color_list,
line_list=line_list, title_inside=True)
if "title" in description:
code += indentation + 'fig.suptitle(%s)\n' % repr(description["title"])
if tight_layout:
code += indentation + "plt.tight_layout()\n"
# Always join after the tight_layout to avoid splitting.
try:
joined = description["joined"]
if joined[1]: # Vertical-joined
code += indentation + "fig.subplots_adjust(hspace=0)\n"
if joined[0]: # Horizontal-joined
code += indentation + "fig.subplots_adjust(wspace=0)\n"
except KeyError:
pass
elif description["type"] == "colorplot":
code += _create_matplotlib_colorplot(description, indentation_level=indentation_level)
if tight_layout:
code += indentation + "plt.tight_layout()\n"
else:
raise ValueError("Unknown plot type: %s" % description["type"])
if export_format is None or not export_format:
code += indentation + 'plt.gcf().canvas.set_window_title(%s)\n' % repr(export_name)
code += indentation + 'plt.show()\n'
else:
if isinstance(export_format, str):
export_format = [export_format]
for f in export_format:
code += indentation + 'plt.savefig("%s.%s")\n' % (export_name, f)
return code
[docs]def export_xlsx(description, file_path):
"""
Create a matplotlib script to plot the VFD with the given description.
Args:
description (dict): Description of the VFD, obtained parsing the JSON.
file_path (str): Path to the created file.
"""
row_start = '3' # Row where the series start in the spreadsheet
if description["type"] == "plot":
workbook = xlsxwriter.Workbook(file_path)
worksheet = workbook.add_worksheet()
bold = workbook.add_format({'bold': 1})
if "title" in description:
worksheet.write(0, 0, description["title"], bold)
if "xlabel" in description:
worksheet.write(0, 1, description["xlabel"], bold)
if "ylabel" in description:
worksheet.write(0, 2, description["ylabel"], bold)
# Prepare a chart with both markers and lines by default
chart = workbook.add_chart({'type': 'scatter', 'subtype': 'straight_with_markers'})
for i, s in enumerate(description["series"]):
col = chr(ord('A') + 2 * i)
col2 = chr(ord('B') + 2 * i)
worksheet.write_column(col + row_start, s["x"])
worksheet.write_column(col2 + row_start, s["y"])
if "label" in s:
worksheet.write(1, 2 * i, s["label"], bold)
# TODO: add error bar support
opts = {
'name': '=Sheet1!$%s$2' % col,
'categories': '=Sheet1!$%s$%s:$%s$%s' % (col, int(row_start), col, str(len(s["x"]) + int(row_start))),
'values': '=Sheet1!$%s$%s:$%s$%s' % (col2, int(row_start), col2, str(len(s["y"]) + int(row_start))),
}
if "joined" in s:
# If joined was explicitly set, remove the unwanted lines or markers
if s["joined"]:
opts['marker'] = {'type': 'none'}
else:
opts['line'] = {'none': True}
chart.add_series(opts)
chart.set_title({'name': '=Sheet1!A1'})
# Attribute "legendtitle" can not be used in xlsx
# A possible workarounds could be adding a dummy series as follows
# if "legendtitle" in description:
# chart.add_series(
# {'name': description["legendtitle"], 'categories': '1', 'values': '1', 'marker': {'type': 'none'},
# 'line': {'none': True}})
# However, I still don't like the result. Better do nothing.
opts = {'name': '=Sheet1!B1'}
if "xlog" in description and description["xlog"]:
opts["log_base"] = 10
if "xrange" in description:
opts["min"], opts["max"] = description["xrange"]
chart.set_x_axis(opts)
opts = {'name': '=Sheet1!C1'}
if "ylog" in description and description["ylog"]:
opts["log_base"] = 10
if "yrange" in description:
opts["min"], opts["max"] = description["yrange"]
chart.set_y_axis(opts)
worksheet.insert_chart('D3', chart, {'x_offset': 25, 'y_offset': 10})
workbook.close()
elif description["type"] == "multiplot":
raise NotImplemented
elif description["type"] == "colorplot":
raise NotImplemented
else:
raise ValueError("Unknown type: %s" % description["type"])
[docs]def create_scripts(path=".", run=False, blocking=True, expand_glob=True, **kwargs):
"""
Create a script to generate a plot for the VFD file in the given path.
Args:
path (str): Path to the VFD file.
run (bool): Whether to run the script upon creation.
blocking (bool): If run is True, whether to wait for the calls to end.
expand_glob (bool): Whether regular expressions are expanded (e.g., *.vfd or **.vfd)
**kwargs: Additional arguments to supply to `create_matplotlib_script`.
Raises:
FileNotFoundError: If the file was not found.
json.JSONDecodeError: If the file was opened, but it is not a well-built JSON.
jsonschema.ValidationError: If the opened file was a well-built JSON but not a well-built VFD.
"""
if expand_glob:
file_list = glob(path)
else:
file_list = [path]
if not file_list:
raise ValueError("No file matching " + path)
for file in file_list:
basename = os.path.basename(file)[:-4]
pyfile_path = file[:-3] + "py"
with _open_write(pyfile_path) as output:
description = json.load(open(file))
validate_vfd(description)
# If it's a single item multiplot, skip the multiplot container
if description["type"] == "multiplot" and len(description["plots"]) == 1 and \
len(description["plots"][0]) == 1:
code = create_matplotlib_script(description["plots"][0][0], export_name=basename, **kwargs)
else:
code = create_matplotlib_script(description, export_name=basename, **kwargs)
if sys.version_info < (3, 0):
output.write(unicode(code)) # noqa
else:
output.write(code)
if run:
# FIXME: Running blocking in current interpreter trying to make pyinstaller work.
# If this change stays, consider changing the API.
if blocking:
if plt is None:
raise ModuleNotFoundError("Matplotlib was not found, scripts can not be run")
old_cwd = os.getcwd()
os.chdir(os.path.abspath(os.path.dirname(pyfile_path)))
plt.close('all')
with io.open(os.path.abspath(pyfile_path), "r", encoding="utf8") as f:
exec(f.read())
plt.close('all')
os.chdir(old_cwd)
else:
subprocess.Popen(["python", os.path.abspath(pyfile_path)],
cwd=os.path.abspath(os.path.dirname(pyfile_path)))
[docs]def create_xlsx(path=".", expand_glob=True):
"""
Create a xlsx file for the VFD file in the given path.
Args:
path (str): Path to the VFD file.
expand_glob (bool): Whether regular expressions are expanded (e.g., *.vfd or **.vfd)
Raises:
FileNotFoundError: If the file was not found.
json.JSONDecodeError: If the file was opened, but it is not a well-built JSON.
jsonschema.ValidationError: If the opened file was a well-built JSON but not a well-built VFD.
"""
# TODO: Refactor to unify with create_scripts
if expand_glob:
file_list = glob(path)
else:
file_list = [path]
if not file_list:
raise ValueError("No file matching " + path)
for file in file_list:
pyfile_path = file[:-3] + "xlsx"
description = json.load(open(file))
validate_vfd(description)
# If it's a single item multiplot, skip the multiplot container
if description["type"] == "multiplot" and len(description["plots"]) == 1 and \
len(description["plots"][0]) == 1:
export_xlsx(description["plots"][0][0], pyfile_path)
else:
export_xlsx(description, pyfile_path)
[docs]def str_to_python(description):
"""
Find a Python representation for the given data in a string.
Args:
description (str): A string defining the JSON object.
Returns:
dict: A python representation of the VFD.
Raises:
json.JSONDecodeError: If the string does not define a well-built JSON.
jsonschema.ValidationError: If string defines a JSON but not a well-built VFD.
"""
data = json.loads(description)
validate_vfd(data)
return data
[docs]def validate_vfd(data):
"""
Check if the given data is a well-built VFD
Args:
data (dict): The data to check
Raises:
jsonschema.ValidationError: If the data is not a well-built VFD.
"""
if "type" not in data:
raise ValueError("No type in provided file")
if data["type"] == "plot":
validate_schema(data, schema_plot)
elif data["type"] == "multiplot":
validate_schema(data, schema_multiplot)
elif data["type"] == "colorplot":
validate_schema(data, schema_colorplot)
else:
raise ValueError("Unknown type: %s" % data["type"])
[docs]def python_to_json(data, compact=False, compact_arrays=True):
"""
Return a JSON representation of the data.
Args:
data (dict): A Python object representing a VFD.
compact (bool): Whether to save space in detriment of readability.
compact_arrays (bool): If compact was False, whether to make 1d arrays of numbers compact.
This both improves readability and saves space.
Returns:
str: A JSON representation of the data.
"""
if compact:
my_json = json.dumps(data, sort_keys=True, separators=(',', ':'))
else:
my_json = json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))
if compact_arrays:
# numbers in an array should join in one line
# "number , " into "number,":
my_json = re.sub(r"(%s)\s*([,\]])\s*" % _float_pattern, r"\g<1>\g<2>", my_json)
# "[ number " into "[number":
my_json = re.sub(r"\[\s*(%s)\s*" % _float_pattern, r"[\g<1>", my_json)
# " number ]" into "number]":
my_json = re.sub(r"\s*(%s)\s*\]" % _float_pattern, r"\g<1>]", my_json)
return my_json