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