Skip to content

Commit 83cddd5

Browse files
authored
Generate PDF figures (#437)
* Generate PDFs instead of SVG for plotly figures * make pdf optional * try updating pipeline for tests * sudo? * fix orca install * whatsnew * bump test? * electron version maybe? * try fixing orca path * remove orca, skip tests
1 parent 642fad0 commit 83cddd5

File tree

6 files changed

+78
-14
lines changed

6 files changed

+78
-14
lines changed

azure-pipelines.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ jobs:
2323
versionSpec: '$(python.version)'
2424
architecture: 'x64'
2525

26-
- task: Npm@1
27-
inputs:
28-
command: 'custom'
29-
customCommand: 'install phantomjs-prebuilt'
30-
displayName: 'Install phantomjs'
31-
3226
- script: python -m pip install --upgrade pip && pip install -r requirements.txt -r requirements-test.txt
3327
displayName: 'Install dependencies'
3428

docs/source/whatsnew/1.0.0rc1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Enhancements
2121
* Datamodel now supports ``'net_load'`` as an allowed variable. (:issue:`55`) (:pull:`392`)
2222
* Posting of daily validation now splits requests to avoid missing periods and
2323
limit each request to one week of data (:issue:`424`) (:pull:`435`)
24+
* PDF report figures are generated instead of SVG for easy integration into PDF
25+
reports (:issue:`360`) (:pull:`437`)
2426

2527

2628
Bug fixes

solarforecastarbiter/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,7 +2001,7 @@ def ser(interval_length):
20012001
{
20022002
'name': 'mae tucson ghi',
20032003
'spec': '{"data":[{"x":[1],"y":[1],"type":"bar"}]}',
2004-
'svg': '<svg></svg>',
2004+
'pdf': ',u@!!/MSk7$7-ue:IY',
20052005
'figure_type': 'bar',
20062006
'category': 'total',
20072007
'metric': 'mae',
@@ -2363,7 +2363,7 @@ def plotly_report_figure_dict():
23632363
return {
23642364
'name': 'mae tucson ghi',
23652365
'spec': '{"data":[{"x":[1],"y":[1],"type":"bar"}]}',
2366-
'svg': '<svg></svg>',
2366+
'pdf': ',u@!!/MSk7$7-ue:IY',
23672367
'figure_type': 'bar',
23682368
'category': 'total',
23692369
'metric': 'mae',
@@ -2435,7 +2435,7 @@ def raw_report_dict_with_event():
24352435
'metric': 'pod',
24362436
'name': 'all',
24372437
'spec': "{}",
2438-
'svg': '<svg></svg>'}],
2438+
'pdf': ',u@!!/MSk7$7-ue:IY'}],
24392439
'plotly_version': '4.5.3',
24402440
'script': None},
24412441
'processed_forecasts_observations': [{

solarforecastarbiter/datamodel.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,19 +1418,22 @@ class PlotlyReportFigure(ReportFigure):
14181418
A descriptive name for the figure.
14191419
spec: str
14201420
JSON string representation of the plotly plot.
1421-
svg: str
1422-
A static svg copy of the plot, for including in the pdf version.
14231421
figure_type: str
14241422
The type of plot, e.g. bar or scatter.
1423+
pdf: str
1424+
A static PDF copy of the plot, for including in PDF reports.
1425+
svg: str
1426+
DEPRECATED for pdf. A static svg copy of the plot.
14251427
category: str
14261428
The metric category. One of ALLOWED_CATEGORIES keys.
14271429
metric: str
14281430
The metric being plotted.
14291431
"""
14301432
name: str
14311433
spec: str
1432-
svg: str
14331434
figure_type: str
1435+
pdf: str = ''
1436+
svg: str = ''
14341437
category: str = ''
14351438
metric: str = ''
14361439
figure_class: str = 'plotly'

solarforecastarbiter/reports/figures/plotly_figures.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Functions to make all of the metrics figures for Solar Forecast Arbiter reports
33
using Plotly.
44
"""
5+
import base64
56
import calendar
67
import datetime as dt
78
from itertools import cycle
@@ -761,6 +762,39 @@ def output_svg(fig):
761762
return svg
762763

763764

765+
def output_pdf(fig):
766+
"""
767+
Generates an PDF from the Plotly figure. Errors in the process are logged
768+
and an PDF with error text is returned.
769+
770+
Parameters
771+
----------
772+
fig : plotly.graph_objects.Figure
773+
774+
Returns
775+
-------
776+
pdf : str
777+
An ASCII-85 encoded PDF
778+
779+
Notes
780+
-----
781+
Requires `Orca <https://plot.ly/python/orca-management/>`_ for generating
782+
pdfs. If orca is not installed, an pdf with an error message will be
783+
returned.
784+
"""
785+
try:
786+
pdf = base64.a85encode(fig.to_image(format='pdf')).decode('utf-8')
787+
except Exception:
788+
try:
789+
name = fig.layout.title['text'][3:-4]
790+
except Exception:
791+
name = 'unnamed'
792+
logger.error('Could not generate PDF for figure %s', name)
793+
# should have same text as fail SVG
794+
pdf = ',u@!!/MSk8$73+IY58P_+>=pV@VQ644<Q:NASu.&BHT/T0Ha7#+<Vd[7VQ[\\ATAnH7VlLTAOL*>De*Dd5!B<pFE1r$D$kNX1K6%.6<uqiV.X\\GOIXKoa;)c"!&3^A=pehYA92j5ARTE_ASu$s@VQ6-+>=pV@VQ5m+<WEu$>"*cDdmGg1E\\@oDdmGg4?Ns74pkk=A8bpl$8N_X+E(_($9UEn03!49AKWX&@:s-o,p4oL+<Vd[:gnBUDKI!U+>=p9$6UH6026"gBjj>HGT^350H`%l0J5:A+>>E,2\'?03+<Vd[6Z6jaASuU2+>b2p+ArOh+<W=-Ec6)>+?Van+<VdL+<W=:H#R=;01U&$F`7[1+<VdL+>6Y902ut#DKBc*Eb0,uGmYZ:+<VdL01d:.Eckq#+<VdL+<W=);]m_]AThctAPu#b$6UH65!B;r+<W=8ATMd4Ear[%+>Y,o+ArP14pkk=A8bpl$8EYW+E(_($9UEn03!49AKWX&@:s.m$6UH602$"iF!+[01*A7n;BT6P+<Vd[6Z7*bF<E:F5!B<bDIdZpC\'ljA0Hb:CC\'m\'c+>6Q3De+!#ATAnA@ps(lD]gbe0fCX<+=LoFFDu:^0/$gDBl\\-)Ea`p#Bk)3:DfTJ>.1.1?+>6*&ART[pDf.sOFCcRC6om(W1,(C>0K1^?0ebFC/MK+20JFp_5!B<bDIdZpC\'lmB0Hb:CC\'m\'c+>6]>E+L.F6Xb(FCi<qn+<Vd[:gn!JF!*1[0Ha7#5!B<bDIdZpC\'o3+AS)9\'+?0]^0JG170JG170H`822)@*4AfqF70JG170JG:B0d&/(0JG1\'DBK9?0JG170JG4>0d&/(0JG1\'DBK9?0JG170JG4<0H`&\'0JG1\'DBK9?0JG170JG182\'=S,0JG1\'DBK9?0JG170JG493?U"00JG1\'DBK9?0JG170JG=?2BX\\-0JG1\'DBK9?0JG170JG@B1*A8)0JG1\'DBK:.Ea`ZuATA,?4<Q:UBmO>53!pcN+>6W2Dfd*\\+>=p9$6UH601g%nD]gq\\0Ha7#5!B<pFCB33G]IA-$8sUq$7-ue:IYZ' # NOQA
795+
return pdf
796+
797+
764798
def raw_report_plots(report, metrics):
765799
"""Create a RawReportPlots object from the metrics of a report.
766800
@@ -791,10 +825,10 @@ def raw_report_plots(report, metrics):
791825
for k, v in figure_dict.items():
792826
cat, met, name = k.split('::', 2)
793827
figure_spec = v.to_json()
794-
svg = output_svg(v)
828+
pdf = output_pdf(v)
795829
mplots.append(datamodel.PlotlyReportFigure(
796830
name=name, category=cat, metric=met, spec=figure_spec,
797-
svg=svg, figure_type='bar'))
831+
pdf=pdf, figure_type='bar'))
798832

799833
out = datamodel.RawReportPlots(tuple(mplots), plotly_version)
800834
return out

solarforecastarbiter/reports/figures/tests/test_plotly_figures.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import shutil
23

34

@@ -164,6 +165,8 @@ def test_output_svg_with_plotly_figure(mocker):
164165
'solarforecastarbiter.reports.figures.plotly_figures.logger')
165166
if shutil.which('orca') is None: # pragma: no cover
166167
pytest.skip('orca must be on PATH to make SVGs')
168+
if shutil.which('xvfb-run') is None: # pragma: no cover
169+
pytest.skip('xvfb-run must be on PATH to make SVGs')
167170
values = list(range(5))
168171
fig = graph_objects.Figure(data=graph_objects.Scatter(x=values, y=values))
169172
svg = figures.output_svg(fig)
@@ -172,6 +175,22 @@ def test_output_svg_with_plotly_figure(mocker):
172175
assert not logger.error.called
173176

174177

178+
def test_output_pdf_with_plotly_figure(mocker):
179+
logger = mocker.patch(
180+
'solarforecastarbiter.reports.figures.plotly_figures.logger')
181+
if shutil.which('orca') is None: # pragma: no cover
182+
pytest.skip('orca must be on PATH to make PDFs')
183+
if shutil.which('xvfb-run') is None: # pragma: no cover
184+
pytest.skip('xvfb-run must be on PATH to make PDFs')
185+
values = list(range(5))
186+
fig = graph_objects.Figure(data=graph_objects.Scatter(x=values, y=values))
187+
pdf = figures.output_pdf(fig)
188+
pdf_bytes = base64.a85decode(pdf)
189+
assert pdf_bytes.startswith(b'%PDF-')
190+
assert pdf_bytes.rstrip(b'\n').endswith(b'%%EOF')
191+
assert not logger.error.called
192+
193+
175194
@pytest.fixture(scope='function')
176195
def remove_orca():
177196
import plotly.io as pio
@@ -190,6 +209,18 @@ def test_output_svg_with_plotly_figure_no_orca(mocker, remove_orca):
190209
assert logger.error.called
191210

192211

212+
def test_output_pdf_with_plotly_figure_no_orca(mocker, remove_orca):
213+
logger = mocker.patch(
214+
'solarforecastarbiter.reports.figures.plotly_figures.logger')
215+
values = list(range(5))
216+
fig = graph_objects.Figure(data=graph_objects.Scatter(x=values, y=values))
217+
pdf = figures.output_pdf(fig)
218+
pdf_bytes = base64.a85decode(pdf)
219+
assert pdf_bytes.startswith(b'%PDF-')
220+
assert pdf_bytes.rstrip(b'\n').endswith(b'%%EOF')
221+
assert logger.error.called
222+
223+
193224
@pytest.fixture()
194225
def metric_dataframe():
195226
return pd.DataFrame({

0 commit comments

Comments
 (0)