prefig 0.2.15.dev20250514053750__py3-none-any.whl → 0.5.6.dev20260130060411__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. prefig/cli.py +46 -26
  2. prefig/core/CTM.py +18 -3
  3. prefig/core/__init__.py +2 -0
  4. prefig/core/annotations.py +115 -2
  5. prefig/core/area.py +20 -4
  6. prefig/core/arrow.py +9 -3
  7. prefig/core/axes.py +909 -0
  8. prefig/core/circle.py +13 -4
  9. prefig/core/clip.py +1 -1
  10. prefig/core/coordinates.py +31 -4
  11. prefig/core/diagram.py +129 -6
  12. prefig/core/graph.py +236 -23
  13. prefig/core/grid_axes.py +181 -495
  14. prefig/core/image.py +127 -0
  15. prefig/core/label.py +14 -4
  16. prefig/core/legend.py +4 -4
  17. prefig/core/line.py +92 -1
  18. prefig/core/math_utilities.py +155 -0
  19. prefig/core/network.py +2 -2
  20. prefig/core/parametric_curve.py +15 -3
  21. prefig/core/path.py +25 -0
  22. prefig/core/point.py +18 -2
  23. prefig/core/polygon.py +7 -1
  24. prefig/core/read.py +6 -3
  25. prefig/core/rectangle.py +10 -4
  26. prefig/core/repeat.py +37 -3
  27. prefig/core/shape.py +12 -1
  28. prefig/core/slope_field.py +142 -0
  29. prefig/core/tags.py +8 -2
  30. prefig/core/tangent_line.py +36 -12
  31. prefig/core/user_namespace.py +8 -0
  32. prefig/core/utilities.py +9 -1
  33. prefig/engine.py +73 -28
  34. prefig/resources/diagcess/diagcess.js +7 -1
  35. prefig/resources/schema/pf_schema.rnc +89 -6
  36. prefig/resources/schema/pf_schema.rng +321 -5
  37. prefig/scripts/install_mj.py +10 -11
  38. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/METADATA +12 -16
  39. prefig-0.5.6.dev20260130060411.dist-info/RECORD +68 -0
  40. prefig-0.2.15.dev20250514053750.dist-info/RECORD +0 -66
  41. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/LICENSE +0 -0
  42. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/WHEEL +0 -0
  43. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/entry_points.txt +0 -0
prefig/core/axes.py ADDED
@@ -0,0 +1,909 @@
1
+ import lxml.etree as ET
2
+ import math
3
+ import re
4
+ import logging
5
+ import numpy as np
6
+ import copy
7
+ from . import math_utilities as math_util
8
+ from . import utilities as util
9
+ from . import user_namespace as un
10
+ from . import label
11
+ from . import line
12
+ from . import arrow
13
+ from . import CTM
14
+
15
+ log = logging.getLogger('prefigure')
16
+
17
+ # These tags can appear in an <axes> or <grid-axes>
18
+ axes_tags = {'xlabel', 'ylabel'}
19
+
20
+ def is_axes_tag(tag):
21
+ return tag in axes_tags
22
+
23
+ # Automate finding the positions where ticks and labels go
24
+ label_delta = {2: 0.2, 3: 0.5, 4: 0.5, 5: 1,
25
+ 6: 1, 7: 1, 8: 1, 9: 1, 10: 1, 11: 1,
26
+ 12: 2, 13: 2, 14: 2, 15: 2, 16: 2, 17: 2,
27
+ 18: 2, 19: 2, 20: 2}
28
+
29
+ def find_label_positions(coordinate_range, pi_format = False):
30
+ if pi_format:
31
+ coordinate_range = [c/math.pi for c in coordinate_range]
32
+ dx = 1
33
+ distance = abs(coordinate_range[1]-coordinate_range[0])
34
+ while distance > 10:
35
+ distance /= 10
36
+ dx *= 10
37
+ while distance <= 1:
38
+ distance *= 10
39
+ dx /= 10
40
+ if dx > 1:
41
+ dx *= label_delta[round(2*distance)]
42
+ dx = int(dx)
43
+ else:
44
+ dx *= label_delta[round(2*distance)]
45
+ if coordinate_range[1] < coordinate_range[0]:
46
+ dx *= -1
47
+ x0 = dx * math.floor(coordinate_range[0]/dx+1e-10)
48
+ x1 = dx * math.ceil(coordinate_range[1]/dx-1e-10)
49
+ else:
50
+ x0 = dx * math.ceil(coordinate_range[0]/dx-1e-10)
51
+ x1 = dx * math.floor(coordinate_range[1]/dx+1e-10)
52
+ return (x0, dx, x1)
53
+
54
+ def find_log_positions(r):
55
+ # argument r could have
56
+ # three arguments if user supplied
57
+ # two arguments if not
58
+ # each range 10^j -> 10^j+1 could have 1, 2, 5, 10, or 1/n lines
59
+ x0 = np.log10(r[0])
60
+ x1 = np.log10(r[-1])
61
+ if len(r) == 3:
62
+ if r[1] < 1:
63
+ spacing = r[1]
64
+ elif r[1] < 2:
65
+ spacing = 1
66
+ elif r[1] < 4:
67
+ spacing = 2
68
+ elif r[1] < 7:
69
+ spacing = 5
70
+ else:
71
+ spacing = 10
72
+ else:
73
+ width = abs(x1 - x0)
74
+ if width < 1.5:
75
+ spacing = 2
76
+ elif width <= 10:
77
+ spacing = 1
78
+ else:
79
+ spacing = 5/width
80
+
81
+ x0 = math.floor(x0)
82
+ x1 = math.ceil(x1)
83
+ positions = []
84
+ if spacing <= 1:
85
+ gap = round(1/spacing)
86
+ x = x0
87
+ while x <= x1:
88
+ positions.append(10**x)
89
+ x += gap
90
+ else:
91
+ if spacing == 2:
92
+ intermediate = [1,5]
93
+ elif spacing == 5:
94
+ intermediate = [1,2,4,6,8]
95
+ elif spacing == 10:
96
+ intermediate = [1,2,3,4,5,6,7,8,9]
97
+ else:
98
+ intermediate = [1]
99
+ x = x0
100
+ while x <= x1:
101
+ positions += [10**x*c for c in intermediate]
102
+ x += 1
103
+ return positions
104
+
105
+ # find a string representation of x*pi
106
+ def get_pi_text(x):
107
+ if abs(abs(x) - 1) < 1e-10:
108
+ if x < 0:
109
+ return r'-\pi'
110
+ return r'\pi'
111
+
112
+ if abs(x - round(x)) < 1e-10:
113
+ return str(round(x))+r'\pi'
114
+ if abs(4*x - round(4*x)) < 1e-10:
115
+ num = round(4*x)
116
+ if num == -1:
117
+ return r'-\frac{\pi}{4}'
118
+ if num == 1:
119
+ return r'\frac{\pi}{4}'
120
+ if num % 2 == 1:
121
+ if num < 0:
122
+ return f'-\\frac{{{-num}\\pi}}{{4}}'
123
+ else:
124
+ return f'\\frac{{{num}\\pi}}{{4}}'
125
+ if abs(2*x - round(2*x)) < 1e-10:
126
+ num = round(2*x)
127
+ if num == -1:
128
+ return r'-\frac{\pi}{2}'
129
+ if num == 1:
130
+ return r'\frac{\pi}{2}'
131
+ if num < 0:
132
+ return f'-\\frac{{{-num}\\pi}}{{2}}'
133
+ else:
134
+ return f'\\frac{{{num}\\pi}}{{2}}'
135
+ if abs(3*x - round(3*x)) < 1e-10:
136
+ num = round(3*x)
137
+ if num == -1:
138
+ return r'-\frac{\pi}{3}'
139
+ if num == 1:
140
+ return r'\frac{\pi}{3}'
141
+ if num < 0:
142
+ return f'-\\frac{{{-num}\\pi}}{{3}}'
143
+ else:
144
+ return f'\\frac{{{num}\\pi}}{{3}}'
145
+ if abs(6*x - round(6*x)) < 1e-10:
146
+ num = round(6*x)
147
+ if num == -1:
148
+ return r'-\frac{\pi}{6}'
149
+ if num == 1:
150
+ return r'\frac{\pi}{6}'
151
+ if num < 0:
152
+ return f'-\\frac{{{-num}\\pi}}{{6}}'
153
+ else:
154
+ return f'\\frac{{{num}\\pi}}{{6}}'
155
+ return r'{0:g}\pi'.format(x)
156
+
157
+
158
+ # Add a graphical element for axes. All the axes sit inside a group
159
+ # There are a number of options to add: labels, tick marks, etc
160
+
161
+ class Axes():
162
+ def __init__(self, element, diagram, parent):
163
+ self.tactile = diagram.output_format() == "tactile"
164
+ self.stroke = element.get('stroke', 'black')
165
+ self.thickness = element.get('thickness', '2')
166
+
167
+ self.axes = ET.SubElement(parent, 'g',
168
+ attrib={
169
+ 'id': element.get('id', 'pf__axes'),
170
+ 'stroke': self.stroke,
171
+ 'stroke-width': self.thickness
172
+ }
173
+ )
174
+ util.cliptobbox(self.axes, element, diagram)
175
+
176
+ # which axes are we asked to build
177
+ self.axes_attribute = element.get("axes", None)
178
+ if self.axes_attribute == "all":
179
+ self.axes_attribute = None
180
+ element.attrib.pop("axes")
181
+ self.horizontal_axis = element.get("axes", "horizontal") == "horizontal"
182
+ self.vertical_axis = element.get("axes", "vertical") == "vertical"
183
+
184
+ self.clear_background = element.get('clear-background', 'no')
185
+ self.decorations = element.get('decorations', 'yes')
186
+ self.h_pi_format = element.get('h-pi-format', 'no') == 'yes'
187
+ self.v_pi_format = element.get('v-pi-format', 'no') == 'yes'
188
+ if self.tactile:
189
+ self.ticksize = (18, 0)
190
+ else:
191
+ self.ticksize = (3, 3)
192
+ if element.get('tick-size', None) is not None:
193
+ self.ticksize = un.valid_eval(element.get('tick-size'))
194
+ if not isinstance(self.ticksize, np.ndarray):
195
+ self.ticksize = [self.ticksize, self.ticksize]
196
+
197
+ self.bbox = diagram.bbox()
198
+ self.position_tolerance = 1e-10
199
+
200
+ try:
201
+ self.arrows = int(element.get('arrows', '0'))
202
+ except:
203
+ log.error(f"Error in <axes> parsing arrows={element.get('arrows')}")
204
+ self.arrows = 0
205
+
206
+ self.position_axes(element, diagram)
207
+ self.apply_axis_labels(element, diagram, parent)
208
+
209
+ if element.get('bounding-box', 'no') == 'yes':
210
+ rect = ET.SubElement(self.axes, 'rect')
211
+ ul = diagram.transform([self.bbox[0], self.bbox[3]])
212
+ lr = diagram.transform([self.bbox[2], self.bbox[1]])
213
+ w = lr[0] - ul[0]
214
+ h = lr[1] - ul[1]
215
+ rect.set('x', util.float2str(ul[0]))
216
+ rect.set('y', util.float2str(ul[1]))
217
+ rect.set('width', util.float2str(w))
218
+ rect.set('height', util.float2str(h))
219
+ rect.set('fill', 'none')
220
+
221
+ if self.horizontal_axis:
222
+ self.add_h_axis(element, diagram, self.arrows)
223
+ self.h_tick_group = ET.Element('g')
224
+ self.horizontal_ticks(element, diagram)
225
+ self.h_labels(element, diagram, parent)
226
+ if self.vertical_axis:
227
+ self.add_v_axis(element, diagram, self.arrows)
228
+ self.v_tick_group = ET.Element('g')
229
+ self.vertical_ticks(element, diagram)
230
+ self.v_labels(element, diagram, parent)
231
+
232
+
233
+ def position_axes(self, element, diagram):
234
+ scales = diagram.get_scales()
235
+ self.y_axis_location = 0
236
+ self.y_axis_offsets = (0,0)
237
+ self.h_zero_include = False
238
+ self.top_labels = False
239
+ if float(self.bbox[1]) * float(self.bbox[3]) >= 0:
240
+ if self.bbox[3] <= 0:
241
+ self.top_labels = True
242
+ self.y_axis_location = self.bbox[3]
243
+ if self.bbox[3] < 0:
244
+ self.y_axis_offsets = (0,-5)
245
+ else:
246
+ if abs(self.bbox[1]) > 1e-10:
247
+ self.y_axis_location = self.bbox[1]
248
+ self.y_axis_offsets = (5,0)
249
+
250
+ h_frame = element.get('h-frame', None)
251
+ if h_frame == 'bottom':
252
+ self.y_axis_location = self.bbox[1]
253
+ self.y_axis_offsets = (0,0)
254
+ self.h_zero_include = True
255
+ if h_frame == 'top':
256
+ self.y_axis_location = self.bbox[3]
257
+ self.y_axis_offsets = (0,0)
258
+ self.h_zero_include = True
259
+ self.top_labels = True
260
+
261
+ if scales[1] == 'log':
262
+ self.y_axis_offsets = (0,0)
263
+ self.h_zero_include = True
264
+ self.y_axis_offsets = np.array(self.y_axis_offsets)
265
+
266
+ # which locations will not get ticks or labels
267
+ self.h_exclude = []
268
+ self.h_zero_label = element.get("h-zero-label", "no") == "yes"
269
+ if (
270
+ not self.h_zero_include and
271
+ self.axes_attribute != 'horizontal' and
272
+ not self.h_zero_label
273
+ ):
274
+ self.h_exclude.append(0)
275
+
276
+ # ticks move up when the horizontal axis is on top
277
+ self.h_tick_direction = 1
278
+ if self.top_labels:
279
+ self.h_tick_direction = -1
280
+
281
+
282
+ self.x_axis_location = 0
283
+ self.x_axis_offsets = (0,0)
284
+ self.v_zero_include = False
285
+ self.right_labels = False
286
+ if float(self.bbox[0]) * float(self.bbox[2]) >= 0:
287
+ if self.bbox[2] <= 0:
288
+ self.right_labels = True
289
+ self.x_axis_location = self.bbox[2]
290
+ if self.bbox[2] < 0:
291
+ self.x_axis_offsets = (0,-10)
292
+ else:
293
+ if abs(self.bbox[0]) > 1e-10:
294
+ self.x_axis_location = self.bbox[0]
295
+ self.x_axis_offsets = (10,0)
296
+
297
+ v_frame = element.get('v-frame', None)
298
+ if v_frame == 'left':
299
+ self.x_axis_location = self.bbox[0]
300
+ self.x_axis_offsets = (0,0)
301
+ self.v_zero_include = True
302
+ if v_frame == 'right':
303
+ self.x_axis_location = self.bbox[2]
304
+ self.x_axis_offsets = (0,0)
305
+ self.v_zero_include = True
306
+ self.right_labels = True
307
+
308
+ if scales[1] == 'log':
309
+ self.x_axis_offsets = (0,0)
310
+ self.v_zero_include = True
311
+
312
+ self.x_axis_offsets = np.array(self.x_axis_offsets)
313
+
314
+ # which locations will not get ticks or labels
315
+ self.v_exclude = []
316
+ self.v_zero_label = element.get("v-zero-label", "no") == "yes"
317
+ if (
318
+ not self.v_zero_include and
319
+ self.axes_attribute != 'vertical' and
320
+ not self.v_zero_label
321
+ ):
322
+ self.v_exclude.append(0)
323
+
324
+ # ticks move right when the vertical axis is on the right
325
+ self.v_tick_direction = 1
326
+ if self.right_labels:
327
+ self.v_tick_direction = -1
328
+
329
+
330
+ def apply_axis_labels(self, element, diagram, parent):
331
+ # process xlabel and ylabel
332
+
333
+ xlabel = element.get('xlabel')
334
+ if xlabel is not None:
335
+ el = ET.Element('label')
336
+ math_element = ET.SubElement(el, 'm')
337
+ math_element.text = xlabel
338
+ el.set('clear-background', 'no')
339
+ el.set('p', '({},{})'.format(self.bbox[2],
340
+ self.y_axis_location))
341
+ el.set('alignment', 'xl')
342
+ if self.arrows > 0:
343
+ if self.tactile:
344
+ el.set('offset', '(-6,6)')
345
+ else:
346
+ el.set('offset', '(-2,2)')
347
+
348
+ el.set('clear-background', self.clear_background)
349
+ label.label(el, diagram, parent, outline_status=None)
350
+
351
+ ylabel = element.get('ylabel')
352
+ if ylabel is not None:
353
+ el = ET.Element('label')
354
+ math_element = ET.SubElement(el, 'm')
355
+ math_element.text = ylabel
356
+ el.set('clear-background', 'no')
357
+ el.set('p', '({},{})'.format(self.x_axis_location,
358
+ self.bbox[3]))
359
+ el.set('alignment', 'se')
360
+ if self.arrows > 0:
361
+ el.set('offset', '(2,-2)')
362
+
363
+ el.set('clear-background', self.clear_background)
364
+ label.label(el, diagram, parent, outline_status=None)
365
+
366
+
367
+ for child in element:
368
+ if child.tag == "xlabel":
369
+ child.tag = "label"
370
+ child.set("user-coords", "no")
371
+ anchor = diagram.transform((self.bbox[2], self.y_axis_location))
372
+ child.set("anchor", util.pt2str(anchor, spacer=","))
373
+ if child.get("alignment", None) is None:
374
+ child.set("alignment", "east")
375
+ if child.get("offset", None) is None:
376
+ if self.arrows > 0:
377
+ child.set("offset", "(2,0)")
378
+ else:
379
+ child.set("offset", "(1,0)")
380
+
381
+ label.label(child, diagram, parent)
382
+ continue
383
+
384
+ if child.tag == "ylabel":
385
+ child.tag = "label"
386
+ child.set("user-coords", "no")
387
+ anchor = diagram.transform((self.x_axis_location, self.bbox[3]))
388
+ child.set("anchor", util.pt2str(anchor, spacer=","))
389
+ if child.get("alignment", None) is None:
390
+ child.set("alignment", "north")
391
+ if child.get("offset", None) is None:
392
+ if self.arrows > 0:
393
+ child.set("offset", "(0,2)")
394
+ else:
395
+ child.set("offset", "(0,1)")
396
+
397
+ label.label(child, diagram, parent)
398
+ continue
399
+ log.info(f"{child.tag} element is not allowed inside a <label>")
400
+ continue
401
+
402
+ def add_h_axis(self, element, diagram, arrows):
403
+ left_axis = diagram.transform((self.bbox[0], self.y_axis_location))
404
+ right_axis = diagram.transform((self.bbox[2], self.y_axis_location))
405
+
406
+ h_line_el = line.mk_line(left_axis,
407
+ right_axis,
408
+ diagram,
409
+ endpoint_offsets = self.x_axis_offsets,
410
+ user_coords = False)
411
+ h_line_el.set('stroke', self.stroke)
412
+ h_line_el.set('stroke-width', self.thickness)
413
+ if arrows > 0:
414
+ arrow.add_arrowhead_to_path(diagram, 'marker-end', h_line_el)
415
+ if arrows > 1:
416
+ arrow.add_arrowhead_to_path(diagram, 'marker-start', h_line_el)
417
+ self.axes.append(h_line_el)
418
+
419
+ def add_v_axis(self, element, diagram, arrows):
420
+ bottom_axis = diagram.transform((self.x_axis_location, self.bbox[1]))
421
+ top_axis = diagram.transform((self.x_axis_location, self.bbox[3]))
422
+
423
+ v_line_el = line.mk_line(bottom_axis,
424
+ top_axis,
425
+ diagram,
426
+ endpoint_offsets = self.y_axis_offsets,
427
+ user_coords = False)
428
+ v_line_el.set('stroke', self.stroke)
429
+ v_line_el.set('stroke-width', self.thickness)
430
+ if arrows > 0:
431
+ arrow.add_arrowhead_to_path(diagram, 'marker-end', v_line_el)
432
+ if arrows > 1:
433
+ arrow.add_arrowhead_to_path(diagram, 'marker-start', v_line_el)
434
+ self.axes.append(v_line_el)
435
+
436
+
437
+ def horizontal_ticks(self, element, diagram):
438
+ hticks = element.get('hticks', None)
439
+ if hticks is None:
440
+ return
441
+
442
+ self.axes.append(self.h_tick_group)
443
+ diagram.add_id(self.h_tick_group)
444
+
445
+ try:
446
+ hticks = un.valid_eval(hticks)
447
+ except:
448
+ log.error(f"Error in <axes> parsing hticks={hticks}")
449
+ return
450
+
451
+ scale = diagram.get_scales()[0]
452
+ if scale == 'log':
453
+ x_positions = find_log_positions(hticks)
454
+ else:
455
+ N = round( (hticks[2] - hticks[0]) / hticks[1])
456
+ x_positions = np.linspace(hticks[0], hticks[2], N+1)
457
+
458
+ for x in x_positions:
459
+ if x < self.bbox[0] or x > self.bbox[2]:
460
+ continue
461
+ if scale == 'log':
462
+ avoid = [abs(np.log10(x) - np.log10(p)) for p in self.h_exclude]
463
+ else:
464
+ avoid = [abs(x - p) for p in self.h_exclude]
465
+ if any([dist < self.position_tolerance for dist in avoid]):
466
+ continue
467
+
468
+ p = diagram.transform((x,self.y_axis_location))
469
+ line_el = line.mk_line((p[0],
470
+ p[1]+self.h_tick_direction*self.ticksize[0]),
471
+ (p[0],
472
+ p[1]-self.h_tick_direction*self.ticksize[1]),
473
+ diagram,
474
+ user_coords=False)
475
+ self.h_tick_group.append(line_el)
476
+
477
+
478
+ def vertical_ticks(self, element, diagram):
479
+ vticks = element.get('vticks', None)
480
+ if vticks is None:
481
+ return
482
+
483
+ self.axes.append(self.v_tick_group)
484
+ diagram.add_id(self.v_tick_group)
485
+
486
+ try:
487
+ vticks = un.valid_eval(vticks)
488
+ except:
489
+ log.error(f"Error in <axes> parsing vticks={vticks}")
490
+ return
491
+
492
+ scale = diagram.get_scales()[1]
493
+ if scale == 'log':
494
+ y_positions = find_log_positions(vticks)
495
+ else:
496
+ N = round( (vticks[2] - vticks[0]) / vticks[1])
497
+ y_positions = np.linspace(vticks[0], vticks[2], N+1)
498
+
499
+ for y in y_positions:
500
+ if y < self.bbox[1] or y > self.bbox[3]:
501
+ continue
502
+ if scale == 'log':
503
+ avoid = [abs(np.log10(y) - np.log10(p)) for p in self.v_exclude]
504
+ else:
505
+ avoid = [abs(y - p) for p in self.v_exclude]
506
+ if any([dist < self.position_tolerance for dist in avoid]):
507
+ continue
508
+
509
+ p = diagram.transform((self.x_axis_location, y))
510
+ line_el = line.mk_line((p[0]-self.v_tick_direction*self.ticksize[0],
511
+ p[1]),
512
+ (p[0]+self.v_tick_direction*self.ticksize[1],
513
+ p[1]),
514
+ diagram,
515
+ user_coords=False)
516
+ self.v_tick_group.append(line_el)
517
+ y += vticks[1]
518
+
519
+
520
+ def h_labels(self, element, diagram, parent):
521
+ hlabels = element.get('hlabels')
522
+ if self.decorations == 'no' and hlabels is None:
523
+ return
524
+
525
+ h_exclude = self.h_exclude[:]
526
+
527
+ scale = diagram.get_scales()[0]
528
+ if hlabels is None:
529
+ if scale == 'log':
530
+ h_positions = find_log_positions((self.bbox[0],
531
+ self.bbox[2]))
532
+ else:
533
+ hlabels = find_label_positions((self.bbox[0], self.bbox[2]),
534
+ pi_format = self.h_pi_format)
535
+ N = round( (hlabels[2] - hlabels[0]) / hlabels[1])
536
+ h_positions = np.linspace(hlabels[0], hlabels[2], N+1)
537
+ h_exclude += [self.bbox[0], self.bbox[2]]
538
+ else:
539
+ try:
540
+ hlabels = un.valid_eval(hlabels)
541
+ if scale == 'log':
542
+ h_positions = find_log_positions(hlabels)
543
+ else:
544
+ N = round( (hlabels[2] - hlabels[0]) / hlabels[1])
545
+ h_positions = np.linspace(hlabels[0], hlabels[2], N+1)
546
+ except:
547
+ log.error(f"Error in <axes> parsing hlabels={hlabels}")
548
+ return
549
+ if self.h_pi_format:
550
+ h_positions = 1/math.pi * h_positions
551
+ h_scale = 1
552
+ if self.h_pi_format:
553
+ h_scale = math.pi
554
+
555
+ if self.h_tick_group.getparent() is None:
556
+ self.axes.append(self.h_tick_group)
557
+
558
+ if self.h_zero_label:
559
+ try:
560
+ h_exclude.remove(0)
561
+ except:
562
+ pass
563
+
564
+ commas = element.get("label-commas", "yes") == "yes"
565
+
566
+ for x in h_positions:
567
+ if x < self.bbox[0] or x > self.bbox[2]:
568
+ continue
569
+ if scale == 'log':
570
+ avoid = [abs(np.log10(x*h_scale) - np.log10(p)) for p in h_exclude]
571
+ else:
572
+ avoid = [abs(x*h_scale - p) for p in h_exclude]
573
+ if any([dist < self.position_tolerance for dist in avoid]):
574
+ continue
575
+
576
+ xlabel = ET.Element('label')
577
+ math_element = ET.SubElement(xlabel, 'm')
578
+ if scale == 'log':
579
+ x_text = np.log10(x)
580
+ frac = x_text % 1.0
581
+ prefix = round(10**frac)
582
+ if prefix != 1:
583
+ x_exp = math.floor(x_text)
584
+ prefix = str(prefix)
585
+ begin = prefix + r'\cdot10^{'
586
+ else:
587
+ x_exp = x_text
588
+ begin = r'10^{'
589
+ math_element.text = begin+'{0:g}'.format(x_exp)+'}'
590
+ xlabel.set('scale', '0.8')
591
+ else:
592
+ #math_element.text = r'\text{'+'{0:g}'.format(x)+'}'
593
+ math_element.text = label_text(x, commas, diagram)
594
+ if self.h_pi_format:
595
+ math_element.text = get_pi_text(x)
596
+
597
+ xlabel.set('p', '({},{})'.format(x*h_scale, self.y_axis_location))
598
+ if self.tactile:
599
+ if self.top_labels:
600
+ xlabel.set('alignment', 'hat')
601
+ xlabel.set('offset', '(0,0)')
602
+ else:
603
+ xlabel.set('alignment', 'ha')
604
+ xlabel.set('offset', '(0,0)')
605
+ else:
606
+ if self.top_labels:
607
+ xlabel.set('alignment', 'north')
608
+ xlabel.set('offset', '(0,7)')
609
+ else:
610
+ xlabel.set('alignment', 'south')
611
+ xlabel.set('offset', '(0,-7)')
612
+
613
+ xlabel.set('clear-background', self.clear_background)
614
+ label.label(xlabel, diagram, parent, outline_status=None)
615
+
616
+ p = diagram.transform((x*h_scale,self.y_axis_location))
617
+ line_el = line.mk_line((p[0],
618
+ p[1]+self.h_tick_direction*self.ticksize[0]),
619
+ (p[0],
620
+ p[1]-self.h_tick_direction*self.ticksize[1]),
621
+ diagram,
622
+ user_coords=False)
623
+
624
+ self.h_tick_group.append(line_el)
625
+
626
+ def v_labels(self, element, diagram, parent):
627
+ vlabels = element.get('vlabels')
628
+ if self.decorations == "no" and vlabels is None:
629
+ return
630
+
631
+ v_exclude = self.v_exclude[:]
632
+
633
+ scale = diagram.get_scales()[1]
634
+ if vlabels is None:
635
+ if scale == 'log':
636
+ v_positions = find_log_positions((self.bbox[1], self.bbox[3]))
637
+ else:
638
+ vlabels = find_label_positions((self.bbox[1], self.bbox[3]),
639
+ pi_format = self.v_pi_format)
640
+ N = round( (vlabels[2] - vlabels[0]) / vlabels[1])
641
+ v_positions = np.linspace(vlabels[0], vlabels[2], N+1)
642
+
643
+ v_exclude += [self.bbox[1], self.bbox[3]]
644
+ else:
645
+ try:
646
+ vlabels = un.valid_eval(vlabels)
647
+ if scale == 'log':
648
+ v_positions = find_log_positions(vlabels)
649
+ else:
650
+ N = round( (vlabels[2] - vlabels[0]) / vlabels[1])
651
+ v_positions = np.linspace(vlabels[0], vlabels[2], N+1)
652
+ except:
653
+ log.error(f"Error in <axes> parsing vlabels={vlabels}")
654
+ return
655
+
656
+ if self.v_pi_format:
657
+ v_positions = 1/math.pi * v_positions
658
+
659
+ v_scale = 1
660
+ if self.v_pi_format:
661
+ v_scale = math.pi
662
+
663
+ if self.v_tick_group.getparent() is None:
664
+ self.axes.append(self.v_tick_group)
665
+
666
+ if element.get("v-zero-label", "no") == "yes":
667
+ try:
668
+ v_exclude.remove(0)
669
+ except:
670
+ pass
671
+
672
+ commas = element.get("label-commas", "yes") == "yes"
673
+
674
+ for y in v_positions:
675
+ if y < self.bbox[1] or y > self.bbox[3]:
676
+ continue
677
+
678
+ if scale == 'log':
679
+ avoid = [abs(np.log10(y*v_scale) - np.log10(p)) for p in v_exclude]
680
+ else:
681
+ avoid = [abs(y*v_scale - p) for p in v_exclude]
682
+ if any([dist < self.position_tolerance for dist in avoid]):
683
+ continue
684
+
685
+ ylabel = ET.Element('label')
686
+ math_element = ET.SubElement(ylabel, 'm')
687
+ if scale == 'log':
688
+ y_text = np.log10(y)
689
+ frac = y_text % 1.0
690
+ prefix = round(10**frac)
691
+ if prefix != 1:
692
+ y_exp = math.floor(y_text)
693
+ prefix = str(prefix)
694
+ begin = prefix + r'\cdot10^{'
695
+ else:
696
+ y_exp = y_text
697
+ begin = r'10^{'
698
+ math_element.text = begin+'{0:g}'.format(y_exp)+'}'
699
+ ylabel.set('scale', '0.8')
700
+ else:
701
+ #math_element.text = r'\text{'+'{0:g}'.format(y)+'}'
702
+ math_element.text = label_text(y, commas, diagram)
703
+ if self.v_pi_format:
704
+ math_element.text = get_pi_text(y)
705
+ # process as a math number
706
+ ylabel.set('p', '({},{})'.format(self.x_axis_location, y*v_scale))
707
+
708
+ if self.tactile:
709
+ if self.right_labels:
710
+ ylabel.set('alignment', 'east')
711
+ ylabel.set('offset', '(25, 0)')
712
+ else:
713
+ ylabel.set('alignment', 'va')
714
+ ylabel.set('offset', '(-25, 0)')
715
+ else:
716
+ if self.right_labels:
717
+ ylabel.set('alignment', 'east')
718
+ ylabel.set('offset', '(7,0)')
719
+ else:
720
+ ylabel.set('alignment', 'west')
721
+ ylabel.set('offset', '(-7,0)')
722
+
723
+ ylabel.set('clear-background', self.clear_background)
724
+ label.label(ylabel, diagram, parent, outline_status=None)
725
+ p = diagram.transform((self.x_axis_location, y*v_scale))
726
+ line_el = line.mk_line((p[0]-self.v_tick_direction*self.ticksize[0],
727
+ p[1]),
728
+ (p[0]+self.v_tick_direction*self.ticksize[1],
729
+ p[1]),
730
+ diagram,
731
+ user_coords=False)
732
+ self.v_tick_group.append(line_el)
733
+
734
+ def label_text(x, commas, diagram):
735
+ # we'll construct a text representation of x
736
+ # maybe it's simple
737
+ if x < 0:
738
+ prefix = '-'
739
+ x = abs(x)
740
+ else:
741
+ prefix = ''
742
+ text = '{0:g}'.format(x)
743
+
744
+ # but it could be in exponential notation
745
+ if text.find('e') >= 0:
746
+ integer = math.floor(x)
747
+ fraction = x - integer
748
+ if fraction > 1e-14:
749
+ suffix = '{0:g}'.format(fraction)[1:]
750
+ else:
751
+ suffix = ''
752
+ int_part = ''
753
+ while integer >= 10:
754
+ int_part = str(integer % 10) + int_part
755
+ integer = int(integer / 10)
756
+ int_part = str(integer) + int_part
757
+ text = int_part + suffix
758
+
759
+ if not commas:
760
+ return r'\text{' + prefix + text + r'}'
761
+
762
+ period = text.find('.')
763
+ comma_include = '{,}'
764
+ if diagram.get_environment() == 'pyodide':
765
+ comma_include = ','
766
+ if period < 0:
767
+ suffix = ''
768
+ else:
769
+ suffix = text[period:]
770
+ text = text[:period]
771
+ while len(text) > 3:
772
+ suffix = comma_include + text[-3:] + suffix
773
+ text = text[:-3]
774
+ text = text + suffix
775
+ return r'\text{' + prefix + text + r'}'
776
+
777
+ def tick_mark(element, diagram, parent, outline_status):
778
+ # tick marks are in the background so there's no need to worry
779
+ # about the outline_status
780
+ if outline_status == 'finish_outline':
781
+ return
782
+
783
+ axis = element.get('axis', 'horizontal')
784
+ tactile = diagram.output_format() == 'tactile'
785
+ location = un.valid_eval(element.get('location', '0'))
786
+ y_axis_location = 0
787
+ x_axis_location = 0
788
+ top_labels = False
789
+ right_labels = False
790
+ if axes_object is not None:
791
+ y_axis_location = axes_object.y_axis_location
792
+ x_axis_location = axes_object.x_axis_location
793
+ top_labels = axes_object.top_labels
794
+ right_labels = axes_object.right_labels
795
+
796
+ if not isinstance(location, np.ndarray):
797
+ if axis == 'horizontal':
798
+ location = (location, y_axis_location)
799
+ else:
800
+ location = (x_axis_location, location)
801
+ p = diagram.transform(location)
802
+
803
+ # ticksize is globally defined but we can change it
804
+ if axes_object is not None:
805
+ size = axes_object.ticksize
806
+
807
+ if element.get('size', None) is not None:
808
+ size = un.valid_eval(element.get('size'))
809
+ if not isinstance(size, np.ndarray):
810
+ size = (size, size)
811
+ else:
812
+ size = (3,3)
813
+
814
+ if tactile:
815
+ size = (18,0)
816
+
817
+ tick_direction = 1
818
+ if axis == 'horizontal':
819
+ if axes_object is not None:
820
+ tick_direction = axes_object.h_tick_direction
821
+ line_el = line.mk_line((p[0], p[1]+tick_direction*size[0]),
822
+ (p[0], p[1]-tick_direction*size[1]),
823
+ diagram,
824
+ user_coords=False)
825
+ else:
826
+ if axes_object is not None:
827
+ tick_direction = axes_object.v_tick_direction
828
+ line_el = line.mk_line((p[0]-tick_direction*size[0], p[1]),
829
+ (p[0]+tick_direction*size[1], p[1]),
830
+ diagram,
831
+ user_coords=False)
832
+
833
+ thickness = element.get('thickness', None)
834
+ if thickness is None:
835
+ if axes_object is None:
836
+ thickness = '2'
837
+ else:
838
+ thickness = axes_object.thickness
839
+
840
+ stroke = element.get('stroke', None)
841
+ if stroke is None:
842
+ if axes_object is None:
843
+ stroke = 'black'
844
+ else:
845
+ stroke = axes_object.stroke
846
+ if tactile:
847
+ thickness = '2'
848
+ stroke = 'black'
849
+
850
+ line_el.set('stroke-width', thickness)
851
+ line_el.set('stroke', stroke)
852
+ parent.append(line_el)
853
+
854
+ try:
855
+ el_text = element.text.strip()
856
+ except:
857
+ el_text = None
858
+ if el_text is not None and (len(el_text) > 0 or len(element) > 0):
859
+ el_copy = copy.deepcopy(element)
860
+ if axis == 'horizontal':
861
+ if tactile:
862
+ if top_labels:
863
+ align = 'hat'
864
+ off = '(0,0)'
865
+ else:
866
+ align = 'ha'
867
+ off = '(0,0)'
868
+ else:
869
+ if top_labels:
870
+ align = 'north'
871
+ off = '(0,7)'
872
+ else:
873
+ align = 'south'
874
+ off = '(0,-7)'
875
+ else:
876
+ if tactile:
877
+ if right_labels:
878
+ align = 'east'
879
+ off = '(25,0)'
880
+ else:
881
+ align = 'va'
882
+ off = '(-25,0)'
883
+ else:
884
+ if right_labels:
885
+ align = 'east'
886
+ off = '(7,0)'
887
+ else:
888
+ align = 'west'
889
+ off = '(-7,0)'
890
+
891
+ if el_copy.get('alignment', None) is None:
892
+ el_copy.set('alignment', align)
893
+ if el_copy.get('offset', None) is None:
894
+ el_copy.set('offset', off)
895
+ el_copy.set("user-coords", "no")
896
+ el_copy.set("anchor", util.pt2str(p, spacer=","))
897
+ label.label(el_copy, diagram, parent, outline_status)
898
+
899
+
900
+ axes_object = None
901
+ def get_axes():
902
+ return axes_object
903
+
904
+ def axes(element, diagram, parent, outline_status):
905
+ if outline_status == "finish_outline":
906
+ return
907
+ global axes_object
908
+ axes_object = Axes(element, diagram, parent)
909
+