Skip to content

Commit fd3a24f

Browse files
committed
Render figure-level text objects.
This fixes issues such as mpld3#296 and maybe more, about figure-level text (such as fig.suptitle) not appearing. It requires the corresponding mplexporter PR to land first, and only then I should also update the submodule commit to that here: mpld3/mplexporter#70 It also sets up support for figure-level transforms, which we'll need to support more figure-level objects in the future. I put the compiled js files in a separate commit for ease of reviewing. This, too, was co-developed with gpt-5.1-codex, but I was heavily involved. Here is how it summarizes the changes: - Export figure-level text (suptitle + fig.text) into the mpld3 figure JSON (texts array) via the vendored exporter/renderer, rather than shoving it into an axes. - JS core updated to handle figure-level coordinates/text: Coordinates can work with a figure, Figure renders figuretexts into a dedicated group, and Text can draw without an axes. - Added tests asserting figure-level texts are exported and included in the figure dict; rebuilt the JS bundle accordingly.
1 parent 3aad00b commit fd3a24f

File tree

6 files changed

+74
-20
lines changed

6 files changed

+74
-20
lines changed

mpld3/mpld3renderer.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def open_figure(self, fig, props):
104104
height=props['figheight'] * props['dpi'],
105105
axes=[],
106106
data={},
107+
texts=[],
107108
id=get_id(fig))
108109

109110
def close_figure(self, fig):
@@ -246,6 +247,21 @@ def draw_text(self, text, position, coordinates, style,
246247
id=get_id(mplobj))
247248
self.axes_json['texts'].append(text)
248249

250+
def draw_figure_text(self, text, position, coordinates, style,
251+
text_type=None, mplobj=None):
252+
text = dict(text=text,
253+
position=tuple(position),
254+
coordinates=coordinates,
255+
h_anchor=TEXT_HA_DICT[style['halign']],
256+
v_baseline=TEXT_VA_DICT[style['valign']],
257+
rotation=-style['rotation'],
258+
fontsize=style['fontsize'],
259+
color=style['color'],
260+
alpha=style['alpha'],
261+
zorder=style['zorder'],
262+
id=get_id(mplobj))
263+
self.figure_json['texts'].append(text)
264+
249265
def draw_image(self, imdata, extent, coordinates, style, mplobj=None):
250266
image = dict(data=imdata, extent=extent, coordinates=coordinates)
251267
image.update(style)

mpld3/tests/test_elements.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import matplotlib.pyplot as plt
66
from .. import fig_to_dict, fig_to_html
7-
from numpy.testing import assert_equal
7+
from numpy.testing import assert_equal, assert_almost_equal
88

99

1010
def test_line():
@@ -137,6 +137,30 @@ def test_image():
137137
assert_equal(image['coordinates'], "data")
138138

139139

140+
def test_figure_texts_are_exported():
141+
fig, ax = plt.subplots()
142+
fig.suptitle("Hello title", x=0.25, y=0.9, ha="left")
143+
fig.text(0.1, 0.05, "Footer", ha="center", va="center")
144+
145+
rep = fig_to_dict(fig)
146+
texts = rep['texts']
147+
148+
assert_equal(len(texts), 2)
149+
150+
title = texts[0]
151+
footer = texts[1]
152+
153+
assert_equal(title['text'], "Hello title")
154+
assert_equal(title['coordinates'], "figure")
155+
assert_almost_equal(title['position'][0], 0.25)
156+
assert_almost_equal(title['position'][1], 0.9)
157+
158+
assert_equal(footer['text'], "Footer")
159+
assert_equal(footer['coordinates'], "figure")
160+
assert_almost_equal(footer['position'][0], 0.1)
161+
assert_almost_equal(footer['position'][1], 0.05)
162+
163+
140164
def test_ticks():
141165
plt.xticks([1,2,3])
142166
rep = fig_to_html(plt.gcf())

mpld3/tests/test_figure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_basic_figure():
1414
plt.close(fig)
1515

1616
assert_equal(list(sorted(rep.keys())),
17-
['axes', 'data', 'height', 'id', 'plugins', 'width'])
17+
['axes', 'data', 'height', 'id', 'plugins', 'texts', 'width'])
1818
assert_equal(rep['width'], size[0] * dpi)
1919
assert_equal(rep['height'], size[1] * dpi)
2020
assert_equal(rep['data'], {})

src/core/coordinates.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@
33
/* `trans` is one of ["data", "figure", "axes", "display"] */
44
mpld3.Coordinates = mpld3_Coordinates;
55

6-
function mpld3_Coordinates(trans, ax) {
6+
function mpld3_Coordinates(trans, ax, fig) {
77
this.trans = trans;
8-
if (typeof(ax) === "undefined") {
9-
this.ax = null;
10-
this.fig = null;
11-
if (this.trans !== "display")
12-
throw "ax must be defined if transform != 'display'";
13-
} else {
14-
this.ax = ax;
15-
this.fig = ax.fig;
16-
}
8+
this.ax = (typeof(ax) === "undefined") ? null : ax;
9+
this.fig = (typeof(fig) === "undefined") ? (this.ax ? this.ax.fig : null) : fig;
10+
if (this.ax === null && this.fig === null && this.trans !== "display")
11+
throw "ax or fig must be defined if transform != 'display'";
12+
if (this.ax === null && this.trans !== "display" && this.trans !== "figure")
13+
throw "ax must be defined if transform != 'display' and transform != 'figure'";
1714
this.zoomable = (this.trans === "data");
1815
this.x = this["x_" + this.trans];
1916
this.y = this["y_" + this.trans];
@@ -46,8 +43,8 @@ mpld3_Coordinates.prototype.y_axes = function(y) {
4643
return this.ax.height * (1 - y);
4744
}
4845
mpld3_Coordinates.prototype.x_figure = function(x) {
49-
return x * this.fig.width - this.ax.position[0];
46+
return this.ax ? x * this.fig.width - this.ax.position[0] : x * this.fig.width;
5047
}
5148
mpld3_Coordinates.prototype.y_figure = function(y) {
52-
return (1 - y) * this.fig.height - this.ax.position[1];
49+
return this.ax ? (1 - y) * this.fig.height - this.ax.position[1] : (1 - y) * this.fig.height;
5350
}

src/core/figure.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mpld3_Figure.prototype.requiredProps = ["width", "height"];
1212
mpld3_Figure.prototype.defaultProps = {
1313
data: {},
1414
axes: [],
15+
texts: [],
1516
plugins: [{type: "reset"}, {type: "zoom"}, {type: "boxzoom"}]
1617
};
1718

@@ -33,6 +34,11 @@ function mpld3_Figure(figid, props) {
3334
for (var i = 0; i < this.props.axes.length; i++)
3435
this.axes.push(new mpld3_Axes(this, this.props.axes[i]));
3536

37+
// Figure-level texts (e.g. suptitle, fig.text)
38+
this.figuretexts = [];
39+
for (var j = 0; j < this.props.texts.length; j++)
40+
this.figuretexts.push(new mpld3.Text(this, this.props.texts[j]));
41+
3642
// Connect the plugins to the figure
3743
this.plugins = [];
3844
this.pluginsByType = {};
@@ -91,6 +97,12 @@ mpld3_Figure.prototype.draw = function() {
9197
this.axes[i].draw();
9298
}
9399

100+
this.figureTextGroup = this.canvas.append("g")
101+
.attr("class", "mpld3-figure-texts");
102+
for (var k = 0; k < this.figuretexts.length; k++) {
103+
this.figuretexts[k].draw();
104+
}
105+
94106
// disable zoom by default; plugins or toolbar items might change this.
95107
this.disableZoom();
96108

src/elements/text.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@ function mpld3_Text(ax, props) {
2020
mpld3_PlotElement.call(this, ax, props);
2121
this.text = this.props.text;
2222
this.position = this.props.position;
23-
this.coords = new mpld3_Coordinates(this.props.coordinates, this.ax);
23+
this.coords = new mpld3_Coordinates(this.props.coordinates, this.ax, this.fig);
2424
};
2525

2626
mpld3_Text.prototype.draw = function() {
27-
if (this.props.coordinates == "data") {
28-
if (this.coords.zoomable) {
29-
this.obj = this.ax.paths.append("text");
27+
if (this.ax) {
28+
if (this.props.coordinates == "data") {
29+
if (this.coords.zoomable) {
30+
this.obj = this.ax.paths.append("text");
31+
} else {
32+
this.obj = this.ax.staticPaths.append("text");
33+
}
3034
} else {
31-
this.obj = this.ax.staticPaths.append("text");
35+
this.obj = this.ax.baseaxes.append("text");
3236
}
3337
} else {
34-
this.obj = this.ax.baseaxes.append("text");
38+
var target = this.fig.figureTextGroup || this.fig.canvas;
39+
this.obj = target.append("text");
3540
}
3641

3742
this.obj

0 commit comments

Comments
 (0)