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/circle.py CHANGED
@@ -47,9 +47,12 @@ def circle(element, diagram, parent, outline_status):
47
47
  if element.get('stroke') is not None:
48
48
  element.set('stroke', 'black')
49
49
  if element.get('fill') is not None:
50
- element.set('fill', 'lightgray')
50
+ if element.get('fill').strip().lower() != 'none':
51
+ element.set('fill', 'lightgray')
52
+ else:
53
+ element.set('fill', 'none')
51
54
  else:
52
- element.set('stroke', element.get('stroke', 'none'))
55
+ element.set('stroke', element.get('stroke', 'black'))
53
56
  element.set('fill', element.get('fill', 'none'))
54
57
  element.set('thickness', element.get('thickness', '2'))
55
58
  util.add_attr(circle, util.get_2d_attr(element))
@@ -120,7 +123,10 @@ def ellipse(element, diagram, parent, outline_status):
120
123
  if element.get('stroke') is not None:
121
124
  element.set('stroke', 'black')
122
125
  if element.get('fill') is not None:
123
- element.set('fill', 'lightgray')
126
+ if elementl.get('fill').strip().lower() != 'none':
127
+ element.set('fill', 'lightgray')
128
+ else:
129
+ element.set('fill', 'none')
124
130
  else:
125
131
  element.set('stroke', element.get('stroke', 'none'))
126
132
  element.set('fill', element.get('fill', 'none'))
@@ -283,7 +289,10 @@ def angle(element, diagram, parent, outline_status):
283
289
  element.set('stroke', element.get('stroke', 'black'))
284
290
  if diagram.output_format() == 'tactile':
285
291
  if element.get('fill') is not None:
286
- element.set('fill', 'lightgray')
292
+ if element.get('fill').strip().lower() != 'none':
293
+ element.set('fill', 'lightgray')
294
+ else:
295
+ element.set('fill', 'none')
287
296
  else:
288
297
  element.set('fill', element.get('fill', 'none'))
289
298
  element.set('thickness', element.get('thickness','2'))
prefig/core/clip.py CHANGED
@@ -18,7 +18,7 @@ def clip(element, diagram, parent, outline_status):
18
18
 
19
19
  clip = ET.Element('clipPath')
20
20
  clip.append(shape)
21
- clip_id = 'clip-'+shape_ref
21
+ clip_id = shape_ref + '-clip'
22
22
  clip.set('id', clip_id)
23
23
 
24
24
  diagram.add_reusable(clip)
@@ -27,6 +27,7 @@ def coordinates(element, diagram, root, outline_status):
27
27
  dest_dy *= -1
28
28
 
29
29
  bbox = un.valid_eval(element.get('bbox'))
30
+
30
31
  if element.get('aspect-ratio', None) is not None:
31
32
  ratio = un.valid_eval(element.get('aspect-ratio'))
32
33
  if element.get('preserve-y-range', 'no') == 'yes':
@@ -58,15 +59,41 @@ def coordinates(element, diagram, root, outline_status):
58
59
  clip_box.set('height', util.float2str(height))
59
60
  diagram.push_clippath(clippath)
60
61
 
62
+ scales = element.get('scales', 'linear')
63
+ if scales == 'linear':
64
+ scales = ['linear', 'linear']
65
+ elif scales == 'semilogx':
66
+ scales = ['log', 'linear']
67
+ elif scales == 'semilogy':
68
+ scales = ['linear', 'log']
69
+ elif scales == 'loglog':
70
+ scales = ['log', 'log']
71
+ else:
72
+ scales = ['linear', 'linear']
73
+
61
74
  ctm = ctm.copy()
75
+ diagram.push_scales(scales)
76
+ scaled_bbox = bbox.copy()
77
+ if scales[0] == 'log':
78
+ scaled_bbox[0] = np.log10(scaled_bbox[0])
79
+ scaled_bbox[2] = np.log10(scaled_bbox[2])
80
+ ctm.set_log_x()
81
+ if scales[1] == 'log':
82
+ scaled_bbox[1] = np.log10(scaled_bbox[1])
83
+ scaled_bbox[3] = np.log10(scaled_bbox[3])
84
+ ctm.set_log_y()
85
+
62
86
  ctm.translate(destination[0], destination[1])
63
- ctm.scale( (destination[2]-destination[0])/float(bbox[2]-bbox[0]),
64
- (destination[3]-destination[1])/float(bbox[3]-bbox[1]) )
65
- ctm.translate(-bbox[0], -bbox[1])
66
- bbox_str = '['+','.join([str(b) for b in bbox])+']'
87
+ ctm.scale( (destination[2]-destination[0]) /
88
+ float(scaled_bbox[2]-scaled_bbox[0]),
89
+ (destination[3]-destination[1]) /
90
+ float(scaled_bbox[3]-scaled_bbox[1]) )
91
+ ctm.translate(-scaled_bbox[0], -scaled_bbox[1])
92
+ bbox_str = '['+','.join([str(b) for b in scaled_bbox])+']'
67
93
  un.valid_eval(bbox_str, 'bbox')
68
94
 
69
95
  diagram.push_ctm([ctm, bbox])
70
96
  diagram.parse(element, root, outline_status)
71
97
  diagram.pop_ctm()
72
98
  diagram.pop_clippath()
99
+ diagram.pop_scales()
prefig/core/diagram.py CHANGED
@@ -3,14 +3,21 @@ import lxml.etree as ET
3
3
  import numpy as np
4
4
  import logging
5
5
  import copy
6
+ import re
6
7
  from . import tags
7
8
  from . import user_namespace as un
8
9
  from . import utilities as util
9
10
  from . import CTM
10
11
  from . import label
12
+ from . import math_utilities as math_util
13
+ from . import annotations
14
+ from . import repeat
11
15
 
12
16
  log = logging.getLogger('prefigure')
13
17
 
18
+ # regular expression to check if id is EPUB compliant
19
+ epub_id_check = re.compile('^[A-Za-z0-9_-]+$')
20
+
14
21
  class Diagram:
15
22
  def __init__(self,
16
23
  diagram_element,
@@ -30,6 +37,18 @@ class Diagram:
30
37
  self.environment = environment
31
38
  self.caption = ""
32
39
 
40
+ self.add_default_annotations = True
41
+ if (self.environment == 'pyodide' and
42
+ len(self.diagram_element.xpath('.//annotations')) == 0
43
+ ):
44
+ diagram_annotations = annotations.diagram_to_speech(diagram_element)
45
+ annotations_tree = ET.SubElement(self.diagram_element,
46
+ 'annotations')
47
+ annotations_tree.append(diagram_annotations)
48
+ self.add_default_annotations = False
49
+
50
+ math_util.set_diagram(self)
51
+
33
52
  label.init(self.format, self.environment)
34
53
 
35
54
  # create the XML tree for the svg output
@@ -41,7 +60,7 @@ class Diagram:
41
60
  self.root = ET.Element("svg", nsmap = nsmap)
42
61
 
43
62
  self.id_suffix = ['']
44
- self.add_id(self.root, diagram_element.get('id', 'diagram'))
63
+ self.add_id(self.root, diagram_element.get('id', 'pf__figure'))
45
64
 
46
65
  # prepare the XML tree for annotations, if there are any
47
66
  self.annotations_root = None
@@ -60,6 +79,9 @@ class Diagram:
60
79
  # a dictionary for holding shapes
61
80
  self.shape_dict = {}
62
81
 
82
+ # dictionary for saving graphical data
83
+ self.saved_data = {}
84
+
63
85
  # each SVG element will have an id, we'll store a count of ids here
64
86
  self.ids = {}
65
87
 
@@ -72,6 +94,9 @@ class Diagram:
72
94
  # stack for managing bounding boxes and clipping
73
95
  self.clippaths = []
74
96
 
97
+ # stack for managing scales of coordinate systems
98
+ self.scale_stack = []
99
+
75
100
  # list for legends
76
101
  self.legends = []
77
102
 
@@ -86,6 +111,9 @@ class Diagram:
86
111
  if publication is not None:
87
112
  for subelement in publication:
88
113
  if subelement.tag == 'external-root':
114
+ log.warning('<external-root> in publication file is deprecated')
115
+ log.warning('Use <directories> instead')
116
+ log.warning('See "Working with data" in the PreFigure documentation"')
89
117
  external = subelement.get('name', None)
90
118
  if external is not None:
91
119
  self.external = external
@@ -93,10 +121,42 @@ class Diagram:
93
121
  log.warning('<external-root> in publication file needs a @name')
94
122
  continue
95
123
  self.defaults[subelement.tag] = subelement
124
+ directories = publication.xpath('.//directories')
125
+ if len(directories) > 0:
126
+ directories = directories[0]
127
+ data_directory = directories.get('data', None)
128
+ if data_directory is not None:
129
+ self.external = data_directory
130
+
131
+ templates = self.diagram_element.xpath('.//templates')
132
+ if len(templates) > 0:
133
+ templates_element = templates[0]
134
+ for template in templates:
135
+ templates_parent = template.getparent()
136
+ templates_parent.remove(template)
137
+ for child in templates_element:
138
+ self.defaults[child.tag] = child
139
+
140
+ author_annotations = self.diagram_element.xpath('.//annotations')
141
+ self.author_annotations_present = len(author_annotations) > 0
142
+ if self.author_annotations_present:
143
+ self.check_annotation_ref(author_annotations[0])
96
144
 
97
145
  if self.defaults.get('macros', None) is not None:
98
146
  label.add_macros(self.defaults.get('macros').text)
99
147
 
148
+ def check_annotation_ref(self, element):
149
+ ref = element.get('ref', None)
150
+ if ref is not None:
151
+ if not bool(epub_id_check.fullmatch(ref)):
152
+ log.error(f"@ref {ref} in an annotation has characters disallowed by EPUB")
153
+ log.error(" We will replace these characters but there may be unexpected behavior")
154
+ log.error(" Search for EPUB in the PreFigure documentation https://prefigure.org")
155
+ ref = repeat.epub_clean(ref)
156
+ element.set('ref', ref)
157
+ for child in element:
158
+ self.check_annotation_ref(child)
159
+
100
160
  def add_legend(self, legend):
101
161
  self.legends.append(legend)
102
162
 
@@ -135,9 +195,12 @@ class Diagram:
135
195
  suffix = ''.join(self.id_suffix)
136
196
  if id is None:
137
197
  self.ids[element.tag] = self.ids.get(element.tag, -1) + 1
138
- return element.tag+'-'+str(self.ids[element.tag])+suffix
198
+ result_id = element.tag+'-'+str(self.ids[element.tag])+suffix
139
199
  else:
140
- return id + suffix
200
+ result_id = id + suffix
201
+ if result_id.startswith('pf__'):
202
+ return result_id
203
+ return 'pf__' + result_id
141
204
 
142
205
  def append_id_suffix(self, element):
143
206
  return self.find_id(element, element.get('id', None))
@@ -148,6 +211,9 @@ class Diagram:
148
211
  def set_output_format(self, format):
149
212
  self.format = format
150
213
 
214
+ def get_environment(self):
215
+ return self.environment
216
+
151
217
  # get the HTML tree so that we can add text for labels
152
218
  def label_html(self):
153
219
  return self.label_html_body
@@ -175,6 +241,12 @@ class Diagram:
175
241
  log.error(f"Unable to apply inverse coordinate transform to {p}")
176
242
  return np.array([0,0])
177
243
 
244
+ def save_data(self, element, data):
245
+ self.saved_data[element] = data
246
+
247
+ def retrieve_data(self, element):
248
+ return self.saved_data.get(element, None)
249
+
178
250
  def begin_figure(self):
179
251
  # set up the dimensions of the diagram in SVG coordinates
180
252
  dims = self.diagram_element.get('dimensions')
@@ -238,6 +310,7 @@ class Diagram:
238
310
  h = height + margins[1]+margins[3]
239
311
  self.root.set("width", str(w))
240
312
  self.root.set("height", str(h))
313
+ self.root.set("viewBox", f"0 0 {w} {h}")
241
314
 
242
315
  # initialize the CTM and push it onto the CTM stack
243
316
  ctm.translate(0, height + margins[1] + margins[3])
@@ -247,6 +320,7 @@ class Diagram:
247
320
  bbox = [0,0,width,height]
248
321
  un.enter_namespace('bbox', bbox)
249
322
  self.ctm_stack = [[ctm, bbox]]
323
+ self.scale_stack = [['linear', 'linear']]
250
324
 
251
325
  # initialize the SVG element 'defs' and add the clipping path
252
326
  self.defs = ET.SubElement(self.root, 'defs')
@@ -269,6 +343,15 @@ class Diagram:
269
343
  def pop_clippath(self):
270
344
  self.clippaths.pop(-1)
271
345
 
346
+ def push_scales(self, scales):
347
+ self.scale_stack.append(scales)
348
+
349
+ def pop_scales(self):
350
+ self.scale_stack.pop(-1)
351
+
352
+ def get_scales(self):
353
+ return self.scale_stack[-1]
354
+
272
355
  def get_clippath(self):
273
356
  return self.clippaths[-1]
274
357
 
@@ -332,6 +415,18 @@ class Diagram:
332
415
  log.error(f"Unable to write SVG at {out+'.svg'}")
333
416
  return
334
417
 
418
+ if self.author_annotations_present and self.environment == "pretext":
419
+ # we will write out a second version of the diagram
420
+ # without the height and width attributes for diagcess use
421
+ self.root.attrib.pop("height")
422
+ self.root.attrib.pop("width")
423
+ try:
424
+ with ET.xmlfile(out + '-diagcess.svg', encoding='utf-8') as xf:
425
+ xf.write(self.root, pretty_print=True)
426
+ except:
427
+ log.error(f"Unable to write SVG at {out+'-diagcess.svg'}")
428
+ return
429
+
335
430
  if self.annotations_root is not None:
336
431
  diagram = ET.Element('diagram')
337
432
  diagram.append(self.annotations_root)
@@ -388,6 +483,15 @@ class Diagram:
388
483
  # we're publicly using 'at' rather than 'id' for handles
389
484
  if child.get('at') is not None:
390
485
  child.set('id', child.get('at'))
486
+
487
+ child_id = child.get('id', None)
488
+ if child_id is not None:
489
+ if not bool(epub_id_check.fullmatch(child_id)):
490
+ log.error(f"The id {child_id} has characters disallowed by EPUB")
491
+ log.error(" We will substitute disallowed characters to make the id EPUB compliant")
492
+ log.error(" Search for EPUB in the PreFigure documentation at https://prefigure.org")
493
+ child.set('id', repeat.epub_clean(child_id))
494
+
391
495
  # see if the publication flie has any defaults
392
496
  defaults = self.defaults.get(child.tag, None)
393
497
  if defaults is not None:
@@ -459,6 +563,13 @@ class Diagram:
459
563
  def get_root(self):
460
564
  return self.root
461
565
 
566
+ def apply_defaults(self, tag, element):
567
+ default = self.defaults.get(tag, None)
568
+ if default is not None:
569
+ for attr, value in default.attrib.items():
570
+ if element.get(attr, None) is None:
571
+ element.set(attr, value)
572
+
462
573
  # when a graphical component is outlined, we first add the component's path
463
574
  # to <defs> so that it can be reused, then we stroke it with a thick white
464
575
  def add_outline(self, element, path, parent, outline_width = None):
@@ -518,7 +629,11 @@ class Diagram:
518
629
  # We have to clean up the arrow heads. The references to the
519
630
  # arrow heads are in the reusable so we'll retrieve them and
520
631
  # and include them with the use element.
521
- reuse_handle = element.get('id')+self.id_suffix[-1]+'-outline'
632
+ element_id = element.get('id')
633
+ if element_id.endswith(self.id_suffix[-1]):
634
+ reuse_handle = element_id + '-outline'
635
+ else:
636
+ reuse_handle = element.get('id')+self.id_suffix[-1]+'-outline'
522
637
  reusable = self.get_reusable(reuse_handle)
523
638
  use.set('href', r'#' + reuse_handle)
524
639
  for marker in ['marker-start', 'marker-end', 'marker-mid']:
@@ -534,6 +649,8 @@ class Diagram:
534
649
  self.annotations_root = ET.Element('annotations')
535
650
 
536
651
  def add_default_annotation(self, annotation):
652
+ if not self.add_default_annotations:
653
+ return
537
654
  self.default_annotations.append(annotation)
538
655
 
539
656
  def get_default_annotations(self):
@@ -557,10 +674,16 @@ class Diagram:
557
674
 
558
675
  def add_annotation_to_branch(self, annotation):
559
676
  if len(self.annotation_branch_stack) == 0:
560
- self.annotation_branches[annotation.get('id')] = annotation
677
+ id = annotation.get('id')
678
+ if not id.startswith('pf__'):
679
+ id = 'pf__' + id
680
+ self.annotation_branches[id] = annotation
561
681
  return
562
682
  self.annotation_branch_stack[-1].append(annotation)
563
- annotation.set('id', self.append_id_suffix(annotation))
683
+ id = self.append_id_suffix(annotation)
684
+ if not id.startswith('pf__'):
685
+ id = 'pf__' + id
686
+ annotation.set('id', id)
564
687
 
565
688
  def get_annotation_branch(self, id):
566
689
  return self.annotation_branches.pop(id, None)
prefig/core/graph.py CHANGED
@@ -1,7 +1,11 @@
1
1
  import lxml.etree as ET
2
2
  import logging
3
+ import math
4
+ import numpy as np
5
+ from . import math_utilities as math_util
3
6
  from . import user_namespace as un
4
7
  from . import utilities as util
8
+ from . import arrow
5
9
 
6
10
  log = logging.getLogger('prefigure')
7
11
 
@@ -14,13 +18,35 @@ def graph(element, diagram, parent, outline_status = None):
14
18
  finish_outline(element, diagram, parent)
15
19
  return
16
20
 
21
+ polar = element.get('coordinates', 'cartesian') == 'polar'
17
22
  # by default, the domain is the width of the bounding box
18
23
  bbox = diagram.bbox()
19
24
  domain = element.get('domain')
20
25
  if domain is None:
21
- domain = [bbox[0], bbox[2]]
26
+ if polar:
27
+ domain = [0, 2*math.pi]
28
+ else:
29
+ domain = [bbox[0], bbox[2]]
22
30
  else:
23
31
  domain = un.valid_eval(domain)
32
+ if domain[0] == -np.inf:
33
+ domain[0] = bbox[0]
34
+ if domain[1] == np.inf:
35
+ domain[1] = bbox[2]
36
+
37
+ # if there are arrows, we need to pull the domain in by two pixels
38
+ # so that the arrows don't go outside the domain
39
+ arrows = int(element.get('arrows', '0'))
40
+ if arrows > 0 and not polar:
41
+ end = diagram.transform((domain[1], 0))
42
+ end[0] -= 2
43
+ new_domain = diagram.inverse_transform(end)
44
+ domain[1] = new_domain[0]
45
+ if arrows == 2 and not polar:
46
+ begin = diagram.transform((domain[0],0))
47
+ begin[0] += 2
48
+ new_domain = diagram.inverse_transform(begin)
49
+ domain[0] = new_domain[0]
24
50
 
25
51
  # retrieve the function from the namespace and generate points
26
52
  try:
@@ -29,28 +55,11 @@ def graph(element, diagram, parent, outline_status = None):
29
55
  log.error(f"Error retrieving function in graph: {str(e)}")
30
56
  return
31
57
 
32
- N = int(element.get('N', 100))
33
- dx = (domain[1] - domain[0])/N
34
- x = domain[0]
35
- cmds = []
36
- next_cmd = 'M'
37
- upper = 2*bbox[3] - bbox[1]
38
- lower = 2*bbox[1] - bbox[3]
39
- for _ in range(N+1):
40
- try:
41
- y = f(x)
42
- except:
43
- next_cmd = 'M'
44
- x += dx
45
- continue
46
- if y > upper or y < lower:
47
- next_cmd = 'M'
48
- x += dx
49
- continue
50
- p = diagram.transform((x, f(x)))
51
- cmds += [next_cmd, util.pt2str(p)]
52
- next_cmd = 'L'
53
- x += dx
58
+ N = int(element.get('N', '100'))
59
+ if polar:
60
+ cmds = polar_path(element, diagram, f, domain, N)
61
+ else:
62
+ cmds = cartesian_path(element, diagram, f, domain, N)
54
63
 
55
64
  # now set up the attributes
56
65
  util.set_attr(element, 'thickness', '2')
@@ -66,9 +75,33 @@ def graph(element, diagram, parent, outline_status = None):
66
75
  'fill': 'none'
67
76
  }
68
77
  )
78
+ if polar and element.get('fill', None) is not None:
79
+ attrib['fill'] = element.get('fill')
69
80
 
70
81
  path = ET.Element('path', attrib = attrib)
71
82
 
83
+ arrows = int(element.get('arrows', '0'))
84
+ forward = 'marker-end'
85
+ backward = 'marker-start'
86
+ if element.get('reverse', 'no') == 'yes':
87
+ forward, backward = backward, forward
88
+ if arrows > 0:
89
+ arrow.add_arrowhead_to_path(
90
+ diagram,
91
+ forward,
92
+ path,
93
+ arrow_width=element.get('arrow-width', None),
94
+ arrow_angles=element.get('arrow-angles', None)
95
+ )
96
+ if arrows > 1:
97
+ arrow.add_arrowhead_to_path(
98
+ diagram,
99
+ backward,
100
+ path,
101
+ arrow_width=element.get('arrow-width', None),
102
+ arrow_angles=element.get('arrow-angles', None)
103
+ )
104
+
72
105
  # By default, we clip the graph to the bounding box
73
106
  if element.get('cliptobbox') is None:
74
107
  element.set('cliptobbox', 'yes')
@@ -92,4 +125,184 @@ def finish_outline(element, diagram, parent):
92
125
  element.get('fill', 'none'),
93
126
  parent)
94
127
 
128
+ def cartesian_path(element, diagram, f, domain, N):
129
+ # Sometimes we encounter a divide by zero when building a graph
130
+ # These are safe to ignore since they return an nan
131
+ np.seterr(divide="ignore")
132
+
133
+ # The graphing routine is relatively straightforward.
134
+ # We just walk across the horizontal axis and connect points with lines
135
+ # We try to detect if the function is not defined or if we have passed
136
+ # a vertical asymptote. One complication is that we try to get close
137
+ # to the singularity or vertical asymptote by subdividing the
138
+ # interval based on the last good point we've seen
139
+ #
140
+ # In the vertical direction, we imagine a buffer (lower, upper)
141
+ # where upper - lower = 3*height and with the viewing box centered inside.
142
+ # We plot anything in the buffer.
143
+ #
144
+ # We maintain a history of sorts using next_cmd, which is either 'M' or 'L'
145
+ # and last_visible, which tells us whether the last point plotted is
146
+ # in the viewing window
147
+
148
+ scales = diagram.get_scales()
149
+ if scales[0] == 'log':
150
+ x_positions = np.logspace(np.log10(domain[0]),
151
+ np.log10(domain[1]),
152
+ N+1)
153
+ else:
154
+ x_positions = np.linspace(domain[0], domain[1], N+1)
155
+
156
+ bbox = diagram.bbox()
157
+ dx = (domain[1] - domain[0])/N
158
+ x = domain[0]
159
+ cmds = []
160
+ next_cmd = 'M'
161
+ if scales[1] == 'log':
162
+ bottom = np.log10(bbox[1])
163
+ top = np.log10(bbox[3])
164
+ lower = 10**(bottom - 3)
165
+ upper = 10**(top + 3)
166
+ else:
167
+ height = (bbox[3] - bbox[1])
168
+ upper = bbox[3] + height
169
+ lower = bbox[1] - height
170
+ last_visible = False
171
+ for i, x in enumerate(x_positions):
172
+ if i > 0:
173
+ dx = x - x_positions[i-1]
174
+ else:
175
+ dx = 0
176
+ try:
177
+ y = f(x)
178
+ except:
179
+ if last_visible:
180
+ # we plotted the last point so let's find the singularity,
181
+ # which is in the interval (x-dx, x). We subdivide 8 times
182
+ # keeping the last valid value in last_good_x
183
+ ddx = dx/2
184
+ xx = x - ddx
185
+ last_good_x = x - dx
186
+ for _ in range(8):
187
+ ddx /= 2
188
+ try:
189
+ y = f(xx)
190
+ except:
191
+ xx -= ddx
192
+ continue
193
+ last_good_x = xx
194
+ xx += ddx
195
+ p = diagram.transform((last_good_x, f(last_good_x)))
196
+ cmds += ['L', util.pt2str(p)]
197
+
198
+ last_visible = False
199
+ next_cmd = 'M'
200
+ x += dx
201
+ continue
202
+ if y > upper or y < lower:
203
+ if last_visible:
204
+ # the last point was visible so this could be a vertical
205
+ # asymptote. We will subdivide until we're in the plotting
206
+ # range
207
+ ddx = dx/2
208
+ xx = x - ddx
209
+ last_good_x = x - dx
210
+ for _ in range(8):
211
+ ddx /= 2
212
+ yy = f(xx)
213
+ if yy > upper or yy < lower:
214
+ xx -= ddx
215
+ else:
216
+ last_good_x = xx
217
+ xx += ddx
218
+ p = diagram.transform((last_good_x, f(last_good_x)))
219
+ cmds += ['L', util.pt2str(p)]
220
+
221
+ last_visible = False
222
+ next_cmd = 'M'
223
+ x += dx
224
+ continue
225
+ if next_cmd == 'M' and x > domain[0]:
226
+ # let's see if we need to back up a bit to find the asymptote
227
+ # or edge of the domain
228
+ ddx = dx/2
229
+ xx = x - ddx
230
+ last_good_x = x
231
+ for _ in range(8):
232
+ ddx /= 2
233
+ try:
234
+ yy = f(xx)
235
+ except:
236
+ xx += ddx
237
+ continue
238
+ if yy > upper or yy < lower:
239
+ xx += ddx
240
+ continue
241
+ last_good_x = xx
242
+ xx -= ddx
243
+
244
+ if last_good_x < x:
245
+ p = diagram.transform((last_good_x, f(last_good_x)))
246
+ cmds += ['M', util.pt2str(p)]
247
+ next_cmd = 'L'
248
+
249
+ p = diagram.transform((x, y))
250
+ cmds += [next_cmd, util.pt2str(p)]
251
+ next_cmd = 'L'
252
+ x += dx
253
+ if y < bbox[3] and y > bbox[1]:
254
+ last_visible = True
255
+ else:
256
+ last_visible = False
257
+
258
+ return cmds
259
+
260
+ def log_path(element, diagram, f, domain, N):
261
+ log_y = diagram.get_scales()[1] == 'log'
262
+ x0 = np.log10(domain[0])
263
+ x1 = np.log10(domain[1])
264
+ x_values = np.logspace(x0, x1, N+1)
265
+ cmds = []
266
+ next_cmd = 'M'
267
+ for x in x_values:
268
+ y = f(x)
269
+ if y < 0 and log_y:
270
+ next_cmd = 'M'
271
+ continue
272
+ p = diagram.transform((x, f(x)))
273
+ cmds += [next_cmd, util.pt2str(p)]
274
+ next_cmd = 'L'
275
+ return cmds
276
+
277
+ def polar_path(element, diagram, f, domain, N):
278
+ bbox = diagram.bbox()
279
+ center = math_util.midpoint(bbox[:2], bbox[2:])
280
+ R = math_util.distance(center, bbox[2:])
281
+
282
+ if element.get('domain-degrees', 'no') == 'yes':
283
+ domain = [math.radians(d) for d in domain]
284
+ t = domain[0]
285
+ dt = (domain[1] - domain[0])/N
286
+ polar_cmds = []
287
+ next_cmd = 'M'
288
+ for _ in range(N+1):
289
+ try:
290
+ r = f(t)
291
+ except:
292
+ next_cmd = 'M'
293
+ t += dt
294
+ continue
295
+
296
+ p = (r*math.cos(t), r*math.sin(t))
297
+ if math_util.distance(p, center) > 2*R:
298
+ next_cmd = 'M'
299
+ t += dt
300
+ continue
301
+ polar_cmds.append(next_cmd)
302
+ polar_cmds.append(util.pt2str(diagram.transform(p)))
303
+ next_cmd = 'L'
304
+ t += dt
305
+ if element.get('closed', 'no') == 'yes':
306
+ polar_cmds.append('Z')
95
307
 
308
+ return polar_cmds