kaxe 1.4.3__tar.gz → 1.4.4.dev3__tar.gz

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 (130) hide show
  1. {kaxe-1.4.3/src/kaxe.egg-info → kaxe-1.4.4.dev3}/PKG-INFO +1 -1
  2. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/pyproject.toml +1 -1
  3. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/legend.py +23 -8
  4. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/shapes.py +3 -1
  5. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/svg.py +56 -0
  6. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/window.py +1 -0
  7. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/d3/plot3d.py +2 -0
  8. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/grid.py +180 -62
  9. {kaxe-1.4.3 → kaxe-1.4.4.dev3/src/kaxe.egg-info}/PKG-INFO +1 -1
  10. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/LICENSE +0 -0
  11. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/MANIFEST.in +0 -0
  12. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/README.md +0 -0
  13. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/setup.cfg +0 -0
  14. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/__init__.py +0 -0
  15. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/chart/__init__.py +0 -0
  16. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/chart/bar.py +0 -0
  17. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/chart/box.py +0 -0
  18. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/chart/pie.py +0 -0
  19. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/chart/qqplot.py +0 -0
  20. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/__init__.py +0 -0
  21. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/axis.py +0 -0
  22. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/color.py +0 -0
  23. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/backend.py +0 -0
  24. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/camera.py +0 -0
  25. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/helper.py +0 -0
  26. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/hud.py +0 -0
  27. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/__init__.py +0 -0
  28. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/color.py +0 -0
  29. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/line.py +0 -0
  30. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/point.py +0 -0
  31. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/pointer.py +0 -0
  32. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/objects/triangle.py +0 -0
  33. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/openglrender.py +0 -0
  34. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/d3/translator.py +0 -0
  35. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/draw.py +0 -0
  36. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/fileloader.py +0 -0
  37. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/helper.py +0 -0
  38. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/line.py +0 -0
  39. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/marker.py +0 -0
  40. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/profiler.py +0 -0
  41. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/round.py +0 -0
  42. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/styles.py +0 -0
  43. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/symbol.py +0 -0
  44. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/core/text.py +0 -0
  45. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/data/__init__.py +0 -0
  46. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/data/excel.py +0 -0
  47. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/__init__.py +0 -0
  48. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/_lazy.py +0 -0
  49. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/__init__.py +0 -0
  50. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/arrow.py +0 -0
  51. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/bubble.py +0 -0
  52. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/contour.py +0 -0
  53. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/equation.py +0 -0
  54. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/fill.py +0 -0
  55. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/function.py +0 -0
  56. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/map.py +0 -0
  57. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/parameter.py +0 -0
  58. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/pillar.py +0 -0
  59. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d2/point.py +0 -0
  60. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/__init__.py +0 -0
  61. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/base.py +0 -0
  62. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/function.py +0 -0
  63. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/mesh.py +0 -0
  64. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/point.py +0 -0
  65. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/d3/potato.py +0 -0
  66. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/function.py +0 -0
  67. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/legend.py +0 -0
  68. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/mapdata.py +0 -0
  69. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/point.py +0 -0
  70. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/objects/text.py +0 -0
  71. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/__init__.py +0 -0
  72. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/_lazy.py +0 -0
  73. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/box.py +0 -0
  74. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/constants.py +0 -0
  75. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/d3/__init__.py +0 -0
  76. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/d3/axes.py +0 -0
  77. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/d3/geometry.py +0 -0
  78. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/d3/variants.py +0 -0
  79. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/double.py +0 -0
  80. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/empty.py +0 -0
  81. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/log.py +0 -0
  82. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/polar.py +0 -0
  83. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/standard.py +0 -0
  84. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/themes.py +0 -0
  85. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/zoom.py +0 -0
  86. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/plot/zoom_connector.py +0 -0
  87. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/__init__.py +0 -0
  88. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/__init__.py +0 -0
  89. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.bright-oblique.ttf +0 -0
  90. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.bright-roman.ttf +0 -0
  91. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.bright-semibold.ttf +0 -0
  92. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.bright-semiboldoblique.ttf +0 -0
  93. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.classical-serif-italic.ttf +0 -0
  94. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.concrete-bold.ttf +0 -0
  95. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.concrete-bolditalic.ttf +0 -0
  96. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.concrete-italic.ttf +0 -0
  97. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.concrete-roman.ttf +0 -0
  98. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-bold.ttf +0 -0
  99. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-boldoblique.ttf +0 -0
  100. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-demi-condensed-demicondensed.ttf +0 -0
  101. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-medium.ttf +0 -0
  102. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-oblique.ttf +0 -0
  103. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-bold.ttf +0 -0
  104. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-bolditalic.ttf +0 -0
  105. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-boldslanted.ttf +0 -0
  106. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-romanslanted.ttf +0 -0
  107. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-italic.ttf +0 -0
  108. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-roman.ttf +0 -0
  109. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.serif-upright-italic-uprightitalic.ttf +0 -0
  110. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bold.ttf +0 -0
  111. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bolditalic.ttf +0 -0
  112. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-italic.ttf +0 -0
  113. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-light.ttf +0 -0
  114. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-lightoblique.ttf +0 -0
  115. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-regular.ttf +0 -0
  116. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-italic.ttf +0 -0
  117. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-medium.ttf +0 -0
  118. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/computer-modern-family/readme.txt +0 -0
  119. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/logo-small.png +0 -0
  120. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/symbolcross.png +0 -0
  121. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/symboldonut.png +0 -0
  122. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/symbollollipop.png +0 -0
  123. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe/resources/symboltriangle.png +0 -0
  124. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe.egg-info/SOURCES.txt +0 -0
  125. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe.egg-info/dependency_links.txt +0 -0
  126. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe.egg-info/requires.txt +0 -0
  127. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/src/kaxe.egg-info/top_level.txt +0 -0
  128. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/tests/test.py +0 -0
  129. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/tests/test2.py +0 -0
  130. {kaxe-1.4.3 → kaxe-1.4.4.dev3}/tests/test3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaxe
3
- Version: 1.4.3
3
+ Version: 1.4.4.dev3
4
4
  Summary: A small graphing tool for functions, points, equations and more
5
5
  Author-email: Valter Yde Daugberg <valteryde@hotmail.com>
6
6
  Project-URL: Homepage, https://github.com/valteryde/kaxe
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kaxe"
3
- version = "1.4.3"
3
+ version = "1.4.4.dev3"
4
4
  authors = [
5
5
  { name="Valter Yde Daugberg", email="valteryde@hotmail.com" },
6
6
  ]
@@ -3,6 +3,7 @@ from .symbol import makeSymbolShapes, symbol
3
3
  from .text import Text
4
4
  from .shapes import shapes
5
5
  from .styles import AttrObject, ComputedAttribute
6
+ from .svg import SvgDocument
6
7
  import math
7
8
  from types import MappingProxyType
8
9
  from PIL import Image
@@ -46,12 +47,12 @@ class LegendBox(AttrObject):
46
47
  self.objects = objects
47
48
 
48
49
 
49
- def finalize(self, parent, sneaky=False): # maybe add as a seperate object
50
+ def finalize(self, parent, sneaky=False, vector=False): # maybe add as a seperate object
50
51
  """
51
52
  stadigvæk lidt skrøbelig
52
53
 
53
54
  if sneaky is True the method will not do anything to parent
54
- The function will also just return an image of the legendbox
55
+ The function will also just return an image of the legendbox (or SvgDocument when vector=True)
55
56
 
56
57
  """
57
58
  self.setAttrMap(parent.attrmap)
@@ -90,6 +91,7 @@ class LegendBox(AttrObject):
90
91
  legendGridSpacing = self.getAttr('gaps')
91
92
  legendSymbolTextSpacing = self.getAttr('symbolTextSpacing')
92
93
  legendGap = self.getAttr('topMargin')
94
+ legendPadding = max(1, fontSize // 16)
93
95
 
94
96
  # legendSizeThickness = 2 #NOTE: STYLE
95
97
  # legendPadding = (5, 5, 5, 5) # NOTE: STYLE, left bottom right top
@@ -118,7 +120,7 @@ class LegendBox(AttrObject):
118
120
  grid.reverse()
119
121
 
120
122
  # create legends
121
- currentLinePos = [0, 0]
123
+ currentLinePos = [legendPadding, legendPadding]
122
124
  maxPos = [0, 0]
123
125
  minPos = [math.inf, math.inf]
124
126
  for row in grid:
@@ -167,8 +169,13 @@ class LegendBox(AttrObject):
167
169
 
168
170
  if debug: shapes.Circle(0,0, 10, batch=self.batch)
169
171
 
170
- height = (maxPos[1] - minPos[1])
171
- width = (maxPos[0] - minPos[0])
172
+ minPos[0] -= legendPadding
173
+ minPos[1] -= legendPadding
174
+ maxPos[0] += legendPadding
175
+ maxPos[1] += legendPadding
176
+
177
+ height = math.ceil(maxPos[1] - minPos[1])
178
+ width = math.ceil(maxPos[0] - minPos[0])
172
179
 
173
180
  ### skub den ned til window 0,0 (her burde title være med)
174
181
  # self.batch.push(-minPos[0], -minPos[1])
@@ -193,11 +200,19 @@ class LegendBox(AttrObject):
193
200
  # tilføj plads til legenden
194
201
  parent.addPaddingCondition(bottom=height+legendGap)
195
202
 
196
- else: # Only works with PILLOW
203
+ else:
197
204
  self.batch.push(-minPos[0], -minPos[1])
198
205
  self.boxshape.push(-minPos[0], -minPos[1])
206
+ if vector:
207
+ doc = SvgDocument((int(width), int(height)))
208
+ self.boxshape.draw(doc)
209
+ self.batch.draw(doc)
210
+ return doc
199
211
  surface = Image.new('RGBA', (int(width), int(height)), (0,0,0,0))
200
-
201
212
  self.boxshape.draw(surface)
202
213
  self.batch.draw(surface)
203
- return surface
214
+ return surface
215
+
216
+ def finalize_svg_sneaky(self, parent):
217
+ """Return an SvgDocument sized to the legend for grid SVG export."""
218
+ return self.finalize(parent, sneaky=True, vector=True)
@@ -276,7 +276,9 @@ class Circle(Shape):
276
276
  doubleradius = int(2*self.radius)
277
277
  circle = Image.new("RGBA", (doubleradius, doubleradius), (0, 0, 0, 0))
278
278
  draw = ImageDraw.Draw(circle)
279
- pos = (0, 0, 2 * self.radius, 2 * self.radius)
279
+ # PIL treats the bbox as inclusive on both ends; shrink by half a pixel so
280
+ # the rendered circle is symmetric and not clipped at the bottom edge.
281
+ pos = (-0.5, -0.5, 2 * self.radius - 0.5, 2 * self.radius - 0.5)
280
282
  if self.fill:
281
283
  draw.ellipse(pos, fill=self.color)
282
284
  else:
@@ -365,9 +365,65 @@ class SvgDocument:
365
365
  for el in self._elements:
366
366
  root.append(el)
367
367
  body = ET.tostring(root, encoding="unicode")
368
+ from fondi.backends.svg import _ascii_safe_svg_markup
369
+ body = _ascii_safe_svg_markup(body)
368
370
  return '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + body
369
371
 
370
372
 
373
+ def parse_svg_root(xml: str) -> ET.Element:
374
+ """Parse SVG XML and return the root element."""
375
+ if xml.startswith("<?"):
376
+ xml = xml.split("\n", 1)[1]
377
+ root = ET.fromstring(xml)
378
+ if not (root.tag.endswith("svg") or root.tag == "svg"):
379
+ raise ValueError("Expected SVG root element")
380
+ return root
381
+
382
+
383
+ def extract_svg_children(root: ET.Element) -> tuple[list[ET.Element], Optional[str]]:
384
+ """Return drawable children (excluding defs) and optional fondi style text."""
385
+ fondi_css: Optional[str] = None
386
+ children: list[ET.Element] = []
387
+ for child in root:
388
+ local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
389
+ if local == "defs":
390
+ for sub in child:
391
+ sub_local = sub.tag.split("}")[-1] if "}" in sub.tag else sub.tag
392
+ if sub_local == "style" and sub.text:
393
+ fondi_css = sub.text
394
+ continue
395
+ children.append(child)
396
+ return children, fondi_css
397
+
398
+
399
+ def embed_svg_children(
400
+ doc: SvgDocument,
401
+ children: list[ET.Element],
402
+ tx: float,
403
+ ty: float,
404
+ ) -> None:
405
+ """Embed copied SVG children at top-left offset (SVG y-down coordinates)."""
406
+ if not children:
407
+ return
408
+ group = ET.Element(
409
+ f"{{{SVG_NS}}}g",
410
+ {"transform": f"translate({round(tx, 3)},{round(ty, 3)})"},
411
+ )
412
+ for child in children:
413
+ group.append(copy.deepcopy(child))
414
+ doc._elements.append(group)
415
+
416
+
417
+ def merge_fondi_css(doc: SvgDocument, *style_texts: Optional[str]) -> None:
418
+ """Set fondi font CSS on doc once (skip duplicates and empty)."""
419
+ if doc._fondi_font_css is not None:
420
+ return
421
+ for text in style_texts:
422
+ if text:
423
+ doc._fondi_font_css = text
424
+ return
425
+
426
+
371
427
  def is_file_path(fname: Any) -> bool:
372
428
  """Return True when fname is a filesystem path (str or os.PathLike)."""
373
429
  return isinstance(fname, (str, os.PathLike))
@@ -69,6 +69,7 @@ class Window(AttrObject):
69
69
  """
70
70
 
71
71
  name = "Window"
72
+ supports_vector_export = True
72
73
 
73
74
  def __init__(self): # |
74
75
  """
@@ -27,6 +27,8 @@ from .axes import Plot3DAxesMixin
27
27
 
28
28
 
29
29
  class Plot3D(Plot3DAxesMixin, Window):
30
+ supports_vector_export = False
31
+
30
32
  """
31
33
  A 3D plotting window with full wireframe box and automatic axis positioning.
32
34
 
@@ -1,12 +1,28 @@
1
1
 
2
2
  from io import BytesIO
3
+ from typing import Optional, Union
3
4
  from ..core.styles import AttrObject, AttrMap
4
5
  from ..core.window import *
5
6
  from ..core.legend import LegendBox
6
- from typing import Union
7
+ from ..core.svg import (
8
+ SvgDocument,
9
+ infer_format,
10
+ is_file_path,
11
+ parse_svg_root,
12
+ extract_svg_children,
13
+ embed_svg_children,
14
+ merge_fondi_css,
15
+ )
7
16
  from PIL import Image
8
17
  from .constants import XYZPLOT
9
18
 
19
+
20
+ def _cell_supports_svg(plot) -> bool:
21
+ if plot == XYZPLOT or isinstance(plot, str):
22
+ return False
23
+ return getattr(plot, "supports_vector_export", False)
24
+
25
+
10
26
  class Grid(AttrObject):
11
27
  """
12
28
  Assemble multiple plots in one image.
@@ -19,6 +35,7 @@ class Grid(AttrObject):
19
35
  >>> grid.addColumn(plt3, plt4)
20
36
  >>> grid.show()
21
37
  >>> grid.save('fname.png')
38
+ >>> grid.save('fname.svg') # 2D vector; 3D cells embed as raster
22
39
 
23
40
  """
24
41
 
@@ -47,6 +64,7 @@ class Grid(AttrObject):
47
64
  self.padding = [0,0,0,0]
48
65
 
49
66
  self.__bakedImage__ = False
67
+ self.__bakedSvg__ = None
50
68
  self.laterDraws = []
51
69
 
52
70
 
@@ -76,39 +94,27 @@ class Grid(AttrObject):
76
94
  self.__legends = legends
77
95
 
78
96
 
79
-
80
- def __bake__(self):
81
- """
82
- only supports pillow images
83
-
84
- TODO: Clean up
85
- """
86
-
97
+ def _prepare_cells(self, vector: bool = False):
98
+ """Style cells, export to memory, and compute composite layout."""
87
99
  grid = self.grid
88
100
  gridSize = ((max([len(i) for i in grid])), len(grid))
89
101
  self.width, self.height = self.getAttr('width'), self.getAttr('height')
90
102
  self.outerPadding = self.getAttr('outerPadding')
91
103
 
92
- # styles values
93
- cellWidth, cellHeight = self.width//gridSize[0], self.height//gridSize[1]
104
+ cellWidth, cellHeight = self.width // gridSize[0], self.height // gridSize[1]
94
105
 
95
- # calculated values
96
106
  height = 0
97
107
  leftpadding = 0
98
108
  rightpadding = 0
99
109
  toppadding = 0
100
110
  bottompadding = 0
101
111
  gapcol = 0
102
-
103
- # add styles to window
104
- # and calculate sizes
105
112
  maxWidth = 0
113
+
106
114
  for row in grid:
107
-
108
115
  maxHeight = 0
109
-
116
+
110
117
  for colNum, plot in enumerate(row):
111
-
112
118
  plot.style(width=cellWidth, height=cellHeight)
113
119
  plot.style(outerPadding=self.outerPadding)
114
120
 
@@ -119,86 +125,177 @@ class Grid(AttrObject):
119
125
  if plot == XYZPLOT:
120
126
  plot.forceWidthHeight = True
121
127
 
122
- plot.save(memfile)
128
+ if vector and _cell_supports_svg(plot):
129
+ plot.save(memfile, format="svg")
130
+ else:
131
+ plot.save(memfile)
132
+
123
133
  plot.__ioBytes = memfile
134
+ memfile.seek(0)
124
135
 
125
136
  w, h = plot.getSize()
126
137
  maxWidth = max(w, maxWidth)
127
-
128
138
  maxHeight = max(h, maxHeight)
129
-
130
- # calculate paddings
139
+
131
140
  if colNum == 0:
132
141
  leftpadding = max(leftpadding, plot.padding[0])
133
142
  bottompadding = max(bottompadding, plot.padding[1])
134
-
135
- # calculate gaps
136
- else: # "låner"/genbruger lige else her
137
-
138
- # Da den næste ikke er lavet bruges den forrige
143
+ else:
139
144
  gapcol = max(gapcol, plot.padding[0] + row[colNum - 1].padding[2])
140
145
 
141
- if colNum == len(row)-1:
146
+ if colNum == len(row) - 1:
142
147
  rightpadding = max(rightpadding, plot.padding[2])
143
148
  toppadding = max(toppadding, plot.padding[3])
144
149
 
145
150
  height += maxHeight
146
-
151
+
147
152
  largetsRowNumber = max(len(i) for i in grid)
148
- width = gapcol * (largetsRowNumber - 1) + largetsRowNumber * cellWidth + leftpadding + rightpadding
153
+ width = (
154
+ gapcol * (largetsRowNumber - 1)
155
+ + largetsRowNumber * cellWidth
156
+ + leftpadding
157
+ + rightpadding
158
+ )
149
159
 
150
160
  size = (
151
161
  width + self.outerPadding[0] + self.outerPadding[2],
152
- height + self.outerPadding[1] + self.outerPadding[3] + toppadding
162
+ height + self.outerPadding[1] + self.outerPadding[3] + toppadding,
153
163
  )
154
-
155
- # Add legend
164
+
165
+ legend_image = None
166
+ legend_doc = None
167
+ legend_top_margin = 0
168
+
156
169
  if self.__legends:
157
170
  self.legendbox = LegendBox()
158
171
  for d in self.__legends:
159
172
  self.legendbox.add(*d)
160
173
 
161
- legendBoxImage = self.legendbox.finalize(self, sneaky=True)
162
-
163
- size = (
164
- size[0],
165
- size[1] + legendBoxImage.height + self.legendbox.getAttr('topMargin'),
166
- )
174
+ if vector:
175
+ legend_doc = self.legendbox.finalize_svg_sneaky(self)
176
+ legend_h = legend_doc.height
177
+ legend_top_margin = self.legendbox.getAttr('topMargin')
178
+ else:
179
+ legend_image = self.legendbox.finalize(self, sneaky=True)
180
+ legend_h = legend_image.height
181
+ legend_top_margin = self.legendbox.getAttr('topMargin')
182
+
183
+ size = (size[0], size[1] + legend_h + legend_top_margin)
184
+
185
+ return {
186
+ "grid": grid,
187
+ "cellWidth": cellWidth,
188
+ "gapcol": gapcol,
189
+ "size": size,
190
+ "leftpadding": leftpadding,
191
+ "toppadding": toppadding,
192
+ "legend_image": legend_image,
193
+ "legend_doc": legend_doc,
194
+ "legend_top_margin": legend_top_margin,
195
+ }
196
+
197
+
198
+ def _composite_png(self, layout: dict) -> Image.Image:
199
+ grid = layout["grid"]
200
+ size = layout["size"]
201
+ cellWidth = layout["cellWidth"]
202
+ gapcol = layout["gapcol"]
203
+ leftpadding = layout["leftpadding"]
204
+ toppadding = layout["toppadding"]
167
205
 
168
206
  image = Image.new('RGBA', size, self.getAttr('backgroundColor'))
169
207
 
170
- if self.__legends:
171
- image.alpha_composite(legendBoxImage, (image.width//2 - legendBoxImage.width//2, image.height - legendBoxImage.height - self.outerPadding[3]))
208
+ legend_image = layout["legend_image"]
209
+ if legend_image is not None:
210
+ image.alpha_composite(
211
+ legend_image,
212
+ (
213
+ image.width // 2 - legend_image.width // 2,
214
+ image.height - legend_image.height - self.outerPadding[3],
215
+ ),
216
+ )
172
217
 
173
- # TEGNER!
174
- # add plots to grid image
175
218
  y = toppadding + self.outerPadding[1]
176
219
 
177
220
  for row in grid:
178
-
179
221
  maxHeight = 0
180
222
  x = leftpadding + self.outerPadding[0]
181
223
 
182
224
  for plot in row:
183
- """
184
- Is a little ineffecient to write and then read from memory with png extenseion
185
- but here goes.
186
- """
187
-
188
- w, h = plot.getSize()
189
-
190
225
  img = Image.open(plot.__ioBytes)
226
+ plot.__ioBytes.seek(0)
191
227
  image.paste(img, (x - plot.padding[0], y - plot.padding[3]))
192
-
193
228
  x += cellWidth + gapcol
194
- maxHeight = max(maxHeight, h)
229
+ maxHeight = max(maxHeight, plot.getSize()[1])
195
230
 
196
231
  y += maxHeight
197
232
 
198
- self.__bakedImage__ = image
233
+ return image
234
+
235
+
236
+ def _composite_svg(self, layout: dict) -> str:
237
+ size = layout["size"]
238
+ grid = layout["grid"]
239
+ cellWidth = layout["cellWidth"]
240
+ gapcol = layout["gapcol"]
241
+ leftpadding = layout["leftpadding"]
242
+ toppadding = layout["toppadding"]
243
+
244
+ doc = SvgDocument(size)
245
+ bg = self.getAttr('backgroundColor')
246
+ if bg[3] != 0:
247
+ doc.add_rect(0, 0, size[0], size[1], bg)
248
+
249
+ legend_doc = layout["legend_doc"]
250
+ if legend_doc is not None:
251
+ lx = size[0] // 2 - legend_doc.width // 2
252
+ ly = size[1] - legend_doc.height - self.outerPadding[3]
253
+ embed_svg_children(doc, list(legend_doc._elements), lx, ly)
254
+ merge_fondi_css(doc, legend_doc._fondi_font_css)
255
+
256
+ y = toppadding + self.outerPadding[1]
257
+
258
+ for row in grid:
259
+ maxHeight = 0
260
+ x = leftpadding + self.outerPadding[0]
261
+
262
+ for plot in row:
263
+ px = x - plot.padding[0]
264
+ py = y - plot.padding[3]
265
+
266
+ if _cell_supports_svg(plot):
267
+ plot.__ioBytes.seek(0)
268
+ xml = plot.__ioBytes.read().decode("utf-8")
269
+ root = parse_svg_root(xml)
270
+ children, fondi_css = extract_svg_children(root)
271
+ embed_svg_children(doc, children, px, py)
272
+ merge_fondi_css(doc, fondi_css)
273
+ else:
274
+ plot.__ioBytes.seek(0)
275
+ img = Image.open(plot.__ioBytes)
276
+ doc.add_image(img, px, py, y_coord="top")
277
+
278
+ x += cellWidth + gapcol
279
+ maxHeight = max(maxHeight, plot.getSize()[1])
280
+
281
+ y += maxHeight
282
+
283
+ return doc.serialize()
199
284
 
285
+
286
+ def __bake__(self):
287
+ layout = self._prepare_cells(vector=False)
288
+ image = self._composite_png(layout)
289
+ self.__bakedImage__ = image
200
290
  return image
201
291
 
292
+
293
+ def __bake_svg__(self) -> str:
294
+ layout = self._prepare_cells(vector=True)
295
+ xml = self._composite_svg(layout)
296
+ self.__bakedSvg__ = xml
297
+ return xml
298
+
202
299
 
203
300
  def addRow(self, *row:list):
204
301
  """
@@ -232,18 +329,36 @@ class Grid(AttrObject):
232
329
  self.grid[i].append(plot)
233
330
 
234
331
 
235
- def save(self, fpath:str):
236
-
332
+ def save(self, fname: Union[str, BytesIO], format: Optional[str] = None):
333
+ fmt = infer_format(fname, format)
334
+
335
+ if fmt == "svg":
336
+ if self.__bakedSvg__ is not None:
337
+ xml = self.__bakedSvg__
338
+ else:
339
+ xml = self.__bake_svg__()
340
+
341
+ if fname is not None:
342
+ if is_file_path(fname):
343
+ with open(fname, 'w', encoding='utf-8') as f:
344
+ f.write(xml)
345
+ else:
346
+ fname.write(xml.encode('utf-8'))
347
+ return
348
+
237
349
  if self.__bakedImage__:
238
- self.__bakedImage__.save(fpath)
350
+ if is_file_path(fname):
351
+ self.__bakedImage__.save(fname)
352
+ else:
353
+ self.__bakedImage__.save(fname, format="png")
239
354
  return
240
355
 
241
356
  img = self.__bake__()
242
-
243
- if fpath is str:
244
- img.save(fpath)
357
+
358
+ if isinstance(fname, str):
359
+ img.save(fname)
245
360
  else:
246
- img.save(fpath, format="png")
361
+ img.save(fname, format="png")
247
362
 
248
363
 
249
364
  def show(self):
@@ -277,4 +392,7 @@ class Grid(AttrObject):
277
392
 
278
393
 
279
394
  def getSize(self):
280
- return self.__bakedImage__.size
395
+ if self.__bakedImage__:
396
+ return self.__bakedImage__.size
397
+ layout = self._prepare_cells(vector=False)
398
+ return layout["size"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaxe
3
- Version: 1.4.3
3
+ Version: 1.4.4.dev3
4
4
  Summary: A small graphing tool for functions, points, equations and more
5
5
  Author-email: Valter Yde Daugberg <valteryde@hotmail.com>
6
6
  Project-URL: Homepage, https://github.com/valteryde/kaxe
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes