kaxe 1.4.2__tar.gz → 1.4.4.dev2__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.2/src/kaxe.egg-info → kaxe-1.4.4.dev2}/PKG-INFO +1 -1
  2. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/pyproject.toml +1 -1
  3. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/legend.py +33 -17
  4. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/shapes.py +3 -1
  5. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/svg.py +54 -0
  6. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/window.py +1 -0
  7. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/d3/plot3d.py +2 -0
  8. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/grid.py +180 -62
  9. {kaxe-1.4.2 → kaxe-1.4.4.dev2/src/kaxe.egg-info}/PKG-INFO +1 -1
  10. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/LICENSE +0 -0
  11. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/MANIFEST.in +0 -0
  12. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/README.md +0 -0
  13. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/setup.cfg +0 -0
  14. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/__init__.py +0 -0
  15. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/chart/__init__.py +0 -0
  16. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/chart/bar.py +0 -0
  17. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/chart/box.py +0 -0
  18. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/chart/pie.py +0 -0
  19. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/chart/qqplot.py +0 -0
  20. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/__init__.py +0 -0
  21. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/axis.py +0 -0
  22. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/color.py +0 -0
  23. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/backend.py +0 -0
  24. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/camera.py +0 -0
  25. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/helper.py +0 -0
  26. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/hud.py +0 -0
  27. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/__init__.py +0 -0
  28. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/color.py +0 -0
  29. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/line.py +0 -0
  30. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/point.py +0 -0
  31. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/pointer.py +0 -0
  32. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/objects/triangle.py +0 -0
  33. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/openglrender.py +0 -0
  34. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/d3/translator.py +0 -0
  35. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/draw.py +0 -0
  36. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/fileloader.py +0 -0
  37. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/helper.py +0 -0
  38. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/line.py +0 -0
  39. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/marker.py +0 -0
  40. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/profiler.py +0 -0
  41. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/round.py +0 -0
  42. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/styles.py +0 -0
  43. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/symbol.py +0 -0
  44. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/core/text.py +0 -0
  45. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/data/__init__.py +0 -0
  46. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/data/excel.py +0 -0
  47. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/__init__.py +0 -0
  48. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/_lazy.py +0 -0
  49. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/__init__.py +0 -0
  50. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/arrow.py +0 -0
  51. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/bubble.py +0 -0
  52. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/contour.py +0 -0
  53. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/equation.py +0 -0
  54. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/fill.py +0 -0
  55. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/function.py +0 -0
  56. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/map.py +0 -0
  57. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/parameter.py +0 -0
  58. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/pillar.py +0 -0
  59. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d2/point.py +0 -0
  60. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/__init__.py +0 -0
  61. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/base.py +0 -0
  62. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/function.py +0 -0
  63. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/mesh.py +0 -0
  64. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/point.py +0 -0
  65. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/d3/potato.py +0 -0
  66. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/function.py +0 -0
  67. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/legend.py +0 -0
  68. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/mapdata.py +0 -0
  69. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/point.py +0 -0
  70. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/objects/text.py +0 -0
  71. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/__init__.py +0 -0
  72. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/_lazy.py +0 -0
  73. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/box.py +0 -0
  74. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/constants.py +0 -0
  75. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/d3/__init__.py +0 -0
  76. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/d3/axes.py +0 -0
  77. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/d3/geometry.py +0 -0
  78. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/d3/variants.py +0 -0
  79. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/double.py +0 -0
  80. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/empty.py +0 -0
  81. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/log.py +0 -0
  82. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/polar.py +0 -0
  83. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/standard.py +0 -0
  84. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/themes.py +0 -0
  85. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/zoom.py +0 -0
  86. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/plot/zoom_connector.py +0 -0
  87. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/__init__.py +0 -0
  88. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/__init__.py +0 -0
  89. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.bright-oblique.ttf +0 -0
  90. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.bright-roman.ttf +0 -0
  91. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.bright-semibold.ttf +0 -0
  92. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.bright-semiboldoblique.ttf +0 -0
  93. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.classical-serif-italic.ttf +0 -0
  94. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.concrete-bold.ttf +0 -0
  95. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.concrete-bolditalic.ttf +0 -0
  96. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.concrete-italic.ttf +0 -0
  97. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.concrete-roman.ttf +0 -0
  98. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-bold.ttf +0 -0
  99. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-boldoblique.ttf +0 -0
  100. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-demi-condensed-demicondensed.ttf +0 -0
  101. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-medium.ttf +0 -0
  102. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-oblique.ttf +0 -0
  103. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-bold.ttf +0 -0
  104. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-bolditalic.ttf +0 -0
  105. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-boldslanted.ttf +0 -0
  106. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-romanslanted.ttf +0 -0
  107. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-italic.ttf +0 -0
  108. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-roman.ttf +0 -0
  109. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.serif-upright-italic-uprightitalic.ttf +0 -0
  110. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bold.ttf +0 -0
  111. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bolditalic.ttf +0 -0
  112. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-italic.ttf +0 -0
  113. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-light.ttf +0 -0
  114. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-lightoblique.ttf +0 -0
  115. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-regular.ttf +0 -0
  116. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-italic.ttf +0 -0
  117. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-medium.ttf +0 -0
  118. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/computer-modern-family/readme.txt +0 -0
  119. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/logo-small.png +0 -0
  120. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/symbolcross.png +0 -0
  121. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/symboldonut.png +0 -0
  122. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/symbollollipop.png +0 -0
  123. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe/resources/symboltriangle.png +0 -0
  124. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe.egg-info/SOURCES.txt +0 -0
  125. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe.egg-info/dependency_links.txt +0 -0
  126. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe.egg-info/requires.txt +0 -0
  127. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/src/kaxe.egg-info/top_level.txt +0 -0
  128. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/tests/test.py +0 -0
  129. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/tests/test2.py +0 -0
  130. {kaxe-1.4.2 → kaxe-1.4.4.dev2}/tests/test3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaxe
3
- Version: 1.4.2
3
+ Version: 1.4.4.dev2
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.2"
3
+ version = "1.4.4.dev2"
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
@@ -106,19 +108,19 @@ class LegendBox(AttrObject):
106
108
  width = symbolSize[0] + text.width + legendGridSpacing[0] + legendSymbolTextSpacing
107
109
  height = max(symbolSize[1], text.height)
108
110
 
109
- currentLineWidth += width
110
- # hvis bredden er for meget så gå en linje ned
111
- if currentLineWidth > legendMaxWidth:
111
+ if currentLineWidth > 0 and currentLineWidth + width > legendMaxWidth:
112
112
  grid.append({"elements":[], "height":0})
113
113
  currentLineWidth = 0
114
-
114
+
115
+ currentLineWidth += width
115
116
  grid[-1]["elements"].append((symbol, text, width, height))
116
117
  grid[-1]["height"] = max(grid[-1]["height"], height)
117
118
 
119
+ grid = [row for row in grid if row["elements"]]
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:
@@ -132,23 +134,24 @@ class LegendBox(AttrObject):
132
134
 
133
135
  currentLinePos[0] += width
134
136
  row_mid = y + row["height"] / 2
135
- symbol.x = x
136
137
  if isinstance(symbol, shapes.Circle):
138
+ symbol.x = x + symbolSize[0] / 2
137
139
  symbol.centerAlign()
138
140
  symbol.y = row_mid
139
- sym_min_x = symbol.x - symbolSize[0] / 2
140
- sym_max_x = symbol.x + symbolSize[0] / 2
141
+ sym_min_x = x
142
+ sym_max_x = x + symbolSize[0]
141
143
  sym_min_y = symbol.y - symbolSize[1] / 2
142
144
  sym_max_y = symbol.y + symbolSize[1] / 2
143
145
  else:
146
+ symbol.x = x
144
147
  symbol.y = row_mid - symbolSize[1] / 2
145
148
  sym_min_x = symbol.x
146
149
  sym_max_x = symbol.x + symbolSize[0]
147
150
  sym_min_y = symbol.y
148
151
  sym_max_y = symbol.y + symbolSize[1]
149
152
  text.setLeftTopPos(
150
- x + symbolSize[0] + legendSymbolTextSpacing,
151
- y + text.height/2 + row["height"]/2 #+ text.height + height/2 - text.height/2 #+ height/2 + text.height/2
153
+ x + symbolSize[0] + legendSymbolTextSpacing,
154
+ y + row["height"] / 2 + text.height / 2,
152
155
  )
153
156
 
154
157
  if debug:
@@ -166,8 +169,13 @@ class LegendBox(AttrObject):
166
169
 
167
170
  if debug: shapes.Circle(0,0, 10, batch=self.batch)
168
171
 
169
- height = (maxPos[1] - minPos[1])
170
- 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])
171
179
 
172
180
  ### skub den ned til window 0,0 (her burde title være med)
173
181
  # self.batch.push(-minPos[0], -minPos[1])
@@ -192,11 +200,19 @@ class LegendBox(AttrObject):
192
200
  # tilføj plads til legenden
193
201
  parent.addPaddingCondition(bottom=height+legendGap)
194
202
 
195
- else: # Only works with PILLOW
203
+ else:
196
204
  self.batch.push(-minPos[0], -minPos[1])
197
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
198
211
  surface = Image.new('RGBA', (int(width), int(height)), (0,0,0,0))
199
-
200
212
  self.boxshape.draw(surface)
201
213
  self.batch.draw(surface)
202
- 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:
@@ -368,6 +368,60 @@ class SvgDocument:
368
368
  return '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + body
369
369
 
370
370
 
371
+ def parse_svg_root(xml: str) -> ET.Element:
372
+ """Parse SVG XML and return the root element."""
373
+ if xml.startswith("<?"):
374
+ xml = xml.split("\n", 1)[1]
375
+ root = ET.fromstring(xml)
376
+ if not (root.tag.endswith("svg") or root.tag == "svg"):
377
+ raise ValueError("Expected SVG root element")
378
+ return root
379
+
380
+
381
+ def extract_svg_children(root: ET.Element) -> tuple[list[ET.Element], Optional[str]]:
382
+ """Return drawable children (excluding defs) and optional fondi style text."""
383
+ fondi_css: Optional[str] = None
384
+ children: list[ET.Element] = []
385
+ for child in root:
386
+ local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
387
+ if local == "defs":
388
+ for sub in child:
389
+ sub_local = sub.tag.split("}")[-1] if "}" in sub.tag else sub.tag
390
+ if sub_local == "style" and sub.text:
391
+ fondi_css = sub.text
392
+ continue
393
+ children.append(child)
394
+ return children, fondi_css
395
+
396
+
397
+ def embed_svg_children(
398
+ doc: SvgDocument,
399
+ children: list[ET.Element],
400
+ tx: float,
401
+ ty: float,
402
+ ) -> None:
403
+ """Embed copied SVG children at top-left offset (SVG y-down coordinates)."""
404
+ if not children:
405
+ return
406
+ group = ET.Element(
407
+ f"{{{SVG_NS}}}g",
408
+ {"transform": f"translate({round(tx, 3)},{round(ty, 3)})"},
409
+ )
410
+ for child in children:
411
+ group.append(copy.deepcopy(child))
412
+ doc._elements.append(group)
413
+
414
+
415
+ def merge_fondi_css(doc: SvgDocument, *style_texts: Optional[str]) -> None:
416
+ """Set fondi font CSS on doc once (skip duplicates and empty)."""
417
+ if doc._fondi_font_css is not None:
418
+ return
419
+ for text in style_texts:
420
+ if text:
421
+ doc._fondi_font_css = text
422
+ return
423
+
424
+
371
425
  def is_file_path(fname: Any) -> bool:
372
426
  """Return True when fname is a filesystem path (str or os.PathLike)."""
373
427
  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.2
3
+ Version: 1.4.4.dev2
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