plot.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. """Visualize DesignSpaceDocument and resulting VariationModel."""
  2. from fontTools.varLib.models import VariationModel, supportScalar
  3. from fontTools.designspaceLib import DesignSpaceDocument
  4. from matplotlib import pyplot
  5. from mpl_toolkits.mplot3d import axes3d
  6. from itertools import cycle
  7. import math
  8. import logging
  9. import sys
  10. log = logging.getLogger(__name__)
  11. def stops(support, count=10):
  12. a, b, c = support
  13. return (
  14. [a + (b - a) * i / count for i in range(count)]
  15. + [b + (c - b) * i / count for i in range(count)]
  16. + [c]
  17. )
  18. def _plotLocationsDots(locations, axes, subplot, **kwargs):
  19. for loc, color in zip(locations, cycle(pyplot.cm.Set1.colors)):
  20. if len(axes) == 1:
  21. subplot.plot([loc.get(axes[0], 0)], [1.0], "o", color=color, **kwargs)
  22. elif len(axes) == 2:
  23. subplot.plot(
  24. [loc.get(axes[0], 0)],
  25. [loc.get(axes[1], 0)],
  26. [1.0],
  27. "o",
  28. color=color,
  29. **kwargs,
  30. )
  31. else:
  32. raise AssertionError(len(axes))
  33. def plotLocations(locations, fig, names=None, **kwargs):
  34. n = len(locations)
  35. cols = math.ceil(n**0.5)
  36. rows = math.ceil(n / cols)
  37. if names is None:
  38. names = [None] * len(locations)
  39. model = VariationModel(locations)
  40. names = [names[model.reverseMapping[i]] for i in range(len(names))]
  41. axes = sorted(locations[0].keys())
  42. if len(axes) == 1:
  43. _plotLocations2D(model, axes[0], fig, cols, rows, names=names, **kwargs)
  44. elif len(axes) == 2:
  45. _plotLocations3D(model, axes, fig, cols, rows, names=names, **kwargs)
  46. else:
  47. raise ValueError("Only 1 or 2 axes are supported")
  48. def _plotLocations2D(model, axis, fig, cols, rows, names, **kwargs):
  49. subplot = fig.add_subplot(111)
  50. for i, (support, color, name) in enumerate(
  51. zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))
  52. ):
  53. if name is not None:
  54. subplot.set_title(name)
  55. subplot.set_xlabel(axis)
  56. pyplot.xlim(-1.0, +1.0)
  57. Xs = support.get(axis, (-1.0, 0.0, +1.0))
  58. X, Y = [], []
  59. for x in stops(Xs):
  60. y = supportScalar({axis: x}, support)
  61. X.append(x)
  62. Y.append(y)
  63. subplot.plot(X, Y, color=color, **kwargs)
  64. _plotLocationsDots(model.locations, [axis], subplot)
  65. def _plotLocations3D(model, axes, fig, rows, cols, names, **kwargs):
  66. ax1, ax2 = axes
  67. axis3D = fig.add_subplot(111, projection="3d")
  68. for i, (support, color, name) in enumerate(
  69. zip(model.supports, cycle(pyplot.cm.Set1.colors), cycle(names))
  70. ):
  71. if name is not None:
  72. axis3D.set_title(name)
  73. axis3D.set_xlabel(ax1)
  74. axis3D.set_ylabel(ax2)
  75. pyplot.xlim(-1.0, +1.0)
  76. pyplot.ylim(-1.0, +1.0)
  77. Xs = support.get(ax1, (-1.0, 0.0, +1.0))
  78. Ys = support.get(ax2, (-1.0, 0.0, +1.0))
  79. for x in stops(Xs):
  80. X, Y, Z = [], [], []
  81. for y in Ys:
  82. z = supportScalar({ax1: x, ax2: y}, support)
  83. X.append(x)
  84. Y.append(y)
  85. Z.append(z)
  86. axis3D.plot(X, Y, Z, color=color, **kwargs)
  87. for y in stops(Ys):
  88. X, Y, Z = [], [], []
  89. for x in Xs:
  90. z = supportScalar({ax1: x, ax2: y}, support)
  91. X.append(x)
  92. Y.append(y)
  93. Z.append(z)
  94. axis3D.plot(X, Y, Z, color=color, **kwargs)
  95. _plotLocationsDots(model.locations, [ax1, ax2], axis3D)
  96. def plotDocument(doc, fig, **kwargs):
  97. doc.normalize()
  98. locations = [s.location for s in doc.sources]
  99. names = [s.name for s in doc.sources]
  100. plotLocations(locations, fig, names, **kwargs)
  101. def _plotModelFromMasters2D(model, masterValues, fig, **kwargs):
  102. assert len(model.axisOrder) == 1
  103. axis = model.axisOrder[0]
  104. axis_min = min(loc.get(axis, 0) for loc in model.locations)
  105. axis_max = max(loc.get(axis, 0) for loc in model.locations)
  106. import numpy as np
  107. X = np.arange(axis_min, axis_max, (axis_max - axis_min) / 100)
  108. Y = []
  109. for x in X:
  110. loc = {axis: x}
  111. v = model.interpolateFromMasters(loc, masterValues)
  112. Y.append(v)
  113. subplot = fig.add_subplot(111)
  114. subplot.plot(X, Y, "-", **kwargs)
  115. def _plotModelFromMasters3D(model, masterValues, fig, **kwargs):
  116. assert len(model.axisOrder) == 2
  117. axis1, axis2 = model.axisOrder[0], model.axisOrder[1]
  118. axis1_min = min(loc.get(axis1, 0) for loc in model.locations)
  119. axis1_max = max(loc.get(axis1, 0) for loc in model.locations)
  120. axis2_min = min(loc.get(axis2, 0) for loc in model.locations)
  121. axis2_max = max(loc.get(axis2, 0) for loc in model.locations)
  122. import numpy as np
  123. X = np.arange(axis1_min, axis1_max, (axis1_max - axis1_min) / 100)
  124. Y = np.arange(axis2_min, axis2_max, (axis2_max - axis2_min) / 100)
  125. X, Y = np.meshgrid(X, Y)
  126. Z = []
  127. for row_x, row_y in zip(X, Y):
  128. z_row = []
  129. Z.append(z_row)
  130. for x, y in zip(row_x, row_y):
  131. loc = {axis1: x, axis2: y}
  132. v = model.interpolateFromMasters(loc, masterValues)
  133. z_row.append(v)
  134. Z = np.array(Z)
  135. axis3D = fig.add_subplot(111, projection="3d")
  136. axis3D.plot_surface(X, Y, Z, **kwargs)
  137. def plotModelFromMasters(model, masterValues, fig, **kwargs):
  138. """Plot a variation model and set of master values corresponding
  139. to the locations to the model into a pyplot figure. Variation
  140. model must have axisOrder of size 1 or 2."""
  141. if len(model.axisOrder) == 1:
  142. _plotModelFromMasters2D(model, masterValues, fig, **kwargs)
  143. elif len(model.axisOrder) == 2:
  144. _plotModelFromMasters3D(model, masterValues, fig, **kwargs)
  145. else:
  146. raise ValueError("Only 1 or 2 axes are supported")
  147. def main(args=None):
  148. from fontTools import configLogger
  149. if args is None:
  150. args = sys.argv[1:]
  151. # configure the library logger (for >= WARNING)
  152. configLogger()
  153. # comment this out to enable debug messages from logger
  154. # log.setLevel(logging.DEBUG)
  155. if len(args) < 1:
  156. print("usage: fonttools varLib.plot source.designspace", file=sys.stderr)
  157. print(" or")
  158. print("usage: fonttools varLib.plot location1 location2 ...", file=sys.stderr)
  159. print(" or")
  160. print(
  161. "usage: fonttools varLib.plot location1=value1 location2=value2 ...",
  162. file=sys.stderr,
  163. )
  164. sys.exit(1)
  165. fig = pyplot.figure()
  166. fig.set_tight_layout(True)
  167. if len(args) == 1 and args[0].endswith(".designspace"):
  168. doc = DesignSpaceDocument()
  169. doc.read(args[0])
  170. plotDocument(doc, fig)
  171. else:
  172. axes = [chr(c) for c in range(ord("A"), ord("Z") + 1)]
  173. if "=" not in args[0]:
  174. locs = [dict(zip(axes, (float(v) for v in s.split(",")))) for s in args]
  175. plotLocations(locs, fig)
  176. else:
  177. locations = []
  178. masterValues = []
  179. for arg in args:
  180. loc, v = arg.split("=")
  181. locations.append(dict(zip(axes, (float(v) for v in loc.split(",")))))
  182. masterValues.append(float(v))
  183. model = VariationModel(locations, axes[: len(locations[0])])
  184. plotModelFromMasters(model, masterValues, fig)
  185. pyplot.show()
  186. if __name__ == "__main__":
  187. import sys
  188. sys.exit(main())