Coverage for src/plotly_gtk/chart.py: 20%

316 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-08 21:22 +0000

1"""This module contains a class for rendering a plotly 

2:class:`plotly.graph_objects.Figure` using GTK.""" 

3 

4import datetime 

5import numbers 

6from datetime import timezone 

7from typing import TYPE_CHECKING 

8 

9import numpy as np 

10import pandas as pd 

11 

12from plotly_gtk._chart import _PlotlyGtk 

13from plotly_gtk.utils import * # pylint: disable=wildcard-import, unused-wildcard-import 

14from plotly_gtk.utils.ticks import Ticks 

15from plotly_gtk.widgets import * # pylint: disable=wildcard-import 

16 

17gi.require_version("Gdk", "4.0") 

18gi.require_version("Gtk", "4.0") 

19from gi.repository import ( # pylint: disable=wrong-import-order,wrong-import-position 

20 Gtk, 

21) 

22 

23if TYPE_CHECKING: 

24 from plotly import graph_objects as go 

25 

26try: 

27 from plotly import graph_objects as go 

28except NameError: 

29 print("plotly not available") 

30 

31 

32class PlotlyGtk(Gtk.Overlay): 

33 """Class for rendering plotly :class:`plotly.graph_objects.Figure`.""" 

34 

35 def __init__(self, fig: "go.Figure | dict"): 

36 super().__init__() 

37 self.pushmargin = {} 

38 try: 

39 fig = fig.to_dict() if isinstance(fig, go.Figure) else fig 

40 except NameError: 

41 pass 

42 

43 self.data = fig["data"] 

44 

45 self.layout = fig["layout"] 

46 if self.layout is None: 

47 self.layout = {} 

48 self._update_layout() 

49 fig["layout"] = self.layout 

50 fig["layout"]["_margin"] = dict(self.layout["margin"]) 

51 

52 self._prepare_data() 

53 fig["data"] = self.data 

54 self.fig = fig 

55 

56 self.overlays = [] 

57 

58 self.connect( 

59 "get_child_position", 

60 lambda overlay, widget, allocation: widget.get_position( 

61 overlay, allocation 

62 ), 

63 ) 

64 

65 self.connect("realize", lambda _: self.update(self.fig)) 

66 

67 def update(self, fig: dict[str, plotly_types.Data | plotly_types.Layout]): 

68 """Update the view. 

69 

70 Parameters 

71 ---------- 

72 fig: dict[str, plotly_types.Data | plotly_types.Layout] 

73 A dictionary representing a plotly figure 

74 """ 

75 self._update_ranges() 

76 self._update_positions_and_domains() 

77 for overlay in self.overlays: 

78 self.remove_overlay(overlay) 

79 self.overlays = [] 

80 self._draw_buttons() 

81 self._draw_legend() 

82 self._draw_titles() 

83 self._draw_annotations() 

84 self.set_child(_PlotlyGtk(fig)) 

85 

86 def _update_ranges(self): 

87 axes = [k for k in self.layout if "axis" in k] 

88 for axis in axes: 

89 if "autorange" in self.layout[axis]: 

90 autorange = self.layout[axis]["autorange"] 

91 if "range" in self.layout[axis] and len(self.layout[axis]["range"]) == 2: 

92 autorange = False 

93 else: 

94 autorange = True 

95 

96 axis_letter = axis[0 : axis.find("axis")] 

97 

98 plots_on_axis = [ 

99 plot 

100 for plot in self.data 

101 if f"{axis_letter}axis" in plot 

102 and plot[f"{axis_letter}axis"] == axis.replace("axis", "") 

103 and ("visible" not in plot or plot["visible"]) 

104 ] 

105 hidden_plots_on_axis = [ 

106 plot 

107 for plot in self.data 

108 if f"{axis_letter}axis" in plot 

109 and plot[f"{axis_letter}axis"] == axis.replace("axis", "") 

110 and ("visible" not in plot or plot["visible"]) 

111 and ("_visible" not in plot or plot["_visible"]) 

112 ] 

113 if len(plots_on_axis) > 1: 

114 plots_on_axis = hidden_plots_on_axis 

115 if plots_on_axis == []: 

116 continue 

117 

118 if autorange: 

119 _range = [ 

120 np.nanmin( 

121 [ 

122 ( 

123 np.nanmin(plot[axis_letter]) 

124 if axis_letter in plot 

125 else np.nan 

126 ) 

127 for plot in plots_on_axis 

128 ] 

129 ), 

130 np.nanmax( 

131 [ 

132 ( 

133 np.nanmax(plot[axis_letter]) 

134 if axis_letter in plot 

135 else np.nan 

136 ) 

137 for plot in plots_on_axis 

138 ] 

139 ), 

140 ] 

141 if _range[0] == _range[-1]: 

142 _range[0] = _range[0] - 1 

143 _range[-1] = _range[1] + 1 

144 self.layout[axis]["_range"] = _range 

145 else: 

146 if self.layout[axis]["type"] == "log": 

147 self.layout[axis]["_range"] = np.array( 

148 [ 

149 10 ** self.layout[axis]["range"][0], 

150 10 ** self.layout[axis]["range"][-1], 

151 ] 

152 ) 

153 else: 

154 self.layout[axis]["_range"] = np.array(self.layout[axis]["range"]) 

155 

156 # Do matching 

157 matched_to_axes = { 

158 self.layout[axis]["matches"] 

159 for axis in axes 

160 if "matches" in self.layout[axis] 

161 } 

162 match_groups = { 

163 axis: [ 

164 ax 

165 for ax in axes 

166 if ax == axis[0] + "axis" + axis[1:] 

167 or ("matches" in self.layout[ax] and self.layout[ax]["matches"] == axis) 

168 ] 

169 for axis in matched_to_axes 

170 } 

171 for match_group in match_groups.values(): 

172 _ranges = [self.layout[axis]["_range"] for axis in match_group] 

173 _range = [min(r[0] for r in _ranges), max(r[-1] for r in _ranges)] 

174 for axis in match_group: 

175 self.layout[axis]["_range"] = _range 

176 

177 for axis in axes: 

178 if "_range" not in self.layout[axis]: 

179 continue 

180 if self.layout[axis]["_type"] == "log": 

181 self.layout[axis]["_range"] = np.log10(self.layout[axis]["_range"]) 

182 range_length = ( 

183 self.layout[axis]["_range"][-1] - self.layout[axis]["_range"][0] 

184 ) 

185 if ( 

186 "range" in self.layout[axis] 

187 and len(self.layout[axis]["range"]) == 2 

188 ): 

189 range_addon = range_length * 0.001 

190 else: 

191 range_addon = range_length * 0.125 / 2 

192 self.layout[axis]["_range"] = [ 

193 self.layout[axis]["_range"][0] - range_addon, 

194 self.layout[axis]["_range"][-1] + range_addon, 

195 ] 

196 else: 

197 range_length = ( 

198 self.layout[axis]["_range"][-1] - self.layout[axis]["_range"][0] 

199 ) 

200 if ( 

201 "range" in self.layout[axis] 

202 and len(self.layout[axis]["range"]) == 2 

203 ): 

204 range_addon = range_length * 0.001 

205 else: 

206 range_addon = range_length * 0.125 / 2 

207 self.layout[axis]["_range"] = [ 

208 self.layout[axis]["_range"][0] - range_addon, 

209 self.layout[axis]["_range"][-1] + range_addon, 

210 ] 

211 if "_ticksobject" not in self.layout[axis]: 

212 self.layout[axis]["_ticksobject"] = Ticks( 

213 self.layout, 

214 axis, 

215 0, 

216 ) 

217 self.layout[axis]["_ticksobject"].calculate() 

218 else: 

219 self.layout[axis]["_ticksobject"].calculate() 

220 

221 def _prepare_data(self): 

222 for plot in self.data: 

223 if self._detect_axis_type(plot["x"]) == "date": 

224 plot["x"] = np.array(plot["x"], dtype="datetime64") 

225 plot["x"] = pd.to_datetime(plot["x"]) 

226 plot["x"] = [ 

227 x.replace(tzinfo=timezone.utc).timestamp() for x in plot["x"] 

228 ] 

229 plots = [] 

230 for plot in self.data: 

231 if plot["type"] in ["scatter", "scattergl"]: 

232 defaults = dict( 

233 visible=True, 

234 showlegend=True, 

235 legend="legend", 

236 legendrank=1000, 

237 legendgroup="", 

238 legendgrouptitle=dict(), 

239 opacity=1, 

240 zorder=0, 

241 text="", 

242 textposition="middle center", 

243 texttemplate="", 

244 hovertext="", 

245 hoverinfo="all", 

246 hovertemplate="", 

247 xhoverformat="", 

248 yhoverformat="", 

249 xaxis="x", 

250 yaxis="y", 

251 marker=dict( 

252 angle=0, 

253 angleref="up", 

254 autocolorscale=True, 

255 cauto=True, 

256 colorbar=dict(), 

257 line=dict( 

258 autocolorscale=True, 

259 cauto=True, 

260 ), 

261 size=6, 

262 sizemin=0, 

263 sizemode="diameter", 

264 sizeref=1, 

265 standoff=0, 

266 symbol="circle", 

267 ), 

268 line=dict( 

269 backoff="auto", 

270 dash="solid", 

271 shape="linear", 

272 simplify=True, 

273 smoothing=1, 

274 width=2, 

275 ), 

276 textfont=dict(), 

277 ) 

278 plot = update_dict(defaults, plot) 

279 elif plot["type"] == "histogram": 

280 defaults = dict(xaxis="x", yaxis="y", visible=True) 

281 plot = update_dict(defaults, plot) 

282 self._bin_histogram(plot) 

283 else: 

284 raise NotImplementedError(f"{plot["type"]} not yet implemented") 

285 plots.append(plot) 

286 self.data = plots 

287 

288 def _bin_histogram(self, plot): 

289 if "binned" not in plot or not plot["binned"]: 

290 n_samples = len(plot["x"]) 

291 n_bins = np.sqrt(n_samples) 

292 bin_width = (np.nanmax(plot["x"]) - np.nanmin(plot["x"])) / n_bins 

293 bin_width = round_sf(bin_width, 1) 

294 bin_start = bin_width * np.floor(np.nanmin(plot["x"]) / bin_width) 

295 bins = np.arange(bin_start, np.nanmax(plot["x"]) + bin_width, bin_width) 

296 

297 counts = pd.cut(plot["x"], bins, right=False).value_counts().to_list() 

298 plot["x"] = bins 

299 plot["y"] = counts 

300 plot["binned"] = True 

301 

302 def automargin(self): 

303 """Calculate margin sizes. 

304 

305 Raises 

306 ------ 

307 NotImplementedError 

308 If unimplemented functionality is called for. 

309 """ 

310 for pushmargin in self.pushmargin.values(): 

311 left = False 

312 right = False 

313 top = False 

314 bottom = False 

315 

316 padding = 12 

317 approximate_padding = 30 

318 width = self.get_width() 

319 height = self.get_height() 

320 

321 if pushmargin["l"] < 0: 

322 left = True 

323 if pushmargin["r"] > 1: 

324 right = True 

325 if pushmargin["t"] < 0: 

326 top = True 

327 if pushmargin["b"] > 1: 

328 bottom = True 

329 

330 # TODO reimplement to search both margins together # pylint: disable=fixme 

331 # >> m.ml 

332 # ans = (sym) 

333 # 

334 # -pad⋅x₁ - pad⋅x₂ + width⋅x₁ - x₁⋅xr - x₂⋅xl 

335 # ─────────────────────────────────────────── 

336 # x₁ - x₂ 

337 # 

338 # >> m.mr 

339 # ans = (sym) 

340 # 

341 # pad⋅x₁ + pad⋅x₂ - 2⋅pad - width⋅x₂ + width + x₁⋅xr + x₂⋅xl - xl - xr 

342 # ──────────────────────────────────────────────────────────────────── 

343 # x₁ - x₂ 

344 

345 if left: 

346 if "x" in pushmargin and "xl" in pushmargin: 

347 new = ( 

348 pushmargin["x"] * (width - self.layout["margin"]["r"]) 

349 - padding 

350 - pushmargin["xl"] 

351 ) / (pushmargin["x"] - 1) 

352 else: 

353 new = ( 

354 approximate_padding 

355 - (width - self.layout["margin"]["r"]) * pushmargin["l"] 

356 ) / (1 - pushmargin["l"]) 

357 self.layout["_margin"]["l"] = max(self.layout["_margin"]["l"], new) 

358 if right: 

359 if "x" in pushmargin and "xr" in pushmargin: 

360 new = ( 

361 (pushmargin["x"] - 1) * (width - self.layout["margin"]["l"]) 

362 + padding 

363 + pushmargin["xr"] 

364 ) / pushmargin["x"] 

365 else: 

366 new = ( 

367 approximate_padding 

368 + (width - self.layout["margin"]["l"]) * (pushmargin["r"] - 1) 

369 ) / pushmargin["r"] 

370 self.layout["_margin"]["r"] = max(self.layout["_margin"]["r"], new) 

371 if top: 

372 if "y" in pushmargin and "yt" in pushmargin: 

373 new = ( 

374 (pushmargin["y"] - 1) * (height - self.layout["margin"]["b"]) 

375 + padding 

376 + pushmargin["yt"] 

377 ) / pushmargin["y"] 

378 else: 

379 raise NotImplementedError 

380 self.layout["_margin"]["t"] = max(self.layout["_margin"]["t"], new) 

381 if bottom: 

382 if "y" in pushmargin and "yb" in pushmargin: 

383 new = ( 

384 pushmargin["y"] * (height - self.layout["margin"]["t"]) 

385 - padding 

386 - pushmargin["yb"] 

387 ) / (pushmargin["y"] - 1) 

388 else: 

389 raise NotImplementedError 

390 self.layout["_margin"]["b"] = max(self.layout["_margin"]["b"], new) 

391 self.queue_allocate() 

392 

393 def _update_layout(self): 

394 xaxes = { 

395 trace["xaxis"].replace("x", "xaxis") 

396 for trace in self.data 

397 if "xaxis" in trace 

398 } 

399 yaxes = { 

400 trace["yaxis"].replace("y", "yaxis") 

401 for trace in self.data 

402 if "yaxis" in trace 

403 } 

404 xaxes.add("xaxis") 

405 yaxes.add("yaxis") 

406 

407 template = self.layout["template"]["layout"] 

408 defaults = dict( 

409 font=dict( 

410 color="#444", 

411 family='"Open Sans", verdana, arial, sans-serif', 

412 size=12, 

413 style="normal", 

414 variant="normal", 

415 weight="normal", 

416 ), 

417 legend=dict( 

418 bordercolor="#444", 

419 borderwidth=0, 

420 entrywidth=0, 

421 entrywidthmode="pixels", 

422 font=dict( 

423 color="#444", 

424 family='"Open Sans", verdana, arial, sans-serif', 

425 size=12, 

426 style="normal", 

427 variant="normal", 

428 weight="normal", 

429 ), 

430 groupclick="togglegroup", 

431 grouptitlefont=dict( 

432 color="#444", 

433 family='"Open Sans", verdana, arial, sans-serif', 

434 size=12, 

435 style="normal", 

436 variant="normal", 

437 weight="normal", 

438 ), 

439 indentation=0, 

440 itemclick="toggle", 

441 itemdoubleclick="toggleothers", 

442 itemsizing="trace", 

443 itemwidth=30, 

444 orientation="v", 

445 title=dict( 

446 font=dict( 

447 color="#444", 

448 family='"Open Sans", verdana, arial, sans-serif', 

449 size=12, 

450 style="normal", 

451 variant="normal", 

452 weight="normal", 

453 ), 

454 text="", 

455 ), 

456 tracegroupgap=10, 

457 traceorder="", 

458 valign="middle", 

459 visible=True, 

460 xanchor="left", 

461 xref="paper", 

462 yanchor="auto", 

463 yref="paper", 

464 ), 

465 margin=dict(autoexpand=True, t=100, l=80, r=80, b=80), 

466 xaxis=dict( 

467 anchor="y", 

468 automargin=True, 

469 autorange=True, 

470 autotickangles=[0, 30, 90], 

471 color="#444", 

472 domain=[0, 1], 

473 gridcolor="#eee", 

474 griddash="solid", 

475 gridwidth=1, 

476 hoverformt="", 

477 layer="above traces", 

478 linecolor="#444", 

479 linewidth=1, 

480 minexponent=3, 

481 minor=dict(), 

482 mirror=False, 

483 nticks=0, 

484 position=0, 

485 rangemode="normal", 

486 showgrid=True, 

487 showline=True, 

488 showticklabels=True, 

489 showtickprefix="all", 

490 showticksuffix="all", 

491 side="bottom", 

492 tickangle="auto", 

493 tickfont=dict(style="normal", variant="normal", weight="normal"), 

494 tickformat="", 

495 ticklabelmode="instant", 

496 ticklabelposition="outside", 

497 ticklabelstep=1, 

498 ticklen=5, 

499 tickprefix="", 

500 ticks="", 

501 tickson="labels", 

502 ticksuffix="", 

503 tickwidth=1, 

504 title=dict( 

505 font=dict(style="normal", variant="normal", weight="normal") 

506 ), 

507 type="-", 

508 zerolinecolor="#444", 

509 zerolinewidth=1, 

510 ), 

511 yaxis=dict( 

512 anchor="x", 

513 automargin=True, 

514 autorange=True, 

515 autotickangles=[0, 30, 90], 

516 color="#444", 

517 domain=[0, 1], 

518 gridcolor="#eee", 

519 griddash="solid", 

520 gridwidth=1, 

521 hoverformt="", 

522 layer="above traces", 

523 linecolor="#444", 

524 linewidth=1, 

525 minexponent=3, 

526 minor=dict(), 

527 mirror=False, 

528 nticks=0, 

529 position=0, 

530 rangemode="normal", 

531 showgrid=True, 

532 showline=True, 

533 showticklabels=True, 

534 showtickprefix="all", 

535 showticksuffix="all", 

536 side="left", 

537 tickangle="auto", 

538 tickfont=dict(style="normal", variant="normal", weight="normal"), 

539 tickformat="", 

540 ticklabelmode="instant", 

541 ticklabelposition="outside", 

542 ticklabelstep=1, 

543 ticklen=5, 

544 tickprefix="", 

545 ticks="", 

546 tickson="labels", 

547 ticksuffix="", 

548 tickwidth=1, 

549 title=dict( 

550 font=dict(style="normal", variant="normal", weight="normal") 

551 ), 

552 type="-", 

553 zerolinecolor="#444", 

554 zerolinewidth=1, 

555 ), 

556 ) 

557 for xaxis in xaxes: 

558 if xaxis not in self.layout: 

559 self.layout[xaxis] = {} 

560 if "type" not in self.layout[xaxis]: 

561 first_plot_on_axis = [ 

562 trace 

563 for trace in self.data 

564 if "xaxis" not in trace 

565 or trace["xaxis"] == xaxis.replace("axis", "") 

566 ][0] 

567 self.layout[xaxis]["_type"] = ( 

568 self._detect_axis_type(first_plot_on_axis["x"]) 

569 if "x" in first_plot_on_axis 

570 else "linear" 

571 ) 

572 else: 

573 self.layout[xaxis]["_type"] = self.layout[xaxis]["type"] 

574 if ( 

575 "side" in self.layout[xaxis] 

576 and self.layout[xaxis]["side"] == "top" 

577 and "position" not in self.layout[xaxis] 

578 ): 

579 self.layout[xaxis]["position"] = 1 

580 template[xaxis] = template["xaxis"] 

581 defaults[xaxis] = defaults["xaxis"] 

582 for yaxis in yaxes: 

583 if yaxis not in self.layout: 

584 self.layout[yaxis] = {} 

585 if "type" not in self.layout[yaxis]: 

586 first_plot_on_axis = [ 

587 trace 

588 for trace in self.data 

589 if "yaxis" not in trace 

590 or trace["yaxis"] == yaxis.replace("axis", "") 

591 ][0] 

592 self.layout[yaxis]["_type"] = ( 

593 self._detect_axis_type(first_plot_on_axis["y"]) 

594 if "y" in first_plot_on_axis 

595 else "linear" 

596 ) 

597 else: 

598 self.layout[yaxis]["_type"] = self.layout[yaxis]["type"] 

599 if ( 

600 "side" in self.layout[yaxis] 

601 and self.layout[yaxis]["side"] == "right" 

602 and "position" not in self.layout[yaxis] 

603 ): 

604 self.layout[yaxis]["position"] = 1 

605 template[yaxis] = template["yaxis"] 

606 defaults[yaxis] = defaults["yaxis"] 

607 self.layout = update_dict(template, self.layout) 

608 self.layout = update_dict(defaults, self.layout) 

609 

610 def _update_positions_and_domains(self): 

611 axes = [k for k in self.layout if "axis" in k] 

612 axes_order = [] 

613 overlayed = [] 

614 for axis in axes: 

615 if "overlaying" in self.layout[axis]: 

616 ax = self.layout[axis]["overlaying"] 

617 overlayed.append(ax[0] + "axis" + ax[1:]) 

618 overlayed = set(overlayed) 

619 for axis in overlayed: 

620 overlayed_by = [ 

621 k 

622 for k in axes 

623 if "overlaying" in self.layout[k] 

624 and self.layout[k]["overlaying"] == axis.replace("axis", "") 

625 ] 

626 left = [ 

627 k 

628 for k in overlayed_by 

629 if "side" in self.layout[k] 

630 and self.layout[k]["side"] == "left" 

631 or "side" not in self.layout[k] 

632 ] 

633 right = [ 

634 k 

635 for k in overlayed_by 

636 if "side" in self.layout[k] and self.layout[k]["side"] == "right" 

637 ] 

638 right = sorted(right) 

639 left = sorted(left) 

640 

641 if "side" in self.layout[axis] and self.layout[axis]["side"] == "right": 

642 right = [axis] + right 

643 else: 

644 left = [axis] + left 

645 

646 for side in [left, right]: 

647 if len(side) == 0: 

648 continue 

649 self.layout[side[0]]["_overlaying"] = "" 

650 for i in range(1, len(side)): 

651 self.layout[side[i]]["_overlaying"] = side[i - 1] 

652 

653 axes_order += left 

654 axes_order += right 

655 

656 other_axes = set(axes) - set(axes_order) 

657 

658 axes_order = sorted(list(other_axes)) + axes_order 

659 

660 for axis in axes_order: 

661 if "linecolor" not in self.layout[axis]: 

662 continue 

663 overlaying_axis = ( 

664 self.layout[axis]["_overlaying"] 

665 if "_overlaying" in self.layout[axis] 

666 else "" 

667 ) 

668 original_overlaying_axis = ( 

669 self.layout[axis]["overlaying"][0] + "axis" + self.layout[axis]["overlaying"][1:] 

670 if "overlaying" in self.layout[axis] 

671 else "" 

672 ) 

673 anchor_axis = ( 

674 "free" 

675 if "anchor" not in self.layout[axis] 

676 or self.layout[axis]["anchor"] == "free" 

677 else ( 

678 self.layout[axis]["anchor"][0] 

679 + "axis" 

680 + self.layout[axis]["anchor"][1:] 

681 ) 

682 ) 

683 domain = ( 

684 self.layout[axis]["domain"] 

685 if "overlaying" not in self.layout[axis] 

686 or original_overlaying_axis == "" 

687 else self.layout[original_overlaying_axis]["domain"] 

688 ) 

689 

690 position = ( 

691 self.layout[overlaying_axis]["_position"] 

692 if "autoshift" in self.layout[axis] 

693 and self.layout[axis]["autoshift"] 

694 and anchor_axis == "free" 

695 else ( 

696 self.layout[axis]["position"] 

697 if "anchor" not in self.layout[axis] or anchor_axis == "free" 

698 else ( 

699 self.layout[anchor_axis]["domain"][0] 

700 if self.layout[axis]["side"] == "left" 

701 or self.layout[axis]["side"] == "bottom" 

702 else self.layout[anchor_axis]["domain"][-1] 

703 ) 

704 ) 

705 ) 

706 self.layout[axis]["_domain"] = domain 

707 self.layout[axis]["_position"] = position 

708 

709 if "autoshift" in self.layout[axis] and self.layout[axis]["autoshift"]: 

710 shift = ( 

711 self.layout[axis]["shift"] 

712 if "shift" in self.layout[axis] 

713 else 3 if self.layout[axis]["side"] == "right" else -3 

714 ) 

715 font_extra = 0 

716 tickfont = update_dict( 

717 self.layout["font"], self.layout[axis]["tickfont"] 

718 ) 

719 tickfont = parse_font(tickfont) 

720 ctx = self.get_pango_context() 

721 layout = Pango.Layout(ctx) 

722 layout.set_font_description(tickfont) 

723 

724 metrics = ctx.get_metrics(tickfont) 

725 font_height = ( 

726 metrics.get_ascent() + metrics.get_descent() 

727 ) / Pango.SCALE 

728 

729 for tick in self.layout[overlaying_axis]["_ticktext"]: 

730 layout.set_text(tick) 

731 font_extra = max(layout.get_pixel_size()[0], font_extra) 

732 autoshift = ( 

733 font_extra 

734 if self.layout[axis]["side"] == "right" 

735 else -font_extra 

736 - self.layout[axis]["title"]["standoff"] 

737 - font_height 

738 ) + self.layout[overlaying_axis]["_shift"] 

739 else: 

740 shift = 0 

741 autoshift = 0 

742 self.layout[axis]["_shift"] = shift + autoshift 

743 

744 @staticmethod 

745 def _detect_axis_type(data): 

746 if any(isinstance(i, list) or isinstance(i, np.ndarray) for i in data): 

747 return "multicategory" 

748 if not isinstance(data, np.ndarray): 

749 data = np.array(data) 

750 

751 length = len(data) 

752 if length >= 1000: 

753 start = np.random.randint(0, length / 1000) 

754 index = np.arange(start, length, length / 1000).astype(np.int32) 

755 data = data[index] 

756 

757 data = set(data) 

758 

759 def to_type(d): 

760 try: 

761 d = np.datetime64(d) 

762 return "date" 

763 except ValueError: 

764 if isinstance(d, numbers.Number): 

765 return "linear" 

766 else: 

767 return "category" 

768 

769 data_types = [to_type(d) for d in data] 

770 data_types = {d: data_types.count(d) for d in set(data_types)} 

771 if len(data_types) == 1: 

772 return list(data_types)[0] 

773 if "linear" not in data_types: 

774 if data_types["date"] > data_types["category"]: 

775 return "date" 

776 return "category" 

777 

778 if "date" in data_types and data_types["date"] > 2 * data_types["linear"]: 

779 return "date" 

780 if ( 

781 "category" in data_types 

782 and data_types["category"] > 2 * data_types["linear"] 

783 ): 

784 return "category" 

785 

786 return "linear" 

787 

788 def _draw_buttons(self): 

789 if "updatemenus" not in self.layout: 

790 return 

791 for updatemenu in self.layout["updatemenus"]: 

792 overlay = UpdateMenu(self, updatemenu) 

793 self.overlays.append(overlay) 

794 self.add_overlay(overlay) 

795 

796 def _draw_legend(self): 

797 legend = self.layout["legend"] 

798 overlay = Legend(self, legend) 

799 self.overlays.append(overlay) 

800 self.add_overlay(overlay) 

801 

802 def _draw_annotations(self): 

803 if "annotations" not in self.layout: 

804 return 

805 for annotation in self.layout["annotations"]: 

806 overlay = Annotation(self, annotation) 

807 self.overlays.append(overlay) 

808 self.add_overlay(overlay) 

809 

810 def _draw_titles(self): 

811 axes = [k for k in self.layout if "axis" in k] 

812 for axis in axes: 

813 if ( 

814 "title" not in self.layout[axis] 

815 or self.layout[axis]["title"] is False 

816 or "text" not in self.layout[axis]["title"] 

817 ): 

818 continue 

819 overlay = AxisTitle(self, self.layout[axis], axis_name=axis) 

820 self.overlays.append(overlay) 

821 self.add_overlay(overlay)