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