Initial project version

This commit is contained in:
sebastiankloth
2022-04-11 11:01:13 +02:00
commit f40f2badd8
18 changed files with 2012 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
doc/_build
.idea
__pycache__
build/
dist/
*.egg-info
logarithmic.*.so
logarithmic.c
coordinates.*.so
coordinates.c

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Niels A. Müller
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.

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# TUDPlot
A package for matplotlib plotting in the corporate style of the TU Darmstadt.
## Installation
Make sure dependencies are installed:
* Python 3.5
* matplotlib
* numpy
Then in the source directory run:
python3 setup.py install
## Usage
Use the function `activate` to activate TU styled plotting:
import tudplot
tudplot.activate()
A call with no arguments will activate a nominal color range of 6 distinct TU colors.
One can specify the keyword `full` to enable a range of continuous colors from the full TU color spectrum.
tudplot.activate(full=10)
TUDPlot also comes with a basic exporter `saveagr`, to save the current matplotlib figure in xmgrace agr format.
from matplotlib import pyplot as plt
fig = plt.figure()
fig.plot([1,2,3], label='Test')
# Do more plotting here...
# When the figure is complete:
tudplot.saveagr('test.agr')

49
pygrace/README.md Normal file
View File

@ -0,0 +1,49 @@
# Pygrace: A tool to convert matplotlib figure to xmgrace plots
After plotting the matplotlib figure, call
pygrace.saveagr('myfig.agr')
to save it as a xmgrace plot.
## Current state of supperted matplotlib porperties
The conversion is done by the appropriate classes (`AgrFigure`, `AgrAxis`, ...) defined in the pygrace module.
They each only support a small subset of matplotlib properties, see below.
### Figures
- canvas size (page size), controllable with `AgrFigure.dpi`
- font size scaling, controllable with `AgrFigure.fontscale`
- map colors
- x/y offset: adds an offset (as fraction of the according edge) to the xmgrace figure. Helpfull if x labels get pushed out of the page.
### Axes
- size, axis limits
- title
- linear or log scale
- labels and ticklabels:
- turn on/off
- position (left, right, bottom, top)
- fontsize
- legend:
- turn on/off
- location
- fontsize
### Lines
- label (for legend)
- linestyle
- line color
- markers:
- shape
- color
- edgewidth
- fill (appart from full and none, this is kind of random)
### Texts:
- location
- fontsize
- color

1
pygrace/__init__.py Normal file
View File

@ -0,0 +1 @@
from .pygrace import saveagr

114
pygrace/_load_stuff.py Normal file
View File

@ -0,0 +1,114 @@
from __future__ import print_function
import os.path
import os
import imp
import sys
import configparser as ConfigParser
if os.name == 'nt':
path_to_log = os.path.expanduser('~\.auswerten')
else:
path_to_log = os.path.expanduser('~/.auswerten')
path_to_module = os.path.dirname(os.path.abspath(__file__))
def reload_(filename):
(path, name) = os.path.split(filename)
(name, ext) = os.path.splitext(name)
try:
file, filename, data = imp.find_module(name, [path])
except ImportError:
print('No module {} found'.format(name))
try:
mod = imp.load_module(name, file, filename, data)
return mod
except UnboundLocalError:
pass
if file:
file.close()
def import_(filename):
"""
Tries to add random file to sys.modules. Insecure as fuck and predestined to break something
(it will overload other modules)
"""
(path, name) = os.path.split(filename)
(name, ext) = os.path.splitext(name)
try:
return sys.modules[name]
except KeyError:
pass
try:
file, filename, data = imp.find_module(name, [path])
except ImportError:
print('No module {} found'.format(name))
try:
mod = imp.load_module(name, file, filename, data)
return mod
except UnboundLocalError:
pass
finally:
# Since we may exit via an exception, close fp explicitly.
try:
if file:
file.close()
except UnboundLocalError:
if not os.path.exists(path):
os.makedirs(path)
from shutil import copyfile
if os.name == 'nt':
copyfile(os.path.join(path_to_module, 'models\myfitmodels.py'), filename)
else:
copyfile(os.path.join(path_to_module, './models/myfitmodels.py'), filename)
# open(filename, 'a').close()
user_model_path = os.path.join(path_to_log, 'myfitmodels.py')
userfitmodels = import_(user_model_path)
def read_grace_conf(mode):
__path_to_config = os.path.join(path_to_log, 'grace.conf')
config = ConfigParser.SafeConfigParser()
config.read(__path_to_config)
if config.has_section(mode):
width = config.getfloat(mode, 'width')
height = config.getfloat(mode, 'height')
bottom_m = config.getfloat(mode, 'bottom margin')
left_m = config.getfloat(mode, 'left margin')
top_m = config.getfloat(mode, 'top margin')
right_m = config.getfloat(mode, 'right margin')
title = config.getint(mode, 'title')
haxis_t = config.getint(mode, 'hor axis tick')
vaxis_t = config.getint(mode, 'ver axis tick')
haxis_l = config.getint(mode, 'hor axis label')
vaxis_l = config.getint(mode, 'ver axis label')
leg = config.getint(mode, 'legend')
else:
width = round(11 * 2.54, 2)
height = round(8.5 * 2.54, 2)
bottom_m = round(0.15 * 8.5 * 2.54, 2)
left_m = round(0.15 * 8.5 * 2.54, 2)
top_m = round(0.15 * 8.5 * 2.54, 2)
right_m = round((11 / 8.5 - 1.15) * 8.5 * 2.54, 2)
haxis_l = 100
haxis_t = 100
vaxis_l = 100
vaxis_t = 100
leg = 100
title = 100
if config.has_section('sizes'):
sym = config.getfloat('sizes', 'symbol')
line = config.getfloat('sizes', 'line')
f_line = config.getfloat('sizes', 'fit_line')
else:
sym = 10
line = 1
f_line = 1
return (width, height), (bottom_m, top_m, left_m, right_m), \
((haxis_t, haxis_l), (vaxis_t, vaxis_l), title, leg), \
(sym, line, f_line)

158
pygrace/colors.py Normal file
View File

@ -0,0 +1,158 @@
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from collections import OrderedDict
from itertools import cycle
def make_cmap(colors, mapping=True):
if isinstance(colors, dict):
colors = np.array(colors.values())
bit_rgb = np.linspace(0, 1, 256)
position = np.linspace(0, 1, len(colors))
_cols = colors[:] + 0
for i in range(len(colors)):
for j in xrange(3):
_cols[i][j] = bit_rgb[colors[i][j]]
if mapping:
cdict = {'red': [], 'green':[], 'blue':[]}
for pos, color in zip(position, _cols):
cdict['red'].append((pos, color[0], color[0]))
cdict['green'].append((pos, color[1], color[1]))
cdict['blue'].append((pos, color[2], color[2]))
cmap = mpl.colors.LinearSegmentedColormap('my_colormap', cdict, 256)
else:
cmap = mpl.colors.ListedColormap(_cols, 'my_colormap')
return cmap
cm_bw = OrderedDict([('white', (255, 255, 255, 1)),
('black', (0, 0, 0, 1))])
cm_grace = OrderedDict([
('red', (255, 0, 0, 255)),
('green', (0, 255, 0, 255)),
('blue', (0, 0, 255, 255)),
('yellow', (255, 255, 0, 255)),
('brown', (188, 143, 143, 255)),
('grey', (220, 220, 220, 255)),
('violet', (148, 0, 211, 255)),
('cyan', (0, 255, 255, 255)),
('magenta', (255, 0, 255, 255)),
('orange', (255, 165, 0, 255)),
('indigo', (114, 33, 188, 255)),
('maroon', (103, 7, 72, 255)),
('turquoise', (64, 224, 208, 255)),
('green4', (0, 139, 0, 255))
])
cm_tuda = OrderedDict([
('tud1a', (93, 133, 195, 255)),
('tud2a', (0, 156, 218, 255)),
('tud3a', (80, 182, 149, 255)),
('tud4a', (175, 204, 80, 255)),
('tud5a', (221, 223, 72, 255)),
('tud6a', (255, 224, 92, 255)),
('tud7a', (248, 186, 60, 255)),
('tud8a', (238, 122, 52, 255)),
('tud9a', (233, 80, 62, 255)),
('tud10a', (201, 48, 142, 255)),
('tud11a', (128, 69, 151, 255))
])
cm_tudb = OrderedDict([
('tud1b', (0, 90, 169, 255)),
('tud2b', (0, 131, 204, 255)),
('tud3b', (0, 157, 129, 255)),
('tud4b', (153, 192, 0, 255)),
('tud5b', (201, 212, 0, 255)),
('tud6b', (253, 202, 0, 255)),
('tud7b', (245, 163, 0, 255)),
('tud8b', (236, 101, 0, 255)),
('tud9b', (230, 0, 26, 255)),
('tud10b', (166, 0, 132, 255)),
('tud11b', (114, 16, 133, 255))
])
cm_tudc = OrderedDict([
('tud1c', (0, 78, 138, 255)),
('tud2c', (0, 104, 157, 255)),
('tud3c', (0, 136, 119, 255)),
('tud4c', (127, 171, 22, 255)),
('tud5c', (177, 189, 0, 255)),
('tud6c', (215, 172, 0, 255)),
('tud7c', (210, 135, 0, 255)),
('tud8c', (204, 76, 3, 255)),
('tud9c', (185, 15, 34, 255)),
('tud10c', (149, 17, 105, 255)),
('tud11c', (97, 28, 115, 255))
])
cm_tudd = OrderedDict([
('tud1d', (36, 53, 114, 255)),
('tud2d', (0, 78, 115, 255)),
('tud3d', (0, 113, 94, 255)),
('tud4d', (106, 139, 55, 255)),
('tud5d', (153, 166, 4, 255)),
('tud6d', (174, 142, 0, 255)),
('tud7d', (190, 111, 0, 255)),
('tud8d', (169, 73, 19, 255)),
('tud9d', (156, 28, 38, 255)),
('tud10d', (115, 32, 84, 255)),
('tud11d', (76, 34, 106, 255))
])
def combine_maps(*args):
color_map = OrderedDict()
for d in args:
for k, v in d.items():
color_map[k] = v
return color_map
cm_tud = combine_maps(cm_tuda, cm_tudb, cm_tudc, cm_tudd)
all_colors = combine_maps(cm_bw, cm_grace, cm_tud)
colors = cm_tud.values()
symbols = cycle(['o', 't', 'd', 's', '+'])
sym_map = {None: '0', 'o': '1', 's': '2', 'd': '3', 't': '6', '+': '9'}
class ColorDict(OrderedDict):
def __init__(self, *args, **kwargs):
super(ColorDict, self).__init__(*args, **kwargs)
for d in [cm_bw, cm_tuda, cm_tudb, cm_tudc, cm_tudd, cm_grace]:
old_length = len(self)
for k, c in enumerate(d.keys()):
rgb = d[c][:3]
self[rgb] = (k + old_length, c)
self._new = OrderedDict()
def __contains__(self, item):
if isinstance(item, tuple):
return item in self.keys()
elif isinstance(item. basestring):
return any([item == v[1] for v in self.values()])
else:
return False
def convert_color(self, color):
c = tuple(color[0:3])
if c not in self.keys():
self[c] = (str(self.__len__()), ''.join('{:02X}'.format(a) for a in c))
self._new[c] = (str(self.__len__()-1), ''.join('{:02X}'.format(a) for a in c))
return self[c][0]
def has_index(self, idx):
return any([idx == v[0] for v in self.values()])
def new_colors(self):
return self._new
if __name__ == '__main__':
pass

355
pygrace/pygrace.py Normal file
View File

@ -0,0 +1,355 @@
import numpy as np
from matplotlib.colors import ColorConverter
from matplotlib.backends import backend_agg
import matplotlib.pyplot as plt
import textwrap
from itertools import zip_longest
from collections import defaultdict
from . import tex2grace
from .tex2grace import latex_to_xmgrace
def update_labels(labels, axis=None):
if axis is None:
axis = plt.gca()
for line, lb in zip_longest(axis.lines, labels, fillvalue=''):
line.set_label(lb)
def sanitize_strings(dic):
"""Do some sanitization for strings."""
for k in dic:
if isinstance(dic[k], str):
dic[k].replace('{', '{{')
dic[k].replace('}', '}}')
def get_world_coords(artist):
"""Get the world coordinates of an artist."""
trans = artist.axes.transData.inverted()
return trans.transform(artist.get_window_extent())
class AgrText:
fmt = """string on
string loctype view
string {position}
string char size {size:.2f}
string color {color}
string def "{value}"
"""
def get_position(self):
trans = self.agr_axis.agr_figure.figure.transFigure.inverted()
pos = trans.transform(self.text.get_window_extent())[0]
pos = (pos + self.agr_figure.offset) * self.agr_figure.pagescale
self.position = '{:.5f}, {:.5f}'.format(*pos)
def __init__(self, text, agr_axis):
self.text = text
self.agr_axis = agr_axis
self.agr_figure = agr_axis.agr_figure
self.value = latex_to_xmgrace(text.get_text())
self.size = text.get_fontsize() * self.agr_figure.fontscale
self.color = AgrLine.color_index(text.get_color())
self.get_position()
sanitize_strings(self.__dict__)
def __str__(self):
return self.fmt.format(**self.__dict__)
class AgrLine:
fmt = """hidden {hidden}
type {type}
legend "{label}"
line linestyle {linestyle}
line linewidth {linewidth}
line color {color}
symbol {marker}
symbol color {markeredgecolor}
symbol fill color {markerfacecolor}
symbol fill pattern {markerfill}
symbol linewidth {linewidth}
"""
width_scale = 2
cc = ColorConverter()
linestyles = {'None': 0, '-': 1, ':': 2, '--': 3, '-.': 5}
markers = defaultdict(
lambda: 1,
{'': 0, 'None': 0, 'o': 1, 's': 2, 'd': 3, '^': 4, '<': 5, 'v': 6, '>': 7, '+': 8, 'x': 9, '*': 10}
)
fillstyles = ('none', 'full', 'right', 'left', 'bottom', 'top')
colors = ['white', 'black']
def color_index(col):
if col not in AgrLine.colors:
AgrLine.colors.append(col)
return AgrLine.colors.index(col)
@property
def data(self):
o = '@type xy\n'
for x, y in self.line.get_xydata():
o += '{} {}\n'.format(x, y)
o += '&'
return o
def get_label(self):
lbl = self.line.get_label()
self.label = latex_to_xmgrace(lbl) if not lbl.startswith('_line') else ''
def get_linestyle(self):
self.linestyle = self.linestyles[self.line.get_linestyle()]
def get_linewidth(self):
self.linewidth = self.line.get_linewidth() * self.width_scale
def get_color(self):
self.color = AgrLine.color_index(self.line.get_color())
def get_marker(self):
mk = self.line.get_marker()
self.marker = self.markers[mk] if mk in self.markers else 1
mfc = self.line.get_markerfacecolor()
self.markerfacecolor = AgrLine.color_index(mfc)
mec = self.line.get_markeredgecolor()
self.markeredgecolor = AgrLine.color_index(mec)
self.markeredgewidth = self.line.get_markeredgewidth() * self.width_scale
self.markerfill = self.fillstyles.index(self.line.get_fillstyle())
def __init__(self, line, agr_axis):
self.agr_axis = agr_axis
self.line = line
self.hidden = 'false'
self.type = 'xy'
# run all get_ methods
for d in dir(self):
if d.startswith('get_'):
getattr(self, d)()
sanitize_strings(self.__dict__)
def __str__(self):
return self.fmt.format(**self.__dict__)
class AgrAxis:
fmt = """world {world}
view {view}
title {title}
yaxes scale {yscale}
yaxis tick major {yticks}
xaxis label "{xlabel}"
xaxis label place {xlabelpos}
xaxis label char size {labelsize}
xaxis tick major {xticks}
xaxis ticklabel {xticklabel}
xaxis ticklabel place {xticklabelpos}
xaxis ticklabel char size {ticklabelsize}
xaxes scale {xscale}
yaxis label "{ylabel}"
yaxis label place {ylabelpos}
yaxis label char size {labelsize}
yaxis ticklabel {yticklabel}
yaxis ticklabel place {yticklabelpos}
yaxis ticklabel char size {ticklabelsize}
legend {legend}
legend loctype world
legend {legend_pos}
legend char size {legend_fontsize}
"""
def get_world(self):
xmin, xmax = self.axis.get_xlim()
ymin, ymax = self.axis.get_ylim()
self.world = '{:.3}, {:.3}, {:.3}, {:.3}'.format(xmin, ymin, xmax, ymax)
box = self.axis.get_position()
fx, fy = self.axis.figure.get_size_inches()
sx, sy = self.agr_figure.pagescale
offx, offy = self.agr_figure.offset
self.view = '{:.3}, {:.3}, {:.3}, {:.3}'.format(
(box.xmin + offx)*sx, (box.ymin + offy)*sy,
(box.xmax + offx)*sx, (box.ymax + offy)*sy
)
def get_title(self):
self.title = latex_to_xmgrace(self.axis.get_title())
def get_xyaxis(self):
self.xlabel = latex_to_xmgrace(self.axis.get_xlabel())
xpos = self.axis.xaxis.get_label_position()
self.xlabelpos = 'normal' if xpos == 'bottom' else 'opposite'
self.ylabel = latex_to_xmgrace(self.axis.get_ylabel())
ypos = self.axis.yaxis.get_label_position()
self.ylabelpos = 'normal' if ypos == 'left' else 'opposite'
xsc = self.axis.get_xscale()
self.xscale = 'Logarithmic' if 'log' in xsc else 'Normal'
xticks = self.axis.get_xticks()
if xsc == 'log':
self.xticks = (xticks[1:] / xticks[:-1]).mean()
else:
self.xticks = (xticks[1:] - xticks[:-1]).mean()
self.xticklabel = 'on' if any([x.get_visible() for x in self.axis.get_xticklabels()]) else 'off'
xpos = self.axis.xaxis.get_ticks_position()
if xpos == 'both':
self.xticklabelpos = 'both'
elif xpos == 'top':
self.xticklabelpos = 'opposite'
else:
self.xticklabelpos = 'normal'
xtlpos = set([x.get_position()[1] for x in self.axis.get_xticklabels() if x.get_visible()])
if len(xtlpos) == 0:
self.xticklabel = 'off'
self.xticklabelpos = 'normal'
elif len(xtlpos) == 1:
self.xticklabel = 'on'
self.xticklabelpos = 'opposite' if 1 in xtlpos else 'normal'
else:
self.xticklabel = 'on'
self.xticklabelpos = 'both'
ytlpos = set([x.get_position()[0] for x in self.axis.get_yticklabels() if x.get_visible()])
if len(ytlpos) == 0:
self.yticklabel = 'off'
self.yticklabelpos = 'normal'
elif len(ytlpos) == 1:
self.yticklabel = 'on'
self.yticklabelpos = 'opposite' if 1 in ytlpos else 'normal'
else:
self.yticklabel = 'on'
self.yticklabelpos = 'both'
ysc = self.axis.get_yscale()
self.yscale = 'Logarithmic' if 'log' in ysc else 'Normal'
yticks = self.axis.get_yticks()
if ysc == 'log':
self.yticks = (yticks[1:] / yticks[:-1]).mean()
else:
self.yticks = (yticks[1:] - yticks[:-1]).mean()
self.labelsize = self.axis.xaxis.get_label().get_fontsize() * self.agr_figure.fontscale
fs = self.axis.xaxis.get_ticklabels()[0].get_fontsize()
self.ticklabelsize = fs * self.agr_figure.fontscale
def get_legend(self):
leg = self.axis.get_legend()
if leg is None:
self.legend = 'off'
self.legend_pos = '0, 0'
self.legend_fontsize = 0
else:
self.legend = 'on'
for lbl, line in zip(leg.get_texts(), leg.get_lines()):
pass
pos = get_world_coords(leg)
self.legend_pos = '{:.3f}, {:.3f}'.format(*pos.diagonal())
self.legend_fontsize = leg.get_texts()[0].get_fontsize() * self.agr_figure.fontscale
def __init__(self, axis, agr_fig):
self.agr_figure = agr_fig
self.axis = axis
# run all get_ methods
for d in dir(self):
if d.startswith('get_'):
getattr(self, d)()
sanitize_strings(self.__dict__)
self.lines = {'s{}'.format(i): AgrLine(l, self) for i, l in enumerate(axis.lines)}
self.texts = [AgrText(t, self) for t in self.axis.texts]
def __str__(self):
o = self.fmt.format(**self.__dict__)
for k, l in self.lines.items():
o += textwrap.indent(str(l), prefix=k + ' ')
for txt in self.texts:
o += 'with string\n'
o += textwrap.indent(str(txt), prefix=' ')
return o
class AgrFigure:
dpi = 96
fontscale = 0.5
fmt = """@version 50125
@page size {page}
"""
data_fmt = "@target {target}\n{data}\n"""
def collect_data(self):
d = {}
for ia, ax in self.axes.items():
for il, ln in ax.lines.items():
d['{}.{}'.format(ia.upper(), il.upper())] = ln.data
return d
def get_figprops(self):
fx, fy = self.figure.get_size_inches()
scx, scy = (1 + self.offset)
# scy = (1 + self.offset_vertical)
self.page = '{}, {}'.format(int(scx * self.dpi * fx), int(scy * self.dpi * fy))
self.fontscale = AgrFigure.fontscale / min(fx * scx, fy * scy)
self.pagescale = np.array([fx, fy]) / min(fx * scx, fy * scy)
def __init__(self, figure, offset_horizontal=0, offset_vertical=0, convert_latex=True):
tex2grace.do_latex_conversion = convert_latex
self.figure = figure
self.offset = np.array([offset_horizontal, offset_vertical])
# make sure to draw the figure...
canv = backend_agg.FigureCanvasAgg(figure)
canv.draw()
# run all get_ methods
for d in dir(self):
if d.startswith('get_'):
getattr(self, d)()
sanitize_strings(self.__dict__)
self.axes = {'g{}'.format(i): AgrAxis(ax, self) for i, ax in enumerate(self.figure.axes)}
def __str__(self):
o = self.fmt.format(**self.__dict__)
for i, col in enumerate(AgrLine.colors):
# in matplotlib-1.5 to_rgb can not handle 'none', this was fixed in 2.0
rgb = [int(x * 255) for x in AgrLine.cc.to_rgba(col)[:3]]
o += '@map color {i} to ({rgb[0]}, {rgb[1]}, {rgb[2]}), "{col}"\n'.format(i=i, rgb=rgb, col=col)
for k, ax in self.axes.items():
o += textwrap.indent("on\nhidden false\ntype XY\nstacked false\n", prefix='@{} '.format(k))
o += '@with {}\n'.format(k)
o += textwrap.indent(str(ax), prefix='@ ')
for k, d in self.collect_data().items():
o += self.data_fmt.format(target=k, data=d)
return o
def saveagr(fname, figure=None, offset_x=0, offset_y=0, convert_latex=True):
"""
Save figure as xmgrace plot.
If no figure is provided, this will save the current figure.
Args:
fname: Filename of the agr plot
figure (opt.): Matplotlib figure to save, if None gcf() is used.
offset_x, offset_y (opt.): Add an offest in x or y direction to the xmgrace plot.
convert_latex (opt.): If latex strings will be converted to xmgrace.
"""
if figure is None:
figure = plt.gcf()
with open(fname, 'w') as f:
af = AgrFigure(figure, offset_horizontal=offset_x, offset_vertical=offset_y,
convert_latex=convert_latex)
f.write(str(af))

201
pygrace/str2format.py Normal file
View File

@ -0,0 +1,201 @@
from __future__ import print_function
import re
small_greek = {'alpha': u'\u03b1',
'beta': u'\u03b2',
'gamma': u'\u03b3',
'delta': u'\u03b4',
'epsilon': u'\u03b5',
'zeta': u'\u03b6',
'eta': u'\u03b7',
'theta': u'\u03b8',
'iota': u'\u03b9',
'kappa': u'\u03ba',
'lambda': u'\u03bb',
'mu': u'\u03bc',
'nu': u'\u03bd',
'xi': u'\u03be',
'omicron': u'\u03bf',
'pi': u'\u03c0',
'rho': u'\u03c1',
'varsigma': u'\u03c2',
'sigma': u'\u03c3',
'tau': u'\u03c4',
'ypsilon': u'\u03c5',
'phi': u'\u03c6',
'chi': u'\u03c7',
'psi': u'\u03c8',
'omega': u'\u03c9'
}
big_greek = {'Alpha': u'\u0391',
'Beta': u'\u0392',
'Gamma': u'\u0393',
'Delta': u'\u0394',
'Epsilon': u'\u0395',
'Zeta': u'\u0396',
'Eta': u'\u0397',
'Theta': u'\u0398',
'Iota': u'\u0399',
'Kappa': u'\u039a',
'Lambda': u'\u039b',
'Mu': u'\u039c',
'Nu': u'\u039d',
'Xi': u'\u039e',
'Omicron': u'\u039f',
'Pi': u'\u03a0',
'Rho': u'\u03a1',
'Varsigma': u'\u03a2',
'Sigma': u'\u03a3',
'Tau': u'\u03a4',
'Ypsilon': u'\u03a5',
'Phi': u'\u03a6',
'Chi': u'\u03a7',
'Psi': u'\u03a8',
'Omega': u'\u03a9'
}
special_chars = {'infty': u'\u221e',
'int': u'\u222b',
'perp': u'\u27c2',
'para': u'\u2225',
'leftarrow': u'\u21d0',
'rightarrow': u'\u21d2',
'leftrightarrow': u'\u21d4',
'\n': '<br>',
'*': u'\u00b7',
'plmin': u'\u00b1'}
small_greek_grace = {'alpha': r'\f{Symbol}a\f{}',
'beta': r'\f{Symbol}b\f{}',
'gamma': r'\f{Symbol}g\f{}',
'delta': r'\f{Symbol}d\f{}',
'epsilon': r'\f{Symbol}e\f{}',
'zeta': r'\f{Symbol}z\f{}',
'eta': r'\f{Symbol}h\f{}',
'theta': r'\f{Symbol}q\f{}',
'iota': r'\f{Symbol}i\f{}',
'kappa': r'\f{Symbol}k\f{}',
'lambda': r'\f{Symbol}l\f{}',
'mu': r'\f{Symbol}m\f{}',
'nu': r'\f{Symbol}n\f{}',
'xi': r'\f{Symbol}x\f{}',
'omicron': r'\f{Symbol}o\f{}',
'pi': r'\f{Symbol}p\f{}',
'rho': r'\f{Symbol}r\f{}',
'sigma': r'\f{Symbol}s\f{}',
'tau': r'\f{Symbol}t\f{}',
'ypsilon': r'\f{Symbol}u\f{}',
'phi': r'\f{Symbol}f\f{}',
'chi': r'\f{Symbol}c\f{}',
'psi': r'\f{Symbol}y\f{}',
'omega': r'\f{Symbol}w\f{}'
}
big_greek_grace = {'Alpha': r'\f{Symbol}A\f{}',
'Beta': r'\f{Symbol}B\f{}',
'Gamma': r'\f{Symbol}g\f{}',
'Delta': r'\f{Symbol}D\f{}',
'Epsilon': r'\f{Symbol}E\f{}',
'Zeta': r'\f{Symbol}Z\f{}',
'Eta': r'\f{Symbol}H\f{}',
'Theta': r'\f{Symbol}Q\f{}',
'Iota': r'\f{Symbol}I\f{}',
'Kappa': r'\f{Symbol}K\f{}',
'Lambda': r'\f{Symbol}L\f{}',
'Mu': r'\f{Symbol}M\f{}',
'Nu': r'\f{Symbol}N\f{}',
'Xi': r'\f{Symbol}X\f{}',
'Omicron': r'\f{Symbol}O\f{}',
'Pi': r'\f{Symbol}P\f{}',
'Rho': r'\f{Symbol}R\f{}',
'Sigma': r'\f{Symbol}S\f{}',
'Tau': r'\f{Symbol}T\f{}',
'Ypsilon': r'\f{Symbol}U\f{}',
'Phi': r'\f{Symbol}F\f{}',
'Chi': r'\f{Symbol}C\f{}',
'Psi': r'\f{Symbol}U\f{}',
'Omega': r'\f{Symbol}Q\f{}'
}
special_chars_grace = {'infty': r'\f{Symbol}\c%\f{}'}
full_chars = dict()
for d in [big_greek, small_greek, special_chars]:
full_chars.update(d)
full_char_reverse = {y: x for x, y in full_chars.items()}
def _replace_unicode(text):
for k, v in full_chars.items():
text = text.replace(k, v)
return text
def _replacesub(text):
sub_pattern_1 = re.compile('(_\{\S*?\})')
sub_pattern_2 = re.compile('_\S')
for s in sub_pattern_1.findall(text):
text = text.replace(s, u'<sub>{0:}</sub>'.format(s[2:-1]))
for s in sub_pattern_2.findall(text):
text = text.replace(s, u'<sub>{0:}</sub>'.format(s[1:]))
return text
def _replacesup(text):
sup_pattern_1 = re.compile('\^\{\S*?\}')
sup_pattern_2 = re.compile('\^\S')
for s in sup_pattern_1.findall(text):
text = text.replace(s, u'<sup>{0:}</sup>'.format(s.strip()[2:-1]))
for s in sup_pattern_2.findall(text):
text = text.replace(s, u'<sup>{0:}</sup>'.format(s.strip()[1:]))
return text
def str2format(text):
text = _replacesub(text)
text = _replacesup(text)
text = _replace_unicode(text)
return text
def format2str(text):
for k, v in full_char_reverse.items():
text = text.replace(k, v)
for k, v in [('<sub>', '_{'), ('</sub>', '}'), ('<sup>', '^{'), ('</sup>', '}')]:
text = text.replace(k, v)
return text
def format2grace(text):
text = format2str(text)
for k, v in [('_{', r'\s'), ('}', r'\N'), ('^{', r'\S'), ('}', r'\N')]:
text = text.replace(k, v)
for k, v in big_greek_grace.items():
text = text.replace(k, v)
for k, v in small_greek_grace.items():
text = text.replace(k, v)
for k, v in special_chars_grace.items():
text = text.replace(k, v)
return text
def grace2str(text):
text = text.replace(r'\s', '_{').replace('\S', '^{')
text = text.replace(r'\N', '}')
text = text.replace(r'\f{}', '')
return text
if __name__ == '__main__':
stringtext = 'M_{infty}[1-alpha exp(-x/T_{1,1})^{beta_1}]'
print('Input:\t{}'.format(stringtext))
print(u'Output:\t{}'.format(str2format(stringtext)))
print('Input 2:\t{}'.format(format2str(str2format(stringtext))))
print('Grace:\t{}'.format(format2grace(str2format(stringtext))))
print(u'Output 2:\t{}'.format(str2format(format2str(str2format(stringtext)))))

37
pygrace/tex2grace.py Normal file
View File

@ -0,0 +1,37 @@
from collections import OrderedDict
import logging
import re
patterns = OrderedDict()
patterns['\$'] = ''
patterns[r'\\math(?:tt|sf|it)({.+})'] = r'\1'
patterns[r'\^({.+}|.)'] = r'\S\1\N'
patterns[r'\_({.+}|.)'] = r'\s\1\N'
# Remove any left over latex groups as the last step
patterns[r'[{}]'] = ''
# Greek letters in xmgrace are written by switching to symbol-font:
# "\x a\f{}" will print an alpha and switch back to normal font
greek = {
'alpha': 'a', 'beta': 'b', 'gamma': 'g', 'delta': 'd', 'epsilon': 'e', 'zeta': 'z',
'eta': 'h', 'theta': 'q', 'iota': 'i', 'kappa': 'k', 'lambda': 'l', 'mu': 'm',
'nu': 'n', 'xi': 'x', 'omicron': 'o', 'pi': 'p', 'rho': 'r', 'sigma': 's',
'tau': 't', 'upsilon': 'u', 'phi': 'f', 'chi': 'c', 'psi': 'y', 'omega': 'w',
'varphi': 'j', 'varepsilon': 'e', 'vartheta': 'J', 'varrho': 'r'
}
for latex, xmg in greek.items():
patt = r'\\{}'.format(latex)
repl = r'\\x {}\\f{{}}'.format(xmg)
patterns[patt] = repl
do_latex_conversion = True
def latex_to_xmgrace(string):
logging.debug('Convert to xmgrace: {}'.format(string))
if do_latex_conversion and '$' in string:
for patt, repl in patterns.items():
string = re.sub(patt, repl, string)
logging.debug('To -> {}'.format(string))
return string

13
setup.py Normal file
View File

@ -0,0 +1,13 @@
from setuptools import setup
setup(
name='tudplot',
description='Matplotlib styling in TU Darmstadt corporate design',
author_email='niels.mueller@physik.tu-darmstadt.de',
packages=['tudplot', 'pygrace'],
version='0.1',
requires=['matplotlib'],
package_data={'tudplot': ['tud.mplstyle']},
zip_safe=True,
)

77
tudplot/__init__.py Executable file
View File

@ -0,0 +1,77 @@
import os
import numpy
import matplotlib as mpl
from matplotlib import pyplot
from cycler import cycler
from .xmgrace import export_to_agr, load_agr_data
from .tud import tudcolors, nominal_colors, sequential_colors
from .utils import facet_plot, CurvedText as curved_text
def activate(scheme='b', full=False, sequential=False, cmap='blue-red', **kwargs):
"""
Activate the tud design.
Args:
scheme (opt.): Color scheme to activate, default is 'b'.
full (opt.):
Activate the full color palette. If False a smaller color palette is used.
If a number N is given, N colors will be chosen based on a interpolation of
all tudcolors.
sequential (opt.): Activate a number of sequential colors from a color map.
cmap (opt.):
Colormap to use for sequential colors, can be either from `~tudplot.tud.cmaps`
or any matplotlib color map. Range of the color map values can be given as
cmap_min and cmap_max, respectively.
**kwargs: Any matplotlib rc paramter may be given as keyword argument.
"""
mpl.pyplot.style.use(os.path.join(os.path.dirname(__file__), 'tud.mplstyle'))
if full:
if isinstance(full, int):
cmap = mpl.colors.LinearSegmentedColormap.from_list('tud{}'.format(scheme),
tudcolors[scheme])
colors = [cmap(x) for x in numpy.linspace(0, 1, full)]
else:
colors = tudcolors[scheme]
elif sequential:
colors = sequential_colors(sequential, cmap=cmap, min=kwargs.pop('cmap_min', 0),
max=kwargs.pop('cmap_max', 1))
else:
colors = nominal_colors[scheme]
mpl.rcParams['axes.prop_cycle'] = cycler('color', colors)
def saveagr(filename, figure=None, convert_latex=True):
"""
Save the current figure in xmgrace format.
Args:
filename: Agrfile to save the figure to
figure (opt.):
Figure that will be saved, if not given the current figure is saved
"""
figure = figure or pyplot.gcf()
figure.canvas.draw()
export_to_agr(figure, filename, convert_latex=convert_latex)
def markfigure(x, y, s, ax=None, **kwargs):
if ax is None:
ax = pyplot.gca()
kwargs['transform'] = ax.transAxes
kwargs['ha'] = 'center'
kwargs['va'] = 'center'
# kwargs.setdefault('fontsize', 'large')
ax.text(x, y, s, **kwargs)
def marka(x, y):
markfigure(x, y, '(a)')
def markb(x, y):
markfigure(x, y, '(b)')

157
tudplot/altair.py Executable file
View File

@ -0,0 +1,157 @@
import altair
from altair import Config, Color, Shape, Column, Row, Encoding, Scale, Axis
from random import randint
import os
import logging
import matplotlib.pyplot as plt
from .tud import nominal_colors, full_colors
def filter_nulltime_json(data):
if 'time' in data:
data = data[data.time > 0]
return altair.pipe(data, altair.to_json)
altair.renderers.enable('notebook')
altair.data_transformers.register('json_logtime', filter_nulltime_json)
altair.data_transformers.enable('json_logtime')
def my_theme(*args, **kwargs):
return {
'range': {
'ordinal': altair.VgScheme('viridis'),
'ramp': {'scheme': 'viridis'}
}
}
altair.themes.register('my-theme', my_theme)
altair.themes.enable('my-theme')
class BaseMixin(Encoding):
def __init__(self, *args, **kwargs):
kwargs.setdefault('scale', altair.Scale(zero=False))
super().__init__(*args, **kwargs)
class LogMixin(BaseMixin):
def __init__(self, *args, **kwargs):
kwargs['scale'] = altair.Scale(type='log')
super().__init__(*args, **kwargs)
class X(altair.X, BaseMixin):
pass
class Y(altair.Y, BaseMixin):
pass
class LogX(altair.X, LogMixin):
pass
class LogY(altair.Y, LogMixin):
pass
class DataHandler(altair.Data):
def __init__(self, df):
self._filename = '.altair.json'
with open(self._filename, 'w') as f:
f.write(df.to_json())
super().__init__(url=self._filename)
class Chart(altair.Chart):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def encode(self, *args, color=None, **kwargs):
if isinstance(color, str):
if color.endswith(':F'):
field = color[:-2]
color = color.replace(':F', ':N')
self.configure_scale(nominalColorRange=full_colors(len(set(self._data[field]))))
for arg in args:
if isinstance(arg, altair.X):
kwargs['x'] = arg
elif isinstance(arg, altair.Y):
kwargs['y'] = arg
return super().encode(color=color, **kwargs)
def to_mpl(self):
d = self.to_dict()
fmt = 'o' if d.get('mark', 'point') is 'point' else '-'
def encode(data, encoding, **kwargs):
logging.debug(str(kwargs))
if 'column' in encoding:
channel = encoding.pop('column')
ncols = len(data[channel.get('field')].unique())
for col, (column, df) in enumerate(data.groupby(channel.get('field'))):
ax = plt.gca() if col > 0 else None
plt.subplot(kwargs.get('nrows', 1), ncols, col + 1, sharey=ax).set_title(column)
encode(df, encoding.copy(), secondary_column=col > 0, **kwargs.copy())
elif 'color' in encoding:
channel = encoding.pop('color')
if channel.get('type') == 'quantitative':
colors = full_colors(len(data[channel.get('field')].unique()))
else:
colors = nominal_colors['b']
while len(colors) < len(data[channel.get('field')].unique()):
colors *= 2
for color, (column, df) in zip(colors, data.groupby(channel.get('field'))):
if 'label' in kwargs:
label = kwargs.pop('label') + ', {}'.format(column)
else:
label = str(column)
encode(df, encoding.copy(), color=color, label=label, **kwargs.copy())
elif 'shape' in encoding:
channel = encoding.pop('shape')
markers = ['h', 'v', 'o', 's', '^', 'D', '<', '>']
while len(markers) < len(data[channel.get('field')].unique()):
markers *= 2
logging.debug(str(data[channel.get('field')].unique()))
for marker, (column, df) in zip(markers, data.groupby(channel.get('field'))):
if 'label' in kwargs:
label = kwargs.pop('label') + ', {}'.format(column)
else:
label = str(column)
encode(df, encoding.copy(), marker=marker, label=label, **kwargs.copy())
else:
x_field = encoding.get('x').get('field')
y_field = encoding.get('y').get('field')
plt.xlabel(x_field)
if not kwargs.pop('secondary_column', False):
plt.ylabel(y_field)
else:
plt.tick_params(axis='y', which='both', labelleft='off', labelright='off')
if 'scale' in encoding.get('x'):
plt.xscale(encoding['x']['scale'].get('type', 'linear'))
if 'scale' in encoding.get('y'):
plt.yscale(encoding['y']['scale'].get('type', 'linear'))
plt.plot(data[x_field], data[y_field], fmt, **kwargs)
plt.legend(loc='best', fontsize='small')
encode(self._data, d.get('encoding'))
plt.tight_layout(pad=0.5)
class Arrhenius(Chart):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# self.transform_data(calculate=[Formula(field='1000K/T', expr='1000/datum.T')])
self.data['1000 K / T'] = 1000 / self.data['T']
self.encode(x=X('1000 K / T'))

53
tudplot/tex2grace.py Executable file
View File

@ -0,0 +1,53 @@
from collections import OrderedDict
import logging
import re
patterns = OrderedDict()
patterns['\$'] = ''
patterns['¹'] = r'\\S1\\N'
patterns['²'] = r'\\S2\\N'
patterns['³'] = r'\\S3\\N'
patterns[''] = r'\\S4\\N'
patterns[''] = r'\\S5\\N'
patterns[''] = r'\\S6\\N'
patterns[''] = r'\\S7\\N'
patterns[''] = r'\\S8\\N'
patterns[''] = r'\\S9\\N'
patterns[''] = r'\\S-\\N'
patterns[r'\\star'] = '*'
patterns[r'\\,'] = r'\\-\\- \\+\\+'
patterns[r'\\[;:.]'] = ''
patterns[r'\\math(?:tt|sf|it|rm)({[^}]+})'] = r'\1'
patterns[r'\^({[^}]+}|.)'] = r'\\S\1\\N'
patterns[r'\_({[^}]+}|.)'] = r'\\s\1\\N'
# Remove any left over latex groups as the last step
patterns[r'[{}]'] = ''
# now any patterns that do need braces...
patterns[r'\\tilde '] = r'\\v{{0.1}}~\\v{{-0.1}}\\M{{1}}'
# Greek letters in xmgrace are written by switching to symbol-font:
# "\x a\f{}" will print an alpha and switch back to normal font
greek = {
'alpha': 'a', 'beta': 'b', 'gamma': 'g', 'delta': 'd', 'epsilon': 'e', 'zeta': 'z',
'eta': 'h', 'theta': 'q', 'iota': 'i', 'kappa': 'k', 'lambda': 'l', 'mu': 'm',
'nu': 'n', 'xi': 'x', 'omicron': 'o', 'pi': 'p', 'rho': 'r', 'sigma': 's',
'tau': 't', 'upsilon': 'u', 'phi': 'f', 'chi': 'c', 'psi': 'y', 'omega': 'w',
'varphi': 'j', 'varepsilon': 'e', 'vartheta': 'J', 'varrho': 'r',
'Phi': 'F',
'langle': r'\#{{e1}}', 'rangle': r'\#{{f1}}', 'infty': r'\\c%\\C', 'cdot': r'\#{{d7}}',
'sqrt': r'\#{{d6}}', 'propto': r'\#{{b5}}', 'approx': r'\#{{bb}}'
}
for latex, xmg in greek.items():
patt = r'\\{}'.format(latex)
repl = r'\\x {}\\f{{{{}}}}'.format(xmg)
patterns[patt] = repl
def latex_to_xmgrace(string):
logging.debug('Convert to xmgrace: {}'.format(string))
for patt, repl in patterns.items():
string = re.sub(patt, repl, string)
logging.debug('To -> {}'.format(string))
return string

7
tudplot/tud.mplstyle Executable file
View File

@ -0,0 +1,7 @@
font.size: 16
lines.linewidth: 1.5
lines.markeredgewidth: 1.5
lines.markersize: 6
markers.fillstyle: full
figure.figsize: 8, 6
savefig.dpi: 300

42
tudplot/tud.py Normal file
View File

@ -0,0 +1,42 @@
import re
import matplotlib as mpl
import numpy
tudcolors = {
'a': ('#5D85C3', '#009CDA', '#50B695', '#AFCC50', '#DDDF48', '#FFE05C',
'#F8BA3C', '#EE7A34', '#E9503E', '#C9308E', '#804597'),
'b': ('#005AA9', '#0083CC', '#009D81', '#99C000', '#C9D400', '#FDCA00',
'#F5A300', '#EC6500', '#E6001A', '#A60084', '#721085'),
'c': ('#004E8A', '#00689D', '#008877', '#7FAB16', '#B1BD00', '#D7AC00',
'#D28700', '#CC4C03', '#B90F22', '#951169', '#611C73'),
'd': ('#243572', '#004E73', '#00715E', '#6A8B22', '#99A604', '#AE8E00',
'#BE6F00', '#A94913', '#961C26', '#732054', '#4C226A'),
}
# Store each color value in the dict as defined in the Style-Guide (e.g. tud9c)
tudcolors.update({'tud{}{}'.format(i + 1, s): col for s in tudcolors for i, col in enumerate(tudcolors[s])})
color_maps = {
'blue-red': (tudcolors['tud1b'], tudcolors['tud9b']),
'black-green': ('black', tudcolors['tud5c']),
'violett-green': (tudcolors['tud11c'], tudcolors['tud5b'])
}
nominal_colors = {scheme: [tudcolors[scheme][i] for i in [1, 8, 3, 9, 6, 2]] for scheme in 'abcd'}
def full_colors(N, scheme='b'):
cmap = mpl.colors.LinearSegmentedColormap.from_list('tud{}'.format(scheme), tudcolors[scheme])
return ['#{:02x}{:02x}{:02x}'.format(*cmap(x, bytes=True)[:3]) for x in numpy.linspace(0, 1, N)]
def sequential_colors(N, cmap='blue-red', min=0, max=1):
if cmap in color_maps:
cmap = mpl.colors.LinearSegmentedColormap.from_list('tud_{}'.format(cmap), color_maps[cmap])
elif '-' in cmap:
cols = [tudcolors[k] if 'tud' in k else k for k in cmap.split('-')]
cmap = mpl.colors.LinearSegmentedColormap.from_list(cmap, cols)
else:
cmap = mpl.pyplot.get_cmap(cmap)
return ['#{:02x}{:02x}{:02x}'.format(*cmap(x, bytes=True)[:3]) for x in numpy.linspace(min, max, N)]

220
tudplot/utils.py Normal file
View File

@ -0,0 +1,220 @@
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from collections import Iterable
from matplotlib.cbook import flatten
from itertools import cycle
def facet_plot(dframe, facets, props, ydata, layout=None, newfig=True, figsize=None,
legend=True, individual_legends=False, hide_additional_axes=True, zorder='default', **kwargs):
if newfig:
nr_facets = len(dframe.groupby(facets))
if layout is None:
for i in range(2, nr_facets // 2):
if nr_facets % i == 0:
layout = (nr_facets // i, i)
break
if layout is None:
n = int(np.ceil(nr_facets / 2))
layout = (n, 2)
fig, axs = plt.subplots(
nrows=layout[0],
ncols=layout[1],
sharex=True, sharey=True, figsize=figsize
)
if hide_additional_axes:
for ax in fig.axes[nr_facets:]:
ax.set_axis_off()
else:
fig = plt.gcf()
axs = fig.axes
cycl = cycle(plt.rcParams['axes.prop_cycle'])
prop_styles = {ps: next(cycl) for ps, _ in dframe.groupby(props)}
if zorder is 'default':
dz = 1
zorder = 0
elif zorder is 'reverse':
dz = -1
zorder = 0
else:
dz = 0
if legend:
ax0 = fig.add_subplot(111, frame_on=False, zorder=-9999)
ax0.set_axis_off()
plot_kwargs = kwargs.copy()
for k in ['logx', 'logy', 'loglog']:
plot_kwargs.pop(k, None)
for l, p in prop_styles.items():
ax0.plot([], label=str(l), **p, **plot_kwargs)
ax0.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize='x-small')
for ax, (ps, df) in zip(flatten(axs), dframe.groupby(facets, squeeze=False)):
for prop, df_prop in df.groupby(props):
df_prop[ydata].plot(ax=ax, label=str(prop), zorder=zorder, **prop_styles[prop], **kwargs)
zorder += dz
# ax.title(0.5, 0.1, '{},{}'.format(*ps), transform=ax.transAxes, fontsize='small')
ax.set_title('; '.join([str(x) for x in ps]) if isinstance(ps, tuple) else str(ps), fontsize='x-small')
if individual_legends:
ax.legend(fontsize='x-small')
plt.sca(ax)
rect = (0, 0, 0.85, 1) if legend else (0, 0, 1, 1)
plt.tight_layout(rect=rect, pad=0.1)
return fig, axs
class CurvedText(mpl.text.Text):
"""A text object that follows an arbitrary curve."""
def __init__(self, x, y, text, axes, **kwargs):
super(CurvedText, self).__init__(x[0],y[0],' ', axes, **kwargs)
axes.add_artist(self)
# # saving the curve:
self.__x = x
self.__y = y
self.__zorder = self.get_zorder()
# # creating the text objects
self.__Characters = []
for c in text:
t = mpl.text.Text(0, 0, c, **kwargs)
# resetting unnecessary arguments
t.set_ha('center')
t.set_rotation(0)
t.set_zorder(self.__zorder +1)
self.__Characters.append((c,t))
axes.add_artist(t)
# # overloading some member functions, to assure correct functionality
# # on update
def set_zorder(self, zorder):
super(CurvedText, self).set_zorder(zorder)
self.__zorder = self.get_zorder()
for c,t in self.__Characters:
t.set_zorder(self.__zorder+1)
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function. Do not do
do any drawing, but update the positions and rotation
angles of self.__Characters.
"""
self.update_positions(renderer)
def update_positions(self,renderer):
"""
Update positions and rotations of the individual text elements.
"""
# preparations
# # determining the aspect ratio:
# # from https://stackoverflow.com/a/42014041/2454357
# # data limits
xlim = self.axes.get_xlim()
ylim = self.axes.get_ylim()
# # Axis size on figure
figW, figH = self.axes.get_figure().get_size_inches()
# # Ratio of display units
_, _, w, h = self.axes.get_position().bounds
# # final aspect ratio
aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])
# points of the curve in figure coordinates:
x_fig,y_fig = (
np.array(l) for l in zip(*self.axes.transData.transform([
(i,j) for i,j in zip(self.__x,self.__y)
]))
)
# point distances in figure coordinates
x_fig_dist = (x_fig[1:]-x_fig[:-1])
y_fig_dist = (y_fig[1:]-y_fig[:-1])
r_fig_dist = np.sqrt(x_fig_dist**2+y_fig_dist**2)
# arc length in figure coordinates
l_fig = np.insert(np.cumsum(r_fig_dist),0,0)
# angles in figure coordinates
rads = np.arctan2((y_fig[1:] - y_fig[:-1]),(x_fig[1:] - x_fig[:-1]))
degs = np.rad2deg(rads)
rel_pos = 10
for c,t in self.__Characters:
# finding the width of c:
t.set_rotation(0)
t.set_va('center')
bbox1 = t.get_window_extent(renderer=renderer)
w = bbox1.width
h = bbox1.height
# ignore all letters that don't fit:
if rel_pos+w/2 > l_fig[-1]:
t.set_alpha(0.0)
rel_pos += w
continue
elif c != ' ':
t.set_alpha(1.0)
# finding the two data points between which the horizontal
# center point of the character will be situated
# left and right indices:
il = np.where(rel_pos+w/2 >= l_fig)[0][-1]
ir = np.where(rel_pos+w/2 <= l_fig)[0][0]
# if we exactly hit a data point:
if ir == il:
ir += 1
# how much of the letter width was needed to find il:
used = l_fig[il]-rel_pos
rel_pos = l_fig[il]
# relative distance between il and ir where the center
# of the character will be
fraction = (w/2-used)/r_fig_dist[il]
# # setting the character position in data coordinates:
# # interpolate between the two points:
x = self.__x[il]+fraction*(self.__x[ir]-self.__x[il])
y = self.__y[il]+fraction*(self.__y[ir]-self.__y[il])
# getting the offset when setting correct vertical alignment
# in data coordinates
t.set_va(self.get_va())
bbox2 = t.get_window_extent(renderer=renderer)
bbox1d = self.axes.transData.inverted().transform(bbox1)
bbox2d = self.axes.transData.inverted().transform(bbox2)
dr = np.array(bbox2d[0]-bbox1d[0])
# the rotation/stretch matrix
rad = rads[il]
rot_mat = np.array([
[np.cos(rad), np.sin(rad)*aspect],
[-np.sin(rad)/aspect, np.cos(rad)]
])
# # computing the offset vector of the rotated character
drp = np.dot(dr,rot_mat)
# setting final position and rotation:
t.set_position(np.array([x,y])+drp)
t.set_rotation(degs[il])
t.set_va('center')
t.set_ha('center')
# updating rel_pos to right edge of character
rel_pos += w-used

461
tudplot/xmgrace.py Executable file
View File

@ -0,0 +1,461 @@
import re
import logging
from collections import OrderedDict
from matplotlib.colors import ColorConverter
from matplotlib.cbook import is_string_like
import numpy as np
import matplotlib as mpl
from .tud import tudcolors
from .tex2grace import latex_to_xmgrace
def indexed(list, default=None):
def index(arg):
for i, v in enumerate(list):
if (isinstance(v, tuple) and arg in v) or arg == v:
return i
return default
return index
def escapestr(s):
raw_map = {8: r'\b', 7: r'\a', 12: r'\f', 10: r'\n', 13: r'\r', 9: r'\t', 11: r'\v'}
return r''.join(i if ord(i) > 32 else raw_map.get(ord(i), i) for i in s)
def get_viewport_coords(artist):
"""
Get the viewport coordinates of an artist.
"""
fxy = artist.figure.get_size_inches()
fxy /= fxy.min()
trans = artist.figure.transFigure.inverted()
return trans.transform(artist.get_window_extent()) * fxy[np.newaxis, :]
def get_world_coords(artist):
"""
Get the world coordinates of an artist.
"""
trans = artist.axes.transData.inverted()
return trans.transform(artist.get_window_extent())
def get_world(axis):
xmin, xmax = axis.get_xlim()
ymin, ymax = axis.get_ylim()
return '{}, {}, {}, {}'.format(xmin, ymin, xmax, ymax)
def get_view(axis):
box = axis.get_position()
fx, fy = axis.figure.get_size_inches()
sx = fx / min(fx, fy)
sy = fy / min(fx, fy)
c = np.array([box.xmin*sx, box.ymin*sy, box.xmax*sx, box.ymax*sy])
return '{:.3}, {:.3}, {:.3}, {:.3}'.format(*c)
def get_major_ticks(dim):
def get_major_dticks(axis):
ticks = getattr(axis, 'get_{}ticks'.format(dim))()
scale = getattr(axis, 'get_{}scale'.format(dim))()
if scale is 'log':
value = (ticks[1:] / ticks[:-1]).mean()
else:
value = (ticks[1:] - ticks[:-1]).mean()
return value
return get_major_dticks
agr_attr_lists = {
# Linestyles in xmgrace: None are styles that are by default
# not defined in matplotlib (longer dashes and double dots)
# First entry should always be None, since index starts at 1
'linestyle': ['None', '-', ':', '--', None, '-.', None, None, None],
'marker': ['None', 'o', 's', 'd', '^', '<', 'v', '>', '+', 'x', '*']
}
def get_ticklabels_on(dim):
def get_ticklabels(axis):
tl = getattr(axis, f'{dim}axis').get_ticklabels()
return 'off' if all([x.get_text() == '' for x in tl]) else 'on'
return get_ticklabels
def get_legend(axis):
if axis.get_legend() is not None:
return 'on'
else:
return 'off'
def get_legend_position(axis):
leg = axis.get_legend()
if leg is not None:
return '{:.3f}, {:.3f}'.format(*get_viewport_coords(leg).diagonal())
else:
return '0, 0'
def get_text_position(text):
#return '{:.3f}, {:.3f}'.format(*get_viewport_coords(text)[0])
return '{:.3f}, {:.3f}'.format(*get_viewport_coords(text).mean(axis=0))
def get_arrow_coordinates(text):
arrow = text.arrow_patch
trans = text.axes.transData.inverted()
xy = trans.transform(arrow.get_path().vertices[[0, 2]])
#xy = get_viewport_coords(text)
return '{:.3f}, {:.3f}, {:.3f}, {:.3f}'.format(*xy[0], *xy[1])
class StaticAttribute:
"""
A static attribute that just writes a line to the agr file if it is set.
Functions also as a base class for other attribute classes.
"""
def __init__(self, key, fmt):
"""
Args:
key: The name of the attribute.
fmt: The string which is written to the agr file.
"""
self.key = key
self.fmt = fmt
def format(self, source=None, **kwargs):
"""
Return the (formatted) string of the attribute.
Args:
source: The python object, which is only included here for signature reasons
"""
return self.fmt
class ValueAttribute(StaticAttribute):
"""
An attribute which writes a key value pair into the agr file.
The agr string has the format: '{fmt} {value}'
"""
attr_lists = {
'linestyle': ('None', '-', ':', '--', None, '-.', None, None, None),
'marker': (('', 'None'), 'o', 's', ('d', 'D'), '^', '<', 'v', '>', '+', 'x', '*'),
'fillstyle': ('none', 'full', ),
'color': ['white', 'black'],
}
def reset_colors():
ValueAttribute.attr_lists['color'] = ['white', 'black']
def _get_value(self, source, convert_latex=True):
value = getattr(source, 'get_{}'.format(self.key))()
if isinstance(value, str):
if convert_latex:
value = latex_to_xmgrace(value)
else:
value = value.replace(r'{}', r'{{}}').replace('{{{}}}', '{{}}')
if not self.index:
value = '"{}"'.format(value)
return value
def __init__(self, *args, index=None, function=None, condition=None):
"""
Args:
*args: Arguments of super().__init__()
index:
True if value should be mapped to an index. If this is a str this will
be used as the index lists key.
function: A function that is used to fetch the value from the source.
condition: A function that decides if the attribute is written to the agr file.
"""
super().__init__(*args)
if index:
if index is True:
self.index = self.key
else:
self.index = index
self.attr_lists.setdefault(self.index, [])
else:
self.index = False
if function is not None:
self._get_value = lambda x, **kwargs: function(x)
self.condition = condition or (lambda x: True)
def format(self, source, convert_latex=True, **kwargs):
value = self._get_value(source, convert_latex=convert_latex)
if not self.condition(value):
return None
if self.index:
attr_list = self.attr_lists[self.index]
index = indexed(attr_list)(str(value))
if index is None:
try:
attr_list.append(value)
index = attr_list.index(value)
except AttributeError:
print('index not found:', value, index, attr_list)
index = 1
value = index
logging.debug('fmt: {}, value: {}'.format(self.fmt, value))
return ' '.join([self.fmt, str(value)])
agr_line_attrs = [
StaticAttribute('hidden', 'hidden false'),
StaticAttribute('type', 'type xy'),
ValueAttribute('label', 'legend', condition=lambda lbl: re.search(r'\\sl\\Nine\d+', lbl) is None),
ValueAttribute('linestyle', 'line linestyle', index=True),
ValueAttribute('linewidth', 'line linewidth'),
ValueAttribute('color', 'line color', index=True),
ValueAttribute('marker', 'symbol', index=True),
ValueAttribute('fillstyle', 'symbol fill pattern', index=True),
ValueAttribute('markeredgecolor', 'symbol color', index='color'),
ValueAttribute('markerfacecolor', 'symbol fill color', index='color'),
ValueAttribute('markeredgewidth', 'symbol linewidth'),
]
agr_axis_attrs = [
StaticAttribute('xaxis', 'frame background pattern 1'),
StaticAttribute('xaxis', 'xaxis label char size 1.0'),
StaticAttribute('yaxis', 'yaxis label char size 1.0'),
StaticAttribute('xaxis', 'xaxis ticklabel char size 1.0'),
StaticAttribute('yaxis', 'yaxis ticklabel char size 1.0'),
ValueAttribute('world', 'world', function=get_world),
ValueAttribute('view', 'view', function=get_view),
ValueAttribute('title', 'subtitle'),
ValueAttribute('xlabel', 'xaxis label'),
ValueAttribute('ylabel', 'yaxis label'),
ValueAttribute('xscale', 'xaxes scale Logarithmic', condition=lambda scale: 'log' in scale),
ValueAttribute('xscale', 'xaxis ticklabel format power', condition=lambda scale: 'log' in scale),
ValueAttribute('xscale', 'xaxis ticklabel prec 0', condition=lambda scale: 'log' in scale),
ValueAttribute('xscale', 'xaxis tick minor ticks', function=lambda ax: 9 if 'log' in ax.get_xscale() else 4),
ValueAttribute('yscale', 'yaxes scale Logarithmic', condition=lambda scale: 'log' in scale),
ValueAttribute('yscale', 'yaxis ticklabel format power', condition=lambda scale: 'log' in scale),
ValueAttribute('yscale', 'yaxis ticklabel prec 0', condition=lambda scale: 'log' in scale),
ValueAttribute('xscale', 'yaxis tick minor ticks', function=lambda ax: 9 if 'log' in ax.get_yscale() else 4),
ValueAttribute('xticks', 'xaxis tick major', function=get_major_ticks('x')),
ValueAttribute('yticks', 'yaxis tick major', function=get_major_ticks('y')),
ValueAttribute('xticklabels', 'xaxis ticklabel', function=get_ticklabels_on('x')),
ValueAttribute('yticklabels', 'yaxis ticklabel', function=get_ticklabels_on('y')),
ValueAttribute('xlabelposition', 'xaxis label place',
function=lambda ax: 'opposite' if ax.xaxis.get_label_position() == 'top' else 'normal'),
ValueAttribute('xtickposition', 'xaxis ticklabel place',
function=lambda ax: 'opposite' if all([t.get_position()[1] >= 0.9 for t in ax.xaxis.get_ticklabels()]) else 'normal'),
ValueAttribute('ylabelposition', 'yaxis label place',
function=lambda ax: 'opposite' if ax.yaxis.get_label_position() == 'right' else 'normal'),
ValueAttribute('ytickposition', 'yaxis ticklabel place',
function=lambda ax: 'opposite' if all([t.get_position()[0] >= 0.9 for t in ax.yaxis.get_ticklabels()]) else 'normal'),
# tax.yaxis.get_ticks_position() == 'right' else 'normal'),
ValueAttribute('legend', 'legend', function=get_legend),
StaticAttribute('legend', 'legend loctype view'),
StaticAttribute('legend', 'legend char size 1.0'),
ValueAttribute('legend', 'legend', function=get_legend_position)
]
agr_text_attrs = [
StaticAttribute('string', 'on'),
StaticAttribute('string', 'loctype view'),
StaticAttribute('string', 'char size 1.0'),
ValueAttribute('position', '', function=get_text_position),
ValueAttribute('text', 'def')
]
agr_arrow_attrs = [
StaticAttribute('line', 'on'),
StaticAttribute('line', 'loctype world'),
StaticAttribute('line', 'color 1'),
StaticAttribute('line', 'linewidth 2'),
StaticAttribute('line', 'linestyle 1'),
StaticAttribute('line', 'arrow 2'),
ValueAttribute('line', '', function=get_arrow_coordinates),
]
class AgrFile:
head = '@version 50125\n'
body = tail = ''
indent = 0
kwargs = {}
def writeline(self, text, part='body', **kwargs):
self.kwargs = {**self.kwargs, **kwargs}
content = getattr(self, part)
content += '@' + ' ' * self.indent + escapestr(text.format(**self.kwargs)) + '\n'
setattr(self, part, content)
def writedata(self, data):
self.tail += '@target {axis}.{line}\n@type xy\n'.format(**self.kwargs)
for x, y in data:
if np.isfinite([x, y]).all():
self.tail += '{} {}\n'.format(x, y)
self.tail += '&\n'
def save(self, filename):
with open(filename, 'w') as file:
file.write(self.head)
file.write(self.body)
file.write(self.tail)
def _process_attributes(attrs, source, agr, prefix=''):
for attr, attr_dict in attrs.items():
attr_type = attr_dict['type']
if 'static' in attr_type:
value = ''
elif 'function' in attr_type:
value = attr_dict['function'](source)
else:
value = getattr(source, 'get_{}'.format(attr))()
if 'condition' in attr_dict:
if not attr_dict['condition'](value):
continue
if is_string_like(value):
value = latex_to_xmgrace(value)
if 'index' in attr_type:
attr_list = agr_attr_lists[attr_dict.get('maplist', attr)]
index = indexed(attr_list)(value)
if index is None:
if 'map' in attr_type:
attr_list.append(value)
index = attr_list.index(value)
else:
index = 1
value = index
agr.writeline(prefix + attr_dict['fmt'], attr=attr, value=value)
def process_attributes(attrs, source, agr, prefix='', **kwargs):
for attr in attrs:
fmt = attr.format(source, **kwargs)
if fmt is not None:
agr.writeline(prefix + fmt)
def export_to_agr(figure, filename, **kwargs):
"""
Export a matplotlib figure to xmgrace format.
"""
ValueAttribute.reset_colors()
cc = ColorConverter()
agr = AgrFile()
# agr_attr_lists['color'] = ['white', 'black']
# agr_colors =
papersize = figure.get_size_inches()*120
agr.writeline('page size {}, {}'.format(*papersize))
agr.writeline('default char size {}'.format(mpl.rcParams['font.size'] / 12))
for i, axis in enumerate(figure.axes):
agr_axis = 'g{}'.format(i)
agr.indent = 0
agr.writeline('{axis} on', axis=agr_axis)
agr.writeline('{axis} hidden false')
agr.writeline('{axis} type XY')
agr.writeline('{axis} stacked false')
agr.writeline('with {axis}')
agr.indent = 4
process_attributes(agr_axis_attrs, axis, agr, **kwargs)
for j, line in enumerate(axis.lines):
agr.kwargs['line'] = 's{}'.format(j)
process_attributes(agr_line_attrs, line, agr, '{line} ', **kwargs)
agr.writedata(line.get_xydata())
for text in axis.texts:
agr.indent = 0
agr.writeline('with string')
agr.indent = 4
process_attributes(agr_text_attrs, text, agr, 'string ', **kwargs)
# this is a text of an arrow-annotation
if hasattr(text, 'arrow_patch'):
agr.indent = 0
agr.writeline('with line')
agr.indent = 4
agr.writeline(f'line {agr_axis}')
process_attributes(agr_arrow_attrs, text, agr, 'line ', **kwargs)
agr.indent = 0
agr.writeline('line def')
agr.indent = 0
tudcol_rev = {}
for name, color in tudcolors.items():
if isinstance(color, str):
rgba, = cc.to_rgba_array(color)
tudcol_rev[tuple(rgba)] = name
for i, color in enumerate(ValueAttribute.attr_lists['color']):
# print(i, color)
if color is not 'none':
rgba, = cc.to_rgba_array(color)
rgb_tuple = tuple(int(255 * c) for c in rgba[:3])
color_name = tudcol_rev.get(tuple(rgba), color)
agr.writeline('map color {index} to {rgb}, "{color}"',
part='head', index=i, rgb=rgb_tuple, color=color_name)
agr.save(filename)
def load_agr_data(agrfile, nolabels=False):
"""
Load all named data sets from an agrfile.
"""
graphs = OrderedDict()
cur_graph = None
target = None
with open(agrfile, 'r', errors='replace') as f:
lines = f.readlines()
for org_line in lines:
line = org_line.lower()
if '@with' in line:
graph_id = line.split()[1]
if graph_id not in graphs:
graphs[graph_id] = {}
cur_graph = graphs[graph_id]
elif 'legend' in line and cur_graph is not None:
ma = re.search('([sS]\d+) .+ "(.*)"', org_line)
if ma is not None:
label = ma.group(2)
sid = ma.group(1).lower()
if label == '' or nolabels:
gid = [k for k, v in graphs.items() if v is cur_graph][0]
label = '{}.{}'.format(gid, sid)
cur_graph[sid] = {'label': label}
elif '@target' in line:
ma = re.search('(g\d+)\.(s\d+)', line.lower())
gid = ma.group(1)
sid = ma.group(2)
target = []
if sid not in graphs[gid]:
graphs[gid][sid] = {'label': '{}.{}'.format(gid, sid)}
graphs[gid][sid]['data'] = target
elif target is not None and '@type' in line:
continue
elif '&' in line:
target = None
elif target is not None:
target.append([float(d) for d in line.split()])
data = OrderedDict()
for _, graph in graphs.items():
for _, set in graph.items():
if 'data' in set:
data[set['label']] = np.array(set['data'])
else:
print(_, set)
data[set['label']] = np.empty((0,))
return data