Skip to content

Commit e1fdbc3

Browse files
committed
Added a Linestyle and Color class
1 parent 76101fc commit e1fdbc3

File tree

8 files changed

+320
-37
lines changed

8 files changed

+320
-37
lines changed

src/maxplotlib/canvas/canvas.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ def plot_matplotlib(self, show=True, savefig=False):
139139
else:
140140
fig_width, fig_height = plt_utils.set_size(width=self._width, ratio=self._ratio)
141141

142-
print(fig_width, fig_height, fig_width / self._dpi, fig_height / self._dpi)
143142
fig, axes = plt.subplots(self.nrows, self.ncols, figsize=(fig_width / self._dpi, fig_height / self._dpi), squeeze=False, dpi = self._dpi)
144143

145144
for (row, col), subplot in self.subplots.items():

src/maxplotlib/colors/__init__.py

Whitespace-only changes.

src/maxplotlib/colors/colors.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import numpy as np
2+
import matplotlib.colors as mcolors
3+
import matplotlib.patches as patches
4+
import re
5+
6+
class Color:
7+
def __init__(self, color_spec):
8+
"""
9+
Initialize the Color object by parsing the color specification.
10+
11+
Parameters:
12+
- color_spec: Can be a TikZ color string (e.g., 'blue!20'), a standard color name,
13+
an RGB tuple, a hex code, etc.
14+
"""
15+
self.color_spec = color_spec
16+
self.rgb = self._parse_color(color_spec)
17+
18+
def _parse_color(self, color_spec):
19+
"""
20+
Internal method to parse the color specification and convert it to an RGB tuple.
21+
22+
Parameters:
23+
- color_spec: The color specification.
24+
25+
Returns:
26+
- rgb: A tuple of (r, g, b) values, each between 0 and 1.
27+
"""
28+
# If it's already an RGB tuple or list
29+
if isinstance(color_spec, (list, tuple)) and len(color_spec) == 3:
30+
# Normalize values if necessary
31+
rgb = tuple(float(c) / 255 if c > 1 else float(c) for c in color_spec)
32+
return rgb
33+
34+
# If it's a hex code
35+
if isinstance(color_spec, str) and color_spec.startswith('#'):
36+
return mcolors.hex2color(color_spec)
37+
38+
# If it's a TikZ color string
39+
match = re.match(r'(\w+)!([\d.]+)', color_spec)
40+
if match:
41+
base_color_name, percentage = match.groups()
42+
percentage = float(percentage)
43+
base_color = mcolors.to_rgb(base_color_name)
44+
white = np.array([1.0, 1.0, 1.0])
45+
mix = percentage / 100.0
46+
color = mix * np.array(base_color) + (1 - mix) * white
47+
return tuple(color)
48+
49+
# Else, try to parse as a standard color name
50+
try:
51+
return mcolors.to_rgb(color_spec)
52+
except ValueError:
53+
raise ValueError(f"Invalid color specification: '{color_spec}'")
54+
55+
def to_rgb(self):
56+
"""
57+
Return the color as an RGB tuple.
58+
59+
Returns:
60+
- rgb: A tuple of (r, g, b) values, each between 0 and 1.
61+
"""
62+
return self.rgb
63+
64+
def to_hex(self):
65+
"""
66+
Return the color as a hex code.
67+
68+
Returns:
69+
- hex_code: A string representing the color in hex format.
70+
"""
71+
return mcolors.to_hex(self.rgb)
72+
73+
def to_rgba(self, alpha=1.0):
74+
"""
75+
Return the color as an RGBA tuple.
76+
77+
Parameters:
78+
- alpha (float): The alpha (opacity) value between 0 and 1.
79+
80+
Returns:
81+
- rgba: A tuple of (r, g, b, a) values.
82+
"""
83+
return (*self.rgb, alpha)

src/maxplotlib/linestyle/__init__.py

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import re
2+
3+
class Linestyle:
4+
def __init__(self, style_spec):
5+
"""
6+
Initialize the Linestyle object by parsing the style specification.
7+
8+
Parameters:
9+
- style_spec: Can be a TikZ-style line style string (e.g., 'dashed', 'dotted', 'solid', 'dashdot'),
10+
or a custom dash pattern.
11+
"""
12+
self.style_spec = style_spec
13+
self.matplotlib_style = self._parse_style(style_spec)
14+
15+
def _parse_style(self, style_spec):
16+
"""
17+
Internal method to parse the style specification and convert it to a Matplotlib linestyle.
18+
19+
Parameters:
20+
- style_spec: The style specification.
21+
22+
Returns:
23+
- linestyle: A Matplotlib linestyle string or dash pattern.
24+
"""
25+
# Predefined mappings from TikZ to Matplotlib
26+
linestyle_mapping = {
27+
'solid': 'solid',
28+
'dashed': 'dashed',
29+
'dotted': 'dotted',
30+
'dashdot': 'dashdot',
31+
# You can add more styles or custom dash patterns
32+
}
33+
34+
# Check for predefined styles
35+
if style_spec in linestyle_mapping:
36+
return linestyle_mapping[style_spec]
37+
else:
38+
# Check if it's a custom dash pattern, e.g., 'dash pattern=on 5pt off 2pt'
39+
match = re.match(r'dash pattern=on ([\d.]+)pt off ([\d.]+)pt', style_spec)
40+
if match:
41+
on_length = float(match.group(1))
42+
off_length = float(match.group(2))
43+
# Matplotlib dash pattern is specified in points
44+
return (0, (on_length, off_length))
45+
else:
46+
# Default to solid if style is unknown
47+
print(f"Unknown line style: '{style_spec}', defaulting to 'solid'")
48+
return 'solid'
49+
50+
def to_matplotlib(self):
51+
"""
52+
Return the line style in Matplotlib format.
53+
54+
Returns:
55+
- linestyle: A Matplotlib linestyle string or dash sequence.
56+
"""
57+
return self.matplotlib_style

src/maxplotlib/subfigure/tikz_figure.py

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
import os
33
import tempfile
44
from matplotlib.image import imread
5+
import numpy as np
6+
import re
7+
8+
9+
import matplotlib.patches as patches
10+
from maxplotlib.colors.colors import Color
11+
from maxplotlib.linestyle.linestyle import Linestyle
512

613
class Node:
714
def __init__(self, x, y, label="", content="",**kwargs):
@@ -214,11 +221,132 @@ def compile_pdf(self, filename='output.pdf'):
214221
print(f"PDF successfully compiled and saved as '{filename}'.")
215222
else:
216223
print("PDF compilation failed. Please check the LaTeX log for details.")
224+
217225
def plot_matplotlib(self, ax):
218226
"""
219-
Plot all lines on the provided axis.
227+
Plot all nodes and paths on the provided axis using Matplotlib.
220228
221229
Parameters:
222-
ax (matplotlib.axes.Axes): Axis on which to plot the lines.
230+
- ax (matplotlib.axes.Axes): Axis on which to plot the figure.
223231
"""
224-
232+
233+
# Plot paths first so they appear behind nodes
234+
for path in self.paths:
235+
x_coords = [next(node.x for node in self.nodes if node.label == label) for label in path.nodes]
236+
y_coords = [next(node.y for node in self.nodes if node.label == label) for label in path.nodes]
237+
238+
# Parse path color
239+
path_color_spec = path.options.get('color', 'black')
240+
try:
241+
color = Color(path_color_spec).to_rgb()
242+
except ValueError as e:
243+
print(e)
244+
color = 'black'
245+
246+
# Parse line width
247+
line_width_spec = path.options.get('line_width', 1)
248+
if isinstance(line_width_spec, str):
249+
match = re.match(r'([\d.]+)(pt)?', line_width_spec)
250+
if match:
251+
line_width = float(match.group(1))
252+
else:
253+
print(f"Invalid line width specification: '{line_width_spec}', defaulting to 1")
254+
line_width = 1
255+
else:
256+
line_width = float(line_width_spec)
257+
258+
# Parse line style using Linestyle class
259+
style_spec = path.options.get('style', 'solid')
260+
linestyle = Linestyle(style_spec).to_matplotlib()
261+
262+
ax.plot(
263+
x_coords,
264+
y_coords,
265+
color=color,
266+
linewidth=line_width,
267+
linestyle=linestyle,
268+
zorder=1 # Lower z-order to place behind nodes
269+
)
270+
271+
# Plot nodes after paths so they appear on top
272+
for node in self.nodes:
273+
# Determine shape and size
274+
shape = node.options.get('shape', 'circle')
275+
fill_color_spec = node.options.get('fill', 'white')
276+
edge_color_spec = node.options.get('draw', 'black')
277+
linewidth = float(node.options.get('line_width', 1))
278+
size = float(node.options.get('size', 1))
279+
280+
# Parse colors using the Color class
281+
try:
282+
facecolor = Color(fill_color_spec).to_rgb()
283+
except ValueError as e:
284+
print(e)
285+
facecolor = 'white'
286+
287+
try:
288+
edgecolor = Color(edge_color_spec).to_rgb()
289+
except ValueError as e:
290+
print(e)
291+
edgecolor = 'black'
292+
293+
# Plot shapes
294+
if shape == 'circle':
295+
radius = size / 2
296+
circle = patches.Circle(
297+
(node.x, node.y),
298+
radius,
299+
facecolor=facecolor,
300+
edgecolor=edgecolor,
301+
linewidth=linewidth,
302+
zorder=2 # Higher z-order to place on top of paths
303+
)
304+
ax.add_patch(circle)
305+
elif shape == 'rectangle':
306+
width = height = size
307+
rect = patches.Rectangle(
308+
(node.x - width / 2, node.y - height / 2),
309+
width,
310+
height,
311+
facecolor=facecolor,
312+
edgecolor=edgecolor,
313+
linewidth=linewidth,
314+
zorder=2 # Higher z-order
315+
)
316+
ax.add_patch(rect)
317+
else:
318+
# Default to circle if shape is unknown
319+
radius = size / 2
320+
circle = patches.Circle(
321+
(node.x, node.y),
322+
radius,
323+
facecolor=facecolor,
324+
edgecolor=edgecolor,
325+
linewidth=linewidth,
326+
zorder=2
327+
)
328+
ax.add_patch(circle)
329+
330+
# Add text inside the shape
331+
if node.content:
332+
ax.text(
333+
node.x,
334+
node.y,
335+
node.content,
336+
fontsize=10,
337+
ha='center',
338+
va='center',
339+
wrap=True,
340+
zorder=3 # Even higher z-order for text
341+
)
342+
343+
# Remove axes, ticks, and legend
344+
ax.axis('off')
345+
346+
# Adjust plot limits
347+
all_x = [node.x for node in self.nodes]
348+
all_y = [node.y for node in self.nodes]
349+
padding = 1 # Adjust padding as needed
350+
ax.set_xlim(min(all_x) - padding, max(all_x) + padding)
351+
ax.set_ylim(min(all_y) - padding, max(all_y) + padding)
352+
ax.set_aspect('equal', adjustable='datalim')

tutorials/tutorial_02.ipynb

Lines changed: 30 additions & 28 deletions
Large diffs are not rendered by default.

tutorials/tutorial_1.ipynb

Lines changed: 19 additions & 5 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)