modelflowib 2.73__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modelreport.py ADDED
@@ -0,0 +1,2187 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Mon Apr 8 14:58:26 2024
4
+
5
+ @author: ibhan
6
+
7
+ The `modelreport` module facilitates the generation, management, and display of data visualizations and tables
8
+ derived from ModelFlow models. It is designed to support a wide range of output formats including LaTeX documents,
9
+ HTML, and interactive IPyWidgets, making it highly versatile for different reporting and analysis needs in Jupyter
10
+ notebook environments.
11
+
12
+ This module provides a structured and extendible approach to organizing and rendering data analysis results, enabling users to
13
+ easily convert complex model outputs into digestible visual representations and tables. It is especially useful
14
+ for automating the reporting process in data analysis, financial modeling, and research projects, leveraging Python's
15
+ capabilities for data processing and visualization along with advanced document presentation features.
16
+
17
+ Key Features:
18
+
19
+ - Dynamic generation of LaTeX and HTML content for integrating data visualizations and tables into reports and
20
+ presentations.
21
+ - Compatibility with IPyWidgets for creating interactive, widget-based displays that enhance the interactivity of
22
+ Jupyter notebooks.
23
+ - Seamless integration with matplotlib for figure generation and pandas for table formatting, providing a comprehensive
24
+ toolkit for data display.
25
+ - Customizable display options and specifications through dataclasses, allowing for tailored presentation styles
26
+ and formats.
27
+
28
+ Classes:
29
+
30
+ - :class:`Options`: Configures display options for managing how data and figures are presented, including naming conventions,
31
+ formatting preferences, and title settings.
32
+ - :class:`Line`: Defines line configurations for table displays, supporting various data representation and difference
33
+ calculations to suit different analysis needs.
34
+ - :class:`DisplaySpec`: Groups display options and line configurations, facilitating the management of complex display setups
35
+ in a structured manner.
36
+ - :class:`DisplayDef`: Base class for display definitions, capable of compiling various display components into cohesive
37
+ specifications for rendering.
38
+ - :class:`LatexRepo`: Handles the generation of LaTeX content, compilation into PDFs, and embedding within Jupyter notebooks,
39
+ supporting both static and dynamic content creation.
40
+ - :class:`DisplayVarTableDef`: Specializes in displaying variable tables, automating the creation and formatting of tables
41
+ from ModelFlow model outputs.
42
+ - :class:`DisplayFigWrapDef`: Focuses on wrapping and adjusting matplotlib figures for inclusion in various display formats,
43
+ ensuring figures are presentation-ready.
44
+ - :class:`SplitTextResult`: Parses a string containing text with embedded <html>, <latex>, and <markdown> tags and separates
45
+ the content accordingly.
46
+ - :class:`DatatypeAccessor`: Manages configurations for different datatypes, allowing for easy access and parsing of
47
+ configuration tables provided in Markdown format.
48
+
49
+
50
+ The `modeldisplay` module bridges the gap between analytical modeling and result presentation, offering a streamlined
51
+ workflow for transforming ModelFlow model outputs into high-quality visual and tabular displays suitable for a wide
52
+ range of purposes.
53
+ """
54
+
55
+
56
+
57
+
58
+
59
+ import pandas as pd
60
+ import matplotlib.pyplot as plt
61
+ import matplotlib as mpl
62
+ import seaborn as sns
63
+ import fnmatch
64
+ import re
65
+ from matplotlib import dates
66
+ import matplotlib.ticker as ticker
67
+ import matplotlib.gridspec as gridspec
68
+ from matplotlib.figure import Figure
69
+ import numpy as np
70
+
71
+ from dataclasses import dataclass, field, fields, asdict
72
+ from typing import Any, List, Dict, Optional , Tuple
73
+ from copy import deepcopy
74
+ import json
75
+ import ipywidgets as widgets
76
+ from io import StringIO
77
+
78
+ from IPython.display import display, clear_output,Latex, Markdown,HTML , IFrame
79
+
80
+
81
+
82
+
83
+
84
+ from subprocess import run
85
+ from pathlib import Path
86
+ import webbrowser as wb
87
+
88
+ WIDTH = '100%'
89
+ HEIGHT = '400px'
90
+
91
+
92
+ from dataclasses import dataclass, field, fields, MISSING
93
+ from typing import Dict, List
94
+ from copy import deepcopy
95
+
96
+
97
+ from modelwidget import fig_to_image,tabwidget,htmlwidget_fig, htmlwidget_df,htmlwidget_text,htmlwidget_style
98
+
99
+ def track_fields():
100
+ """
101
+ A decorator to track which fields in a dataclass have been explicitly set during initialization.
102
+
103
+ This decorator modifies the `__init__` method of a dataclass to keep track of which fields
104
+ have been explicitly set by the user when an instance is created. The explicitly set fields
105
+ are stored in a set called `__explicitly_set__`.
106
+
107
+ Returns:
108
+ wrap (function): A decorator function that modifies the dataclass to track explicitly set fields.
109
+
110
+ """
111
+ def wrap(cls):
112
+ cls.__original_init = cls.__init__
113
+ def __init__(self, *args, **kw):
114
+ kw.update(dict(zip(cls.__dataclass_fields__.keys(), args)))
115
+ self.__explicitly_set__ = set(kw.keys())
116
+ kw = {k: v for k, v in kw.items() if k in cls.__dataclass_fields__}
117
+ self.__original_init(**kw)
118
+ cls.__init__ = __init__
119
+ return cls
120
+ return wrap
121
+
122
+ class DatatypeAccessor:
123
+ def __init__(self, datatype, **kwargs):
124
+ """
125
+ Initializes the ConfigAccessor with a datatype and a configuration table in Markdown format.
126
+
127
+ :param datatype: A string keyword to fetch configuration for.
128
+ :param config_table: A string representing the configuration in Markdown table format.
129
+ """
130
+
131
+ self.datatype = datatype
132
+
133
+ # {var_name}
134
+ # {var_description}
135
+ # {compare} Keep name used for comparison
136
+
137
+ if 'config_table' in kwargs:
138
+ config_table = kwargs.get('config_table')
139
+ else:
140
+
141
+ config_table = r"""
142
+ | datatype | showtype | diftype | col_desc | ax_title_template|
143
+ |-----------|----------|---------|------------------|------------|
144
+ | growth | growth | nodif | Percent growth | {var_description} \n% growth |
145
+ | difgrowth | growth | dif | Impact, Percent growth | {var_description} \nImpact, % growth v|
146
+ | gdppct | gdppct | nodif | Percent of GDP | {var_description} \n(% GDP) |
147
+ | difgdppct | gdppct | dif | Impact, Percent of GDP | {var_description} \nImpact (% GDP) vs {compare} |
148
+ | level | level | nodif | Level | {var_description} |
149
+ | diflevel | level | dif | Impact, Level | {var_description} \nChange vs {compare} |
150
+ | difpctlevel| level | difpct | Impact in percent | {var_description} \n% Change vs {compare} |
151
+ | qoq_ar | qoq_ar | nodif | Q-Q anuallized | {var_description} \nQ-Q annualized |
152
+ | difqoq_ar | qoq_ar | dif | Impact Q-Q anuallized | {var_description} \nImpact Q-Q annualized vs {compare} |
153
+ | baselevel | level | basedf | Base Level | {var_description} \nBase Level |
154
+ | basegrowth | growth | basedf | Base Percent growth | {var_description} \nBase % growth |
155
+ | basegdppct | gdppct | basedf | Base Percent of GDP | {var_description} \nBase % of GDP |
156
+ | baseqoq_ar | qoq_ar | basedf | Base Q-Q anuallized | {var_description} \nBase Q-Q annualized |
157
+
158
+ """
159
+
160
+
161
+ config_table = r"""
162
+ | datatype | showtype | diftype | col_desc | ax_title_template | ax_title_template_df |
163
+ |--------------|-----------|---------|---------------------------|-------------------------------------------------|------------------------------------------------|
164
+ | growth | growth | nodif | Percent growth | {var_description} \n% growth | {var_description} \n% growth |
165
+ | difgrowth | growth | dif | Impact, Percent growth | {var_description} \nImpact, % growth vs {compare} | {var_description} \nImpact, % |
166
+ | gdppct | gdppct | nodif | Percent of GDP | {var_description} \n(% GDP) | {var_description} \n(% GDP) |
167
+ | difgdppct | gdppct | dif | Impact, Percent of GDP | {var_description} \nImpact (% GDP) vs {compare} | {var_description} \nImpact (% GDP) |
168
+ | level | level | nodif | Level | {var_description} | {var_description} |
169
+ | diflevel | level | dif | Impact, Level | {var_description} \nChange vs {compare} | {var_description} \nChange |
170
+ | difpctlevel | level | difpct | Impact in percent | {var_description} \n% Change vs {compare} | {var_description} \n% Change |
171
+ | qoq_ar | qoq_ar | nodif | Q-Q annualized | {var_description} \nQ-Q annualized | {var_description} \nQ-Q annualized |
172
+ | difqoq_ar | qoq_ar | dif | Impact Q-Q annualized | {var_description} \nImpact Q-Q annualized vs {compare} | {var_description} \nImpact Q-Q annualized |
173
+ | baselevel | level | basedf | Baseline | {var_description} \nBaseline | {var_description} \nBaseline Level |
174
+ | basegrowth | growth | basedf | Baseline Percent growth | {var_description} \nBaseline % growth | {var_description} \nBaseline % growth |
175
+ | basegdppct | gdppct | basedf | Baseline Percent of GDP | {var_description} \nBaseline % of GDP | {var_description} \nBaseline % of GDP |
176
+ | baseqoq_ar | qoq_ar | basedf | Baseline Q-Q annualized | {var_description} \nBaseline Q-Q annualized | {var_description} \\nBaseline Q-Q annualized |
177
+
178
+ """
179
+
180
+
181
+ self.configurations = self.parse_config_table(config_table)
182
+ # print(self.configurations)
183
+
184
+ # Apply any overrides from kwargs
185
+ # if datatype in self.configurations:
186
+ # self.configurations[datatype].update((k, v) for k, v in kwargs.items() if k in self.configurations[datatype])
187
+
188
+
189
+ def parse_config_table(self,config_table):
190
+ """
191
+ Parses a Markdown table into a dictionary of configurations.
192
+
193
+ :param config_table: Markdown table as a string.
194
+ :return: Dictionary with datatype keys and property dictionaries as values.
195
+ """
196
+
197
+ lines = config_table.strip().split('\n')
198
+ headers = re.split(r'\s*\|\s*', lines[0].strip('|').strip())
199
+ configs = {}
200
+
201
+ for line in lines[2:]: # Skip the header and delimiter rows
202
+ values = re.split(r'\s*\|\s*', line.strip('|').strip())
203
+ # print(f'{line=}')
204
+ # print(f'{values=}')
205
+ config = {headers[i]: values[i] for i in range(len(values))}
206
+ datatype = config.pop('datatype') # Remove the datatype key to use as the dictionary key
207
+ configs[datatype] = config
208
+
209
+ return configs
210
+
211
+ def __getattr__(self, item):
212
+ """
213
+ Provides dynamic access to configuration properties based on the initial datatype.
214
+
215
+ :param item: The property name to fetch from the configuration for the provided datatype.
216
+ :return: The value associated with 'item' under the specified datatype's configuration.
217
+ """
218
+ config_data = self.configurations.get(self.datatype)
219
+
220
+ if not config_data:
221
+ allowed = '\nAllowed datatypes:'+'\n' + '\n'.join(self.configurations.keys() )
222
+ raise ValueError(f"Configuration for datatype '{self.datatype}' not found"+allowed)
223
+
224
+ return config_data.get(item, '')
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
233
+ @track_fields()
234
+ @dataclass
235
+ class Options:
236
+ """
237
+ Represents configuration options for data display definitions.
238
+
239
+ Args:
240
+ name (str): Name for this display. Default is 'Display'.
241
+ foot (str): Footer if relevant. Default is an empty string.
242
+ rename (bool): If True, allows renaming of data columns. Default is True.
243
+ decorate (bool): If True, decorates row descriptions based on the showtype. Default is True.
244
+ width (int): Specifies the width for formatting output in characters. Default is 20.
245
+ custom_description (Dict[str, str]): Custom descriptions to augment or override default descriptions. Empty by default.
246
+ title (str): Text for the title. Default is an empty string.
247
+ chunk_size (int): Specifies the number of columns per chunk in the display output. Default is 0 (no chunking).
248
+ timeslice (List): Specifies the time slice for data display. Empty by default.
249
+ max_cols (int): Maximum columns when displayed as string. Default is 6.
250
+ last_cols (int): In Latex, specifies the number of last columns to include in a display slice. Default is 1.
251
+ ncol (int): Number of columns in figures. Default is 2.
252
+ samefig (bool): If True, use the same figure for multiple plots. Default is False.
253
+ size (tuple): Tuple specifying the figure size (width, height). Default is (10, 6).
254
+ legend (bool): If True, display legend in plots. Default is True.
255
+ transpose (bool): If True, transpose the data when displaying. Default is False.
256
+ scenarios (str): Text specifying the scenarios for the display. Default is an empty string. if Empty use basedf/lastdf
257
+ smpl (tuple): Tuple specifying start and end periods. Default is ('', '').
258
+ landscape (bool): If True, the table will be displayed in landscape mode. Default is False.
259
+ latex_text (str): Text for a LaTeX output. Default is an empty string.
260
+ html_text (str): Text for an HTML output. Default is an empty string.
261
+ text_text (str): Text for a plain text output. Default is an empty string.
262
+ markdown_text (str): Text for a Markdown output. Default is an empty string.
263
+
264
+
265
+ Methods:
266
+ __add__(other): Merges this Options instance with another 'Options' instance or a dictionary. It returns a new Options
267
+ instance that combines settings from both, preferring non-default values from 'other'. If 'other' is a
268
+ dictionary, attributes not existing in this instance will raise an AttributeError. TypeErrors are raised
269
+ when 'other' is neither a dictionary nor an Options instance.
270
+
271
+
272
+ """
273
+ name: str = 'Display'
274
+ foot: str = ''
275
+ rename: bool = True
276
+ decorate: bool = True
277
+ width: int = 20
278
+ custom_description: Dict[str, str] = field(default_factory=dict)
279
+ title: str = ''
280
+ chunk_size: int = 0
281
+ timeslice: List = field(default_factory=list)
282
+ max_cols: int = 6
283
+ last_cols: int = 3
284
+
285
+ ncol : int = 2
286
+ samefig :bool = False
287
+ size: tuple = (10, 6)
288
+ legend: bool = True
289
+ transpose : bool = False
290
+ scenarios : str =''
291
+ smpl : tuple = ('','')
292
+ landscape : bool = False
293
+
294
+ latex_text :str =''
295
+ html_text :str =''
296
+ text_text :str =''
297
+ markdown_text :str =''
298
+
299
+ def __post_init__(self):
300
+
301
+ if ' ' in self.name:
302
+ self.name = self.name.replace(' ','_')
303
+ # print(f'Blank space is not allowed in name, renamed: {self.name}')
304
+
305
+
306
+ def was_explicitly_set(self, field_name: str) -> bool:
307
+ # Using getattr with three arguments to avoid KeyError and return False when the attribute isn't found
308
+ return getattr(self, '__explicitly_set__', set()).__contains__(field_name)
309
+
310
+
311
+ def __add__(self, other):
312
+ if not isinstance(other, (Options, dict)):
313
+ raise TypeError("Operand must be an instance of Options or dict.")
314
+
315
+ # Create a new instance by deeply copying the current one
316
+ new_instance = deepcopy(self)
317
+
318
+ # Get default values for comparison
319
+ default_values = {f.name: f.default if f.default is not MISSING else f.default_factory()
320
+ for f in fields(self)}
321
+
322
+ def is_explicitly_set(attr, value, default):
323
+ return value != default or attr in other # Check if the attribute is present in 'other'
324
+
325
+
326
+ if isinstance(other, dict):
327
+ # Update using dictionary
328
+ for key, value in other.items():
329
+ if hasattr(new_instance, key):
330
+ setattr(new_instance, key, value)
331
+ else:
332
+ ...
333
+ # raise AttributeError(f"No such attribute {key} in Options class.")
334
+ elif isinstance(other, Options):
335
+ # Update using another Options instance, but only for non-default values
336
+ for key, value in vars(other).items():
337
+ if other.was_explicitly_set(key):
338
+ setattr(new_instance, key, value)
339
+
340
+ return new_instance
341
+
342
+
343
+
344
+
345
+
346
+ @dataclass
347
+ class Line:
348
+ """
349
+ A dataclass for representing and validating line configurations for data display.
350
+
351
+ Attributes:
352
+ datatype (str): Specifies the datatype as defines in DatatypeAccessor
353
+ textlinetype (str): = 'textline' if the line is a textline in a table
354
+ centertext (str): Center text used when showtype is 'textline'. Default is a space.
355
+ rename (bool): If True, allows renaming of data columns. Default is True.
356
+ dec (int): Specifies the number of decimal places to use for numerical output. Default is 2.
357
+ pat (str): Pattern or identifier used to select data for the line. Default is '#Headline'.
358
+ latexfont (str) : Modifier used in lates for instande r'\textbf'
359
+ default_ax_title_template(str) Table specific template for individual chart titles
360
+ ax_title_template(str) user provided Table specific template for individual chart titles
361
+ """
362
+
363
+ datatype :str = 'level'
364
+ scale : str = 'linear'
365
+ kind : str = 'line'
366
+ centertext : str = ''
367
+ rename: bool = False
368
+ dec: int = 2
369
+ pat : str = '#Headline'
370
+ latexfont :str =''
371
+ by_var :bool = True
372
+ mul : float = 1.0
373
+ yunit : str = ''
374
+ datatype_desc :str = ''
375
+ ax_title_template :str = ''
376
+ textlinetype :str = ''
377
+
378
+ # default_ax_title_template :str = field(init=False)
379
+ # default_ax_title_template_df :str = field(init=False)
380
+ # showtype: str = field(init=False)
381
+ # diftype: str = field(init=False)
382
+
383
+
384
+ def __post_init__(self):
385
+
386
+ config = DatatypeAccessor(self.datatype)
387
+ self.showtype = config.showtype
388
+ self.diftype = config.diftype
389
+ self.default_ax_title_template = config.ax_title_template
390
+ self.default_ax_title_template_df = config.ax_title_template_df
391
+
392
+ valid_showtypes = {'level', 'growth', 'change', 'basedf', 'gdppct' ,'textline','qoq_ar'}
393
+ valid_diftypes = {'nodif', 'dif', 'difpct', 'basedf', 'lastdf'}
394
+
395
+ if self.showtype not in valid_showtypes:
396
+ raise ValueError(f"showtype must be one of {valid_showtypes}, got {self.showtype}")
397
+
398
+ if self.diftype not in valid_diftypes:
399
+ raise ValueError(f"diftype must be one of {valid_diftypes}, got {self.diftype}")
400
+
401
+
402
+ @dataclass
403
+ class DisplaySpec:
404
+ """
405
+ A dataclass to encapsulate display specifications including options and a list of line configurations.
406
+
407
+ Attributes:
408
+ options (Options): An instance of the Options dataclass specifying configuration options.
409
+ lines (List[Line]): A list of Line instances specifying individual line configurations.
410
+ """
411
+ display_type : str = ''
412
+ options: Options = field(default_factory=Options)
413
+ lines: List[Line] = field(default_factory=list)
414
+
415
+ def __add__(self, other):
416
+
417
+ if isinstance(other, DisplaySpec):
418
+ new_options = self.options + other.options
419
+ new_lines = self.lines + other.lines # extends the list with other's lines
420
+ elif isinstance(other, Options):
421
+ new_options = self.options + other # update options
422
+ new_lines = self.lines
423
+ elif isinstance(other, dict):
424
+ new_options = self.options + other # update options
425
+ new_lines = self.lines
426
+ elif isinstance(other, Line):
427
+ new_lines = self.lines + [other] # creates a new list with the added line
428
+ elif isinstance(other, list) and all(isinstance(line, Line) for line in other):
429
+ new_lines = self.lines + other # extends the list with the new lines
430
+ else:
431
+ raise TypeError("Operand must be an instance of DisplaySpec, Options, dict, Line, or list of Line instances.")
432
+
433
+ return DisplaySpec(options=new_options, lines=new_lines)
434
+
435
+
436
+ @classmethod
437
+ def from_json(cls, json_str: str) -> 'DisplaySpec':
438
+ """
439
+ Creates a DisplaySpec instance from a JSON string using the class that called this method,
440
+ which supports use by subclasses.
441
+
442
+ Args:
443
+ json_str (str): A JSON string representation of a DisplaySpec instance.
444
+
445
+ Returns:
446
+ DisplaySpec: The constructed DisplaySpec instance (or an instance of a subclass).
447
+ """
448
+ data = json.loads(json_str)
449
+ options_data = data['options']
450
+ lines_data = data['lines']
451
+
452
+ # Create Options instance from options data
453
+ options = Options(**options_data)
454
+
455
+ # Create list of Line instances from lines data
456
+ lines = [Line(**line_data) for line_data in lines_data]
457
+
458
+ # Return an instance of the calling class, which may be DisplaySpec or any of its subclasses
459
+ return cls(options=options, lines=lines)
460
+
461
+ def to_json(self,display_type) -> str:
462
+ """
463
+ Converts the DisplaySpec instance into a JSON string.
464
+
465
+ Returns:
466
+ str: A JSON string representation of the DisplaySpec instance.
467
+ """
468
+ # Convert DisplaySpec instance to dictionary
469
+
470
+ display_spec_dict = {"display_type":display_type, "options": asdict(self.options), "lines": [asdict(line) for line in self.lines]}
471
+ # print(display_spec_dict)
472
+ # Serialize the dictionary to a JSON string
473
+ return json.dumps(display_spec_dict, indent=4)
474
+
475
+
476
+
477
+
478
+ @dataclass
479
+ class DisplayDef:
480
+ mmodel : Any = None
481
+ spec : DisplaySpec = field(default_factory=DisplaySpec)
482
+ name : str = ''
483
+
484
+
485
+ def __post_init__(self):
486
+ self.options = self.spec.options
487
+ self.lines = self.spec.lines
488
+
489
+ try:
490
+ self.var_description = self.mmodel.defsub(self.mmodel.var_description | self.options.custom_description )
491
+ except:
492
+ self.var_description = {}
493
+
494
+
495
+ self.name = (self.name if self.name else self.options.name).replace(' ','_')
496
+ self.options.name = self.name
497
+ self.timeslice = self.options.timeslice if self.options.timeslice else []
498
+
499
+
500
+ def set_name(self,name):
501
+ self.name = name.replace(' ','_')
502
+ self.options.name = self.name
503
+ return self
504
+
505
+
506
+
507
+ @property
508
+ def get_report_smpl(self):
509
+ if type(self.mmodel.current_per[0]) == np.int64:
510
+ report_smpl = (int(self.mmodel.current_per[0]),int(self.mmodel.current_per[-1]))
511
+ else:
512
+ report_smpl = (str(self.mmodel.current_per[0]),str(self.mmodel.current_per[-1]) )
513
+ return report_smpl
514
+
515
+
516
+ @property
517
+ def save_spec(self):
518
+ display_type = self.__class__.__name__
519
+ new_spec = self.spec + Options(smpl = self.report_smpl)
520
+ # print(f"\n{Options(smpl = self.report_smpl)=}")
521
+ # print(f"\n{new_spec=}")
522
+ # print(f"\n{new_spec=}")
523
+ # out = self.spec.to_json(display_type)
524
+ out = new_spec.to_json(display_type)
525
+ return out
526
+
527
+ def get_rowdes(self,df,line,row=True):
528
+ thisdf = df.copy() if row else df.copy().T
529
+ if self.options.rename or line.rename:
530
+ rowdes = [self.var_description[v] for v in thisdf.index]
531
+ else:
532
+ rowdes = [v for v in thisdf.index]
533
+
534
+ if self.options.decorate :
535
+ match line.showtype:
536
+ case 'growth':
537
+ rowdes = [f'{des}, % growth' for des in rowdes]
538
+
539
+ case 'gdppct':
540
+ rowdes = [f'{des.split(",")[0].split("mill")[0]}, % of GDP' for des in rowdes]
541
+
542
+
543
+ case _:
544
+ rowdes = rowdes
545
+ # print(f'get_rowdes {line=}')
546
+ thisdf.index = rowdes
547
+ dfout = thisdf if row else thisdf.T
548
+ return dfout
549
+
550
+
551
+
552
+
553
+
554
+
555
+
556
+ def pdf(self,pdfopen=False,show=True,width=WIDTH,height=HEIGHT,typesetter='xelatex -interaction=batchmode -no-shell-escape'):
557
+ try:
558
+ repo = LatexRepo(self.latex ,name=self.name)
559
+ return repo.pdf(pdfopen,show,width,height,typesetter)
560
+ except Exception as e:
561
+ print(f"⚠️ Warning: PDF generation failed: {e}")
562
+ return None
563
+
564
+ def __add__(self, other):
565
+ """
566
+ Combines two DisplayDef instances into a new DisplayDef with combined specifications.
567
+
568
+ :param other: Another DisplayDef instance to add.
569
+ :return: A new DisplayDef instance with merged specifications.
570
+ """
571
+ if isinstance(other, str):
572
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= [self] + [get_DisplayTextDef(self.mmodel,other)])
573
+
574
+ else:
575
+
576
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= [self,other])
577
+
578
+ # Create a new DisplayDef with the combined specifications
579
+ return out
580
+
581
+ def __radd__(self, other):
582
+ """
583
+ Combines two DisplayDef instances into a new DisplayDef with combined specifications.
584
+
585
+ :param other: Another DisplayDef instance to add.
586
+ :return: A new DisplayDef instance with merged specifications.
587
+ """
588
+ if isinstance(other, str):
589
+ # If the left-hand side operand is a string, this method will be called
590
+ linstance = get_DisplayTextDef(self.mmodel,input_string = other)
591
+
592
+
593
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= [linstance,self])
594
+
595
+ elif isinstance(other, int):
596
+ # used in sums
597
+ return self
598
+
599
+ else:
600
+ return NotImplemented
601
+
602
+
603
+ # out = DisplayContainerDef(mmodel=self.mmodel,reports= [linstance,self])
604
+
605
+ # Create a new DisplayDef with the combined specifications
606
+ return out
607
+
608
+
609
+
610
+
611
+ def __or__(self, other):
612
+ """
613
+ Combines two DisplayDef instances into a new DisplayDef with combined specifications.
614
+
615
+ :param other: Another DisplayDef instance to add.
616
+ :return: A new DisplayDef instance with merged specifications.
617
+ """
618
+ if not isinstance(other, self.__class__):
619
+ return NotImplemented
620
+
621
+ # Combine options using the existing __add__ method of DisplaySpec
622
+ new_spec = self.spec + other.spec
623
+ # print(f'{self.spec=}')
624
+ # print(f'{new_spec=}')
625
+ # Merge names if they differ, separated by a comma
626
+ new_name = self.name if self.name == other.name else f"{self.name}_{other.name}"
627
+ # print(f'{new_name=}')
628
+
629
+ # Create a new DisplayDef with the combined specifications
630
+ return self.__class__(mmodel=self.mmodel, spec=new_spec, name=new_name)
631
+
632
+ def __and__(self, other):
633
+ if hasattr(other,'latex'):
634
+ # If the other object is an instance of LaTeXHolder, concatenate their LaTeX strings
635
+ return LatexRepo(latex =self.latex + '\n' + other.latex,name=self.name)
636
+ elif isinstance(other, str):
637
+ # If the other object is a string, assume it's a raw LaTeX string and concatenate
638
+ return LatexRepo(latex = self.latex + '\n' + DisplayLatexDef(DisplaySpec(options = Options(latex_text=other,name=self.name))).latex )
639
+ else:
640
+ # If the other object is neither a LaTeXHolder instance nor a string, raise an error
641
+ raise ValueError("Can only add another LaTeXHolder instance or a raw LaTeX string.")
642
+
643
+ def __rand__(self, other):
644
+ if isinstance(other, str):
645
+ # If the left-hand side operand is a string, this method will be called
646
+ return LatexRepo(latex = DisplayLatexDef(spec = DisplaySpec(options = Options(latex_text=other,name=self.name))).latex + '\n' + self.latex)
647
+ else:
648
+ # Handling unexpected types gracefully
649
+ raise ValueError("Left operand must be a string for LaTeX concatenation.")
650
+
651
+
652
+
653
+
654
+
655
+ def _ipython_display_(self):
656
+ display(self.out_html)
657
+
658
+
659
+
660
+ def set_options(self,**kwargs):
661
+ spec = self.spec + kwargs
662
+ try:
663
+ #We want to keep the smpl which shere used originaly if it is a tab
664
+ with self.mmodel.set_smpl(self.df.columns[0],self.df.columns[-1]):
665
+ out = self.__class__(mmodel=self.mmodel,spec= spec)
666
+ except:
667
+ out = self.__class__(mmodel=self.mmodel,spec= spec)
668
+
669
+ return out
670
+
671
+ def figwrap(self,chart,pgf=False):
672
+ latex_dir = Path(f'../{self.name}')
673
+
674
+ if pgf:
675
+ out = r'''
676
+ \begin{figure}[htbp]
677
+ \centering
678
+ \resizebox{\textwidth}{!}'''
679
+ out = out + r'{\input{' +fr'"{(latex_dir / chart).as_posix()}.pgf"'+'}}'
680
+ out = out + fr'''
681
+ \caption{{{self.titledic[chart]}}}
682
+ \end{{figure}} '''
683
+ else: # for pandoc and word
684
+ out = r'''
685
+ \begin{figure}[htbp]
686
+ \centering
687
+ '''
688
+ out = out + r'\includegraphics[width=\textwidth]{' +fr'"{(latex_dir / chart).as_posix()}.png"'+'}'
689
+ caption = self.titledic[chart].replace("_",r'\_').replace('%',r'\%')
690
+ out = out + fr'''
691
+ \caption{{{caption}}}
692
+ \end{{figure}} '''
693
+
694
+
695
+
696
+ return out
697
+
698
+
699
+
700
+
701
+ @dataclass
702
+ class LatexRepo:
703
+ latex: str = ""
704
+ name : str ='latex_test'
705
+
706
+ def set_name(self,name):
707
+ self.name = name.replace(' ','_')
708
+ return self
709
+
710
+ def latexwrap(self):
711
+
712
+ latex_pre = r'''\documentclass{article}
713
+ \usepackage{booktabs}
714
+ \usepackage{caption} % Include the caption package
715
+ \captionsetup{justification=raggedright,singlelinecheck=false}
716
+ \usepackage{graphicx}
717
+ \usepackage{pgf}
718
+ \usepackage{lscape}
719
+ \usepackage{amsmath,amssymb}
720
+ \usepackage{placeins} % for \FloatBarrier
721
+
722
+
723
+ \begin{document}
724
+
725
+ '''
726
+
727
+ latex_post = r'''
728
+ \end{document}
729
+ '''
730
+ out = latex_pre + self.latex + latex_post
731
+ return out
732
+
733
+
734
+
735
+ def pdf(self,pdfopen=False,show=True,width=WIDTH,height=HEIGHT,
736
+ typesetter='xelatex -interaction=batchmode -no-shell-escape '):
737
+
738
+ """
739
+ Generates a PDF file from the LaTeX content and optionally displays it.
740
+
741
+ This method creates a directory for LaTeX files, writes the LaTeX content
742
+ to a `.tex` file, and uses a specified typesetter to compile it into a PDF.
743
+ The resulting PDF can be displayed in an `IFrame` or opened in the default
744
+ PDF viewer.
745
+
746
+ Requires that miktex or another appropiate latex framework is installed.
747
+
748
+ Inspect the latex source by specifying: typesetter='texworks'
749
+
750
+ The files are located in the folder called latex/{name}
751
+
752
+ - The .tex file is called {name.tex}
753
+ - the .pdf file is called {name.pdf}
754
+
755
+ Args:
756
+ pdfopen (bool): If True, opens the generated PDF file in the default viewer.
757
+ show (bool): If True, shows the pdf as a IFrame in Jupyter notebook .
758
+ width (int): The width of the `IFrame` if `show` is True.
759
+ height (int): The height of the `IFrame` if `show` is True.
760
+ typesetter (str): The LaTeX engine to use for compilation (e.g., 'xelatex (default), pdflatex, texworks or latexmk').
761
+
762
+ Returns:
763
+ IFrame: An iframe displaying the PDF file if `show` is True.
764
+
765
+ Raises:
766
+ Exception: If the typesetter returns a non-zero exit code, indicating an error
767
+ in generating the PDF. Opens the directory containing the LaTeX file for inspection.
768
+ """
769
+
770
+
771
+
772
+
773
+ latex_dir = Path(f'latex/{self.name}')
774
+ latex_dir.mkdir(parents=True, exist_ok=True)
775
+
776
+ latex_file = latex_dir / f'{self.name}.tex'
777
+ pdf_file = latex_dir / f'{self.name}.pdf'
778
+
779
+ latex_file.unlink(missing_ok=True)
780
+ pdf_file.unlink(missing_ok=True)
781
+
782
+
783
+
784
+
785
+ # Now open the file for writing within the newly created directory
786
+ with open(latex_file, 'wt', encoding='utf-8') as f:
787
+ f.write(self.latexwrap()) # Assuming tab.fulllatexwidget is the content you want to write
788
+
789
+
790
+ # xx0 = run(f'pdflatex {self.name}.tex' ,cwd = f'{latex_dir}')
791
+ try:
792
+
793
+ xx0 = run(f'{typesetter} {self.name}.tex' ,cwd = f'{latex_dir}', shell=True)
794
+ except FileNotFoundError as e:
795
+ # Catch the FileNotFoundError and print a message
796
+ print(f"Error: {e}")
797
+ print(f'The typesetter "{typesetter}" was not found. Please check the name and file path and try again.')
798
+
799
+ return
800
+ except Exception as e:
801
+ # Catch any other exceptions
802
+ print(f"Preparing PDF file an unexpected error occurred: {e}")
803
+ return
804
+
805
+ # xx0 = run(f'latexmk -pdf -dvi- -ps- -f {self.name}.tex' ,cwd = f'{latex_dir}')
806
+ if xx0.returncode:
807
+ wb.open(latex_dir.absolute(), new=1)
808
+
809
+ raise Exception(f'Error creating PDF file, {xx0.returncode},\n is latex installed?. If so look in the latex file, {latex_file}')
810
+
811
+ if pdfopen:
812
+ fileurl = f'file://{pdf_file.resolve()}'
813
+ # print(fileurl)
814
+ wb.open(fileurl , new=2)
815
+ # from modelhelp import debug_var
816
+ if show:
817
+ # debug_var(show,pdf_file)
818
+ # debug_var(IFrame(pdf_file, width=width, height=height))
819
+
820
+ return IFrame(pdf_file, width=width, height=height)
821
+
822
+
823
+ def __and__(self, other):
824
+
825
+ if isinstance(other,str):
826
+ other_latex = DisplayLatexDef(spec = DisplaySpec(options = Options(latex_text=other,name=self.name))).latex
827
+
828
+ else:
829
+ if hasattr(other,'latex'):
830
+ other_latex= other.latex
831
+ else:
832
+ raise Exception('Trying to join latex from object without latex content ')
833
+ out = LatexRepo(self.latex + other_latex )
834
+ return out
835
+
836
+ def __rand__(self, other):
837
+ if isinstance(other, str):
838
+ # If the left-hand side operand is a string, this method will be called
839
+ return LatexRepo(latex=DisplayLatex(spec = DisplaySpec(options = Options(latex_text=other,name=self.name))).latex + '\n' + self.latex, name=self.name)
840
+ else:
841
+ # Handling unexpected types gracefully
842
+ raise ValueError("Left operand must be a string for LaTeX concatenation.")
843
+
844
+
845
+ def _repr_html_(self):
846
+ self.pdf(show=False)
847
+ pdf_file = f"latex/{self.name}/{self.name}.pdf"
848
+ return f'<iframe src="{pdf_file}" width={WIDTH} height={HEIGHT}></iframe>'
849
+
850
+
851
+ DisplayDef.pdf.__doc__ = LatexRepo.pdf.__doc__
852
+
853
+
854
+ @dataclass
855
+ class DisplayVarTableDef(DisplayDef):
856
+
857
+
858
+ def __post_init__(self):
859
+ super().__post_init__() # Call the parent class's __post_init__
860
+
861
+
862
+ with self.mmodel.set_smpl(*self.options.smpl):
863
+ self.dfs = [self.make_var_df(line).astype('float') for line in self.lines ]
864
+ self.report_smpl = self.get_report_smpl
865
+
866
+ if self.options.transpose:
867
+ self.df = self.dfs[-1].T
868
+ ...
869
+ else:
870
+ self.df = pd.concat( self.dfs )
871
+ # assert 1==2
872
+ return
873
+
874
+ def make_var_df(self, line):
875
+ showtype = line.showtype
876
+ diftype = line.diftype
877
+
878
+ with self.mmodel.keepswitch(switch=True):
879
+
880
+ # Pre-process for cases that use linevars and linedes
881
+ if line.textlinetype in ['textline']:
882
+ linedf = pd.DataFrame(np.nan , index=self.mmodel.current_per, columns=[line.centertext]).T
883
+ self.unitline = self.lines[0].centertext
884
+ else:
885
+ def getline(start_ofset= 0,**kvargs):
886
+ locallinedfdict = self.mmodel.keep_get_plotdict_new(pat=line.pat,showtype=showtype,
887
+ diftype = diftype,by_var=False)
888
+ if diftype == 'basedf':
889
+ locallinedf = next(iter((locallinedfdict.values()))).T
890
+ else:
891
+ locallinedf = next(iter(reversed(locallinedfdict.values()))).T
892
+
893
+ return locallinedf.loc[:,self.mmodel.current_per]
894
+
895
+ # print(line.mul)
896
+
897
+
898
+
899
+ linedf = getline() * line.mul
900
+ linedf = self.get_rowdes(linedf,line)
901
+
902
+ return(linedf)
903
+
904
+
905
+
906
+
907
+ @property
908
+ def df_str_old(self):
909
+ width = self.options.width
910
+ # df = self.df.copy( )
911
+ if self.options.transpose:
912
+ dec = self.lines[-1].dec
913
+ thisdf = self.df.loc[self.timeslice,:] if self.timeslice else self.df
914
+ # df_char = pd.DataFrame(' ', index=self.thisdf.index, columns=self.thisdf.columns)
915
+ df_char = thisdf.map(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
916
+ else:
917
+ df_char = pd.DataFrame(' ', index=self.df.index, columns=self.df.columns)
918
+
919
+ format_decimal = [ line.dec for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
920
+ for i, dec in enumerate(format_decimal):
921
+ df_char.iloc[i] = self.df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
922
+
923
+ return df_char
924
+
925
+ @property
926
+ def df_str(self):
927
+ width = self.options.width
928
+ # df = self.df.copy( )
929
+ if self.options.transpose:
930
+ dec = self.lines[-1].dec
931
+ thisdf = self.df.loc[self.timeslice,:] if self.timeslice else self.df
932
+ df_char = pd.DataFrame(' ', index=thisdf.index, columns=thisdf.columns)
933
+ # for c in thisdf.columns:
934
+ # df_char.loc[:,c] = thisdf.loc[:,c].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
935
+ for i,_ in enumerate(thisdf.index):
936
+ df_char.iloc[i] = self.df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
937
+ else:
938
+ df_char = pd.DataFrame(' ', index=self.df.index, columns=self.df.columns)
939
+
940
+ format_decimal = [ line.dec for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
941
+ for i, dec in enumerate(format_decimal):
942
+ df_char.iloc[i] = self.df.iloc[i].apply(lambda x: " " * width if pd.isna(x) else f"{x:>{width},.{dec}f}".strip() )
943
+
944
+ return df_char
945
+
946
+
947
+
948
+ @property
949
+ def df_str_disp(self):
950
+ center = [ (line.textlinetype == 'textline' and line.centertext !='' ) for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
951
+ center_index = [index+1 for index, value in enumerate(center) if value]
952
+
953
+ width = self.options.width
954
+ thisdf = self.df_str.loc[:,self.timeslice] if self.timeslice else self.df_str
955
+
956
+ rawdata = thisdf.to_string(max_cols= self.options.max_cols).split('\n')
957
+ data = center_title_under_years(rawdata,center_index)
958
+ # print(*data,sep='\n')
959
+ # rawdata[0],rawdata[1] = rawdata[1],rawdata[0]
960
+ out = '\n'.join(data)
961
+ return out
962
+
963
+ @property
964
+ def df_str_disp_transpose(self):
965
+
966
+
967
+ rawdata = self.df_str.to_string(max_cols= self.options.max_cols).split('\n')
968
+ data = [self.unitline ] +rawdata
969
+ data[0],data[1] = data[1],data[0]
970
+ data = center_title_under_years(data,title_row_index=[1],year_row_index=0)
971
+ out = '\n'.join(data)
972
+ return out
973
+
974
+
975
+ @property
976
+ def show(self):
977
+ if self.options.title:
978
+ print(self.mmodel.string_substitution(self.options.title))
979
+ print(self.df_str_disp_transpose if self.options.transpose else self.df_str_disp)
980
+ if self.options.foot:
981
+ print(self.options.foot)
982
+
983
+
984
+ @property
985
+ def sheetwidget(self):
986
+
987
+ return [self.out_html]
988
+
989
+
990
+ @property
991
+ def out_html(self):
992
+
993
+
994
+
995
+ return HTML(self.htmlwidget)
996
+
997
+ @property
998
+ def htmlwidget_old(self):
999
+
1000
+
1001
+ if self.options.transpose:
1002
+ thisdf = self.df_str.loc[self.timeslice,:] if self.timeslice else self.df_str
1003
+ else:
1004
+ thisdf = self.df_str.loc[:,self.timeslice] if self.timeslice else self.df_str
1005
+ center = [ (line.textlinetype == 'textline' and line.centertext !='' ) for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
1006
+
1007
+
1008
+ outsty = self.make_html_style(thisdf)
1009
+
1010
+ if self.options.title:
1011
+ outsty = outsty.set_caption(self.options.title)
1012
+
1013
+ if self.options.foot:
1014
+ out = add_footer_to_styler(outsty,self.options.foot)
1015
+ else:
1016
+ out = outsty.to_html(na_rep='')
1017
+
1018
+ if self.options.transpose:
1019
+ out = insert_centered_row(out,self.unitline,len(self.df.columns))
1020
+
1021
+ return out
1022
+
1023
+ @property
1024
+ def htmlwidget(self):
1025
+ endhtml = ''
1026
+ def tab_to_html(i,df,line):
1027
+ nonlocal endhtml
1028
+ out = ''
1029
+ html_all = self.mmodel.ibsstyle(df,use_tooltip=False,dec=line.dec).to_html()
1030
+ splitted_html = HTMLSplitData(html_all)
1031
+ if i == 0:
1032
+ caption = f'<caption>{self.mmodel.string_substitution(self.options.title)}</caption>' if self.options.title else ''
1033
+ out = splitted_html.text_before_thead + caption + '<thead>' + splitted_html.thead+'\n'
1034
+
1035
+
1036
+ endhtml = splitted_html.text_after_tbody
1037
+
1038
+ if (line.textlinetype == 'textline' and line.centertext !='' ):
1039
+ col0 = '<tr><td <th class="row_heading level0 row0" > </td>'
1040
+ out = out + '\n'+col0 + f"<td colspan='{len(df.columns)}' style='text-align: center;position: sticky; top: 0; background: white; left: 0;'>{line.centertext}</td></tr>"
1041
+
1042
+ else:
1043
+ out = out + splitted_html.tbody
1044
+
1045
+ return out
1046
+
1047
+
1048
+ if self.options.transpose:
1049
+ thisdf = self.df_str.loc[self.timeslice,:] if self.timeslice else self.df_str
1050
+
1051
+
1052
+ outsty = self.make_html_style(thisdf)
1053
+
1054
+ if self.options.title:
1055
+ outsty = outsty.set_caption(self.options.title)
1056
+
1057
+ if self.options.foot:
1058
+ out = add_footer_to_styler(outsty,self.options.foot)
1059
+ else:
1060
+ out = outsty.to_html(na_rep='')
1061
+
1062
+ if self.options.transpose:
1063
+ out = insert_centered_row(out,self.unitline,len(self.df.columns))
1064
+
1065
+ else:
1066
+ thisdfs = [(i,df.loc[:,self.timeslice] if self.timeslice else df, line) for i,(df,line) in
1067
+ enumerate(zip(self.dfs,self.lines))]
1068
+
1069
+ out = '\n'.join([tab_to_html(i,df,line) for i,df,line in thisdfs])+'\n'
1070
+ if self.options.foot:
1071
+ foot = f"<tfoot><tr><td colspan='5' style='text-align: left;'>{self.options.foot}</td></tr></tfoot>"
1072
+ else:
1073
+ foot =''
1074
+
1075
+ out = out + foot + '</table>'
1076
+
1077
+
1078
+
1079
+
1080
+ return out
1081
+
1082
+
1083
+
1084
+ @property
1085
+ def latex(self):
1086
+ return self.latex_transpose if self.options.transpose else self.latex_straight
1087
+
1088
+
1089
+ @property
1090
+ def latex_straight(self):
1091
+ last_cols = 0
1092
+ rowlines = [ line for line,df in zip(self.lines,self.dfs) for row in range(len(df))]
1093
+ if self.timeslice:
1094
+ dfs = [self.df_str.loc[:,self.timeslice]]
1095
+ else:
1096
+ if self.options.chunk_size:
1097
+ dfs = [self.df_str.iloc[:, i:i+self.options.chunk_size] for i in range(0, self.df_str.shape[1], self.options.chunk_size)]
1098
+ else:
1099
+ if len(self.df_str.columns) > self.options.max_cols:
1100
+
1101
+ last_cols = self.options.last_cols
1102
+ first_cols = self.options.max_cols - last_cols
1103
+
1104
+ dfs = [pd.concat([self.df_str.iloc[:, :first_cols], self.df_str.iloc[:, -last_cols:]], axis=1)]
1105
+ else:
1106
+ dfs = [self.df_str]
1107
+ outlist = []
1108
+ for i,df in enumerate(dfs):
1109
+ ncol=len(df.columns)
1110
+ newindex = [fr'&\multicolumn{{{ncol}}}'+'{c}{' + f'{line.latexfont}' + '{' + df.index[i]+'}}'
1111
+ if line.textlinetype == 'textline'
1112
+ else df.index[i]
1113
+ for i, line in enumerate(rowlines)]
1114
+
1115
+ df.index = newindex
1116
+ tabformat = 'l'+'r'*(ncol-last_cols) + ( ('|'+'r'*last_cols) if last_cols else '')
1117
+ outlist = outlist + [df.style.format(lambda x:x) \
1118
+ .set_caption(self.mmodel.string_substitution(self.options.title) + ('' if i == 0 else ' - continued ')) \
1119
+ .to_latex(hrules=True, position='ht', column_format=tabformat).replace('%',r'\%').replace('US$',r'US\$').replace('...',r'\dots')
1120
+ .replace(r'\caption{',r'\caption{')] # to be used if no numbering
1121
+
1122
+ # print(outlist)
1123
+ out = r' '.join(outlist)
1124
+
1125
+ out = '\n'.join(l.replace('& ','') if 'multicolum' in l else l for l in out.split('\n'))
1126
+ if self.options.foot:
1127
+ out = out.replace(r'\end{tabular}', r'\end{tabular}'+'\n'+rf'\caption*{{{self.options.foot}}}')
1128
+
1129
+ if self.options.landscape:
1130
+ out= r'\begin{landscape}'+'\n' + out + r'\end{landscape}' +'\n'
1131
+
1132
+ return out
1133
+
1134
+ @property
1135
+ def latex_transpose_old(self):
1136
+
1137
+ df = (self.df_str.loc[:,self.timeslice] if self.timeslice else self.df_str).T
1138
+ multi_df = create_column_multiindex(df)
1139
+ tabformat = 'l'+'r'*len(multi_df.columns)
1140
+
1141
+ latex_df = (multi_df.style.format(lambda x:x)
1142
+ .set_caption(self.options.title)
1143
+ .to_latex(hrules=True, position='ht', column_format=tabformat)
1144
+ .replace('%',r'\%').replace('US$',r'US\$').replace('...',r'\dots') )
1145
+ out = latex_df
1146
+ data = out.split('\n')
1147
+ # print(*[f'{i} {d}' for i,d in enumerate(data)],sep='\n')
1148
+ data[6],data[4],data[5] = data [4],data[5],data[6]
1149
+ # print(*[f'{i} {d}' for i,d in enumerate(data)],sep='\n')
1150
+ out = '\n'.join(data)
1151
+ out = '\n'.join(l.replace('{r}','{c}') if 'multicolum' in l else l for l in out.split('\n'))
1152
+ out = out.replace(r'\caption{',r'\caption{')
1153
+ if self.options.foot:
1154
+ out = out.replace(r'\end{tabular}', r'\end{tabular}'+'\n'+rf'\caption*{{{self.options.foot}}}')
1155
+
1156
+ return out
1157
+
1158
+ @property
1159
+ def latex_transpose(self):
1160
+
1161
+ thisdf = self.df_str.loc[self.timeslice,:] if self.timeslice else self.df_str
1162
+ tabformat = 'l'+'r'*len(thisdf.columns)
1163
+
1164
+ latex_df = (thisdf.style.format(lambda x:x)
1165
+ .set_caption(self.mmodel.string_substitution(self.options.title))
1166
+ .to_latex(hrules=True, position='ht', column_format=tabformat)
1167
+ .replace('%',r'\%').replace('US$',r'US\$').replace('...',r'\dots') )
1168
+ out = latex_df
1169
+ data = out.split('\n')
1170
+ # print(*[f'{i} {d}' for i,d in enumerate(data)],sep='\n')
1171
+
1172
+ unit_line_latex = fr'&\multicolumn{{{len(thisdf.columns)}}}'+'{c}{{' + self.unitline+r'}}\\'
1173
+ data = data[:6]+ [unit_line_latex] + data[6:]
1174
+
1175
+ # print(*[f'{i} {d}' for i,d in enumerate(data)],sep='\n')
1176
+ out = '\n'.join(data)
1177
+ out = '\n'.join(l.replace('{r}','{c}') if 'multicolum' in l else l for l in out.split('\n'))
1178
+ out = out.replace(r'\caption{',r'\caption{')
1179
+ if self.options.foot:
1180
+ out = out.replace(r'\end{tabular}', r'\end{tabular}'+'\n'+rf'\caption*{{{self.options.foot}}}')
1181
+
1182
+ if self.options.landscape:
1183
+ out= r'\begin{landscape}'+'\n' + out + r'\end{landscape}' +'\n'
1184
+
1185
+
1186
+ return out
1187
+
1188
+ def make_html_style(self,df,use_tooltips =False ) :
1189
+ out = df.style.set_table_styles([
1190
+ {
1191
+ 'selector': '.row_heading, .corner',
1192
+ 'props': [
1193
+ ('position', 'sticky'),
1194
+ ('left', '0'),
1195
+ ('z-index', '3'),
1196
+ ('background-color', 'white'),
1197
+ ('width', '300px'), # Set the width of the row headings
1198
+ ('min-width', '200px'), # Ensure the minimum width is respected
1199
+ ('max-width', '400px') # Ensure the maximum width is respected
1200
+ ]
1201
+ },
1202
+ {
1203
+ 'selector': '.col_heading',
1204
+ 'props': [
1205
+ ('position', 'sticky'),
1206
+ ('top', '0'),
1207
+ ('z-index', '2'),
1208
+ ('background-color', 'white')
1209
+ ]
1210
+ },
1211
+ {
1212
+ 'selector': 'th',
1213
+ 'props': [
1214
+ ('text-align', 'left') , # Align text to the left
1215
+ ('background-color', 'white'), # Ensuring headers are not transparent
1216
+ ('z-index', '2') # Headers z-index on par with column headings
1217
+ ]
1218
+ },
1219
+ {
1220
+ 'selector': 'td', # Targeting data cells
1221
+ 'props': [
1222
+ ('z-index', '1'), # Lower z-index than headers
1223
+ ]
1224
+ },
1225
+
1226
+
1227
+ {
1228
+ 'selector': 'caption',
1229
+ 'props': [
1230
+ ('font-size', '16px'), # Make the font larger
1231
+ ('font-weight', 'bold') # Make the font bold
1232
+ ]
1233
+ }
1234
+ ],overwrite=True)
1235
+ if use_tooltips:
1236
+ tt = pd.DataFrame([[v for v in df.columns ]for t in df.index] ,index=df.index,columns=df.columns)
1237
+ try:
1238
+ out=out.set_tooltips(tt, props='visibility: hidden; position: absolute; z-index: 1; border: 1px solid #000066;'
1239
+ 'background-color: white; color: #000066; font-size: 0.8em;width:100%'
1240
+ 'transform: translate(0px, -24px); padding: 0.6em; border-radius: 0.5em;')
1241
+ except Exception as e:
1242
+ ...
1243
+ out= out.set_caption(self.options.title)
1244
+
1245
+ return out
1246
+
1247
+ def make_html_style(self,df,use_tooltips =False ) :
1248
+ # out = self.mmodel.ibsstyle(self.df_str,use_tooltip=False)
1249
+ out = self.mmodel.ibsstyle(df,use_tooltip=False)
1250
+
1251
+ return out
1252
+
1253
+ def __repr__(self):
1254
+ return f"MyDataClass({self.options!r} {self.lines!r})"
1255
+
1256
+
1257
+ @dataclass
1258
+ class DisplayKeepFigDef(DisplayDef):
1259
+
1260
+
1261
+ def __post_init__(self):
1262
+ super().__post_init__() # Call the parent class's __post_init__
1263
+ self.base_last = not self.options.scenarios
1264
+ # print(self.options.scenarios)
1265
+ # print(self.base_last)
1266
+ with self.mmodel.keepswitch(scenarios=self.options.scenarios,base_last = self.base_last):
1267
+
1268
+ with self.mmodel.set_smpl(*self.options.smpl):
1269
+
1270
+ self.dfs = [f for line in self.lines for f in self.make_df(line) ]
1271
+
1272
+ self.figs = self.make_figs(showfig=False)
1273
+ self.report_smpl = self.get_report_smpl
1274
+
1275
+
1276
+ self.chart_names = list(self.figs.keys() )
1277
+
1278
+
1279
+
1280
+
1281
+
1282
+ return
1283
+
1284
+
1285
+ def make_df(self, line):
1286
+ if line.textlinetype in ['textline']:
1287
+ # textdf = pd.DataFrame(float('nan'), index=self.mmodel.current_per, columns=[line.centertext]).T
1288
+ outlist = []
1289
+ else:
1290
+ # print(f'{line.pat=}')
1291
+ locallinedfdict = self.mmodel.keep_get_plotdict_new(
1292
+ pat=line.pat,
1293
+ showtype=line.showtype,
1294
+ diftype = line.diftype,
1295
+ by_var=line.by_var)
1296
+
1297
+ # print(f'before {locallinedfdict.keys()=}')
1298
+ if self.base_last or line.diftype == 'basedf':
1299
+ if line.by_var :
1300
+ if line.diftype == 'basedf':
1301
+ locallinedfdict = {k: df.iloc[:,[0]] for k,df in locallinedfdict.items() }
1302
+ else:
1303
+ locallinedfdict = {k: df.iloc[:,[-1]] for k,df in locallinedfdict.items() }
1304
+ else:
1305
+ if line.diftype == 'basedf':
1306
+ first_key = next(iter(locallinedfdict)) # First key
1307
+ locallinedfdict = {first_key: locallinedfdict[first_key]}
1308
+ else:
1309
+ last_key = next(reversed(locallinedfdict)) # Last key
1310
+ locallinedfdict = {last_key: locallinedfdict[last_key]}
1311
+
1312
+
1313
+
1314
+ # print(f'after {locallinedfdict.keys()=}')
1315
+
1316
+ outlist = [{'line':line, 'key':k ,
1317
+ 'df' : self.get_rowdes(df.loc[self.mmodel.current_per,:],line,row=False)
1318
+ } for k,df in locallinedfdict.items() ]
1319
+
1320
+ return(outlist)
1321
+
1322
+ def make_figs(self,showfig=True):
1323
+ # def keep_plot(self, pat='*', start='', end='', start_ofset=0, end_ofset=0, showtype='level',
1324
+ # diff=False, diffpct=False, mul=1.0, title='Scenarios', legend=False, scale='linear',
1325
+ # yunit='', ylabel='', dec='', trans=None, showfig=True, kind='line', size=(10, 6),
1326
+ # vline=None, savefig='', by_var=True, dataonly=False, samefig=False, ncol=2):
1327
+ """
1328
+ Generate and display plots for specified scenarios and variables.
1329
+ Returns:
1330
+ dict: A dictionary of Matplotlib figures, with keys being the variable names and values being the figure objects.
1331
+
1332
+ Raises:
1333
+ ZeroDivisionError: If no kept solution is available for plotting.
1334
+ """
1335
+ # Function implementation...
1336
+
1337
+ # Function implementation...
1338
+
1339
+ # Figures are built with the object-oriented matplotlib API (Figure())
1340
+ # so they are NOT registered in pyplot's global figure manager. This
1341
+ # prevents the %matplotlib inline backend from auto-rendering them at
1342
+ # end-of-cell (which caused a spurious, stale duplicate plot in
1343
+ # addition to this object's own rendering).
1344
+ dfsres = self.dfs
1345
+
1346
+
1347
+
1348
+ number = len(dfsres)
1349
+ options = self.options
1350
+
1351
+ if options.samefig:
1352
+ ...
1353
+ all_by_var = all([item['line'].by_var for item in dfsres])
1354
+ xcol = options.ncol
1355
+ xrow=-((-number )//options.ncol)
1356
+ figsize = (xcol*options.size[0],xrow*options.size[1])
1357
+
1358
+ # print(f'{size=} {figsize=}')
1359
+ fig = Figure(figsize=figsize)
1360
+ #gs = gridspec.GridSpec(xrow + 1, xcol, figure=fig) # One additional row for the legend
1361
+
1362
+ if options.legend and all_by_var:
1363
+ extra_row = 1
1364
+ row_heights = [1] * xrow + [0.5] # Assuming equal height for all plot rows, and half for the legend
1365
+
1366
+ else:
1367
+ extra_row = 0
1368
+ row_heights = [1] * xrow # Assuming equal height for all plot rows,
1369
+
1370
+
1371
+ gs = gridspec.GridSpec(xrow + extra_row , xcol, figure=fig, height_ratios=row_heights)
1372
+
1373
+ fig.set_constrained_layout(True)
1374
+
1375
+ # Create axes for the plots
1376
+ axes = [fig.add_subplot(gs[i, j]) for i in range(xrow) for j in range(xcol)]
1377
+ if options.legend and all_by_var and not self.base_last:
1378
+ legend_ax = fig.add_subplot(gs[-1, :]) # Span the legend axis across the bottom
1379
+
1380
+ figs = {self.name : fig}
1381
+
1382
+ else:
1383
+ if 1:
1384
+ keys = format_list_with_numbers([f'{self.var_description[dr["key"]]}, {dr["line"].showtype} '+
1385
+ f'{dr["line"].diftype} '.replace('nodif','') for dr in dfsres ])
1386
+ #print(f'{keys}=')
1387
+ else:
1388
+ keys = format_list_with_numbers([dr['key'] for dr in dfsres ])
1389
+ ...
1390
+ # figs_and_ax = {f'{self.name}_{i}' : plt.subplots(figsize=options.size) for i,v in enumerate(dfsres)}
1391
+ def _new_fig_ax():
1392
+ f = Figure(figsize=options.size)
1393
+ a = f.subplots()
1394
+ return f, a
1395
+ figs_and_ax = {v : _new_fig_ax() for v in keys}
1396
+ figs = {v : fig for v,(fig,ax) in figs_and_ax.items() }
1397
+ axes = [ ax for fig,ax in figs_and_ax.values() ]
1398
+
1399
+
1400
+ for i,item in enumerate(dfsres):
1401
+ v = item['key']
1402
+ df = item['df']
1403
+ line = item['line']
1404
+
1405
+ mul = line.mul
1406
+ by_var= line.by_var
1407
+ aspct = ' as pct ' if line.diftype in {'difpct'} else ' '
1408
+ dftype = line.showtype.capitalize()
1409
+ dec = line.dec
1410
+
1411
+ ylabel = 'Percent' if (line.showtype in { 'growth','gdppct'} or line.diftype == 'difpct' ) else ''
1412
+
1413
+ # if by_var:
1414
+ # pretitle = (f'Difference{aspct}to "{list(self.mmodel.keep_solutions.keys())[0] }" for {dftype}:'
1415
+ # if (line.diftype in {'difpct', 'dif'}) else f'{dftype}:')
1416
+ # else:
1417
+ # pretitle = (f'Difference{aspct}to "{df.columns[0] }" for {dftype}:'
1418
+ # if (line.diftype in {'difpct','dif'}) else f'{dftype}:')
1419
+
1420
+
1421
+ compare = f"{list(self.mmodel.keep_solutions.keys())[0] }" if by_var else f"{df.columns[0] }"
1422
+ var_name = v
1423
+ var_description = self.var_description[v]
1424
+
1425
+ default_title = line.default_ax_title_template_df if self.base_last else line.default_ax_title_template
1426
+ if self.base_last:
1427
+ ax_title_template = line.ax_title_template if line.ax_title_template else line.default_ax_title_template_df
1428
+ else:
1429
+ ax_title_template = line.ax_title_template if line.ax_title_template else line.default_ax_title_template
1430
+
1431
+ ax_title =ax_title_template.format(compare=compare,var_name = var_name,var_description=var_description).replace(r'\n','\n')
1432
+
1433
+ # print(v,ax_title_template)
1434
+ # title=(f'Difference{aspct}to "{df.columns[0] if not by_var else list(self.mmodel.keep_solutions.keys())[0] }" for {dftype}:'
1435
+ # if (line.diftype in {'difpct'}) else f'{dftype}:')
1436
+
1437
+ self.mmodel.plot_basis_ax(axes[i], v , df*mul, legend=options.legend,
1438
+ scale='linear', trans=self.var_description if self.options.rename else {},
1439
+ ax_title = ax_title ,
1440
+ yunit=line.yunit,
1441
+ ylabel='Percent' if (line.showtype in {'growth','gdppct'} or line.diftype in {'difpct'})else ylabel,
1442
+ xlabel='',kind = line.kind ,samefig=options.samefig and all_by_var,
1443
+ dec=dec)
1444
+
1445
+ if options.legend and self.base_last and axes[i].get_legend() is not None:
1446
+ axes[i].get_legend().remove()
1447
+
1448
+ for ax in axes[number:]:
1449
+ ax.set_visible(False)
1450
+
1451
+
1452
+ if options.samefig:
1453
+ fig.suptitle(self.mmodel.string_substitution(options.title) ,fontsize=20)
1454
+
1455
+ if options.legend and all_by_var and not self.base_last:
1456
+ handles, labels = axes[0].get_legend_handles_labels() # Assuming the first ax has the handles and labels
1457
+ legend_ax.legend(handles, labels, loc='center', ncol=3 if True else len(labels), fontsize='large')
1458
+ legend_ax.axis('off') # Hide the axis
1459
+
1460
+ else:
1461
+ for v,fig in figs.items() :
1462
+ if options.title:
1463
+ fig.suptitle(self.mmodel.string_substitution(options.title) ,fontsize=20)
1464
+
1465
+
1466
+
1467
+ if showfig:
1468
+ ...
1469
+ for f in figs.values():
1470
+ display(f)
1471
+
1472
+
1473
+ return figs
1474
+
1475
+ @property
1476
+ def latex(self):
1477
+ latex_dir = Path(f'../{self.name}')
1478
+
1479
+ if self.options.samefig:
1480
+ chart = list(self.figs.keys())[0]
1481
+ self.titledic = {chart: self.options.title}
1482
+ else:
1483
+ self.titledic = {chart: fig.axes[0].get_title() for chart,fig in self.figs.items() }
1484
+
1485
+ for fig in self.figs.values():
1486
+ fig.axes[0].set_title('')
1487
+ ## pgf may not work in new version of matplotlib
1488
+ self.mmodel.savefigs(figs=self.figs, location = './latex',
1489
+ experimentname = self.name ,extensions= ['png'],
1490
+ # experimentname = self.name ,extensions= ['png','pgf'],
1491
+ xopen=False)
1492
+
1493
+ if not self.options.samefig:
1494
+ for c,fig in self.figs.items():
1495
+ fig.axes[0].set_title(self.titledic[c])
1496
+
1497
+
1498
+
1499
+ out = '\n'.join( self.figwrap(chart) for chart in self.chart_names)
1500
+ return out
1501
+
1502
+ def savefigs(self,**kwargs):
1503
+ '''
1504
+ Saves a collection of matplotlib figures to a specified directory.
1505
+
1506
+ Parameters:
1507
+ - location (str): The base folder in which to save the charts. Defaults to './graph'.
1508
+ - experimentname (str): A subfolder under 'location' where charts are saved. Defaults to 'experiment1'.
1509
+ - addname (str): An additional name added to each figure filename. Defaults to an empty string.
1510
+ - extensions (list): A list of string file extensions for saving the figures. Defaults to ['svg'].
1511
+ - xopen (bool): If True, open the saved figure locations in a web browser.
1512
+
1513
+ Returns:
1514
+ str: The absolute path to the folder where figures are saved.
1515
+
1516
+ Raises:
1517
+ Exception: If the folder cannot be created or a figure cannot be saved/opened.
1518
+ '''
1519
+
1520
+ return self.mmodel.savefigs(figs=self.figs,**kwargs)
1521
+
1522
+
1523
+ @property
1524
+ def sheetwidget(self):
1525
+ return [self.out_html ]
1526
+
1527
+
1528
+ @property
1529
+ def out_html(self):
1530
+ figlist = {t: htmlwidget_fig(f) for t,f in self.figs.items() }
1531
+ out = tabwidget(figlist,tab=False,selected_index=0)
1532
+ return out.datawidget
1533
+
1534
+
1535
+ # def _ipython_display_(self):
1536
+ # display(self.out_html)
1537
+
1538
+ # display(self.out_html)
1539
+
1540
+ @property
1541
+ def show(self):
1542
+ for f in self.figs.values():
1543
+ display(f)
1544
+
1545
+
1546
+ @dataclass
1547
+ class DisplayTextDef(DisplayDef):
1548
+
1549
+
1550
+ def __post_init__(self):
1551
+ super().__post_init__() # Call the parent class's __post_init__
1552
+ self.latex_text = self.options.latex_text
1553
+ self.html_text = self.options.html_text
1554
+ self.text_text = self.options.text_text
1555
+ self.markdown_text = self.options.markdown_text
1556
+ self.report_smpl =('','')
1557
+
1558
+ def __or__(self, other):
1559
+ """
1560
+ Combines two DisplayDef instances into a new DisplayDef with combined specifications.
1561
+
1562
+ :param other: Another DisplayDef instance to add.
1563
+ :return: A new DisplayDef instance with merged specifications.
1564
+ """
1565
+ if isinstance(other, str):
1566
+ out = get_DisplayTextDef(self.mmodel,other)
1567
+
1568
+ elif isinstance(other, self.__class__):
1569
+ out = other
1570
+ else:
1571
+ return NotImplemented
1572
+
1573
+ # Combine options using the existing __add__ method of DisplaySpec
1574
+
1575
+ # Merge names if they differ, separated by a comma
1576
+ new_name = self.name if self.name == out.name else f"{self.name}_{out.name}"
1577
+ new_spec = DisplaySpec(options=Options(
1578
+ text_text=self.text_text + out.text_text ,
1579
+ html_text=self.html_text + out.html_text,
1580
+ latex_text=self.latex_text + out.latex_text,
1581
+ markdown_text=self.markdown_text + out.markdown_text,
1582
+ name='some_text'))
1583
+ # Create a new DisplayDef with the combined specifications
1584
+ return self.__class__(mmodel=self.mmodel, spec=new_spec, name=new_name)
1585
+
1586
+
1587
+ @property
1588
+ def latex(self) :
1589
+ return self.latex_text
1590
+
1591
+
1592
+
1593
+ @property
1594
+ def out_html(self):
1595
+ return HTML(self.mmodel.string_substitution(self.html_text))
1596
+
1597
+ @property
1598
+ def sheetwidget(self):
1599
+ tablist = [self.out_html]
1600
+ return tablist
1601
+
1602
+
1603
+
1604
+ @property
1605
+ def show(self):
1606
+ print(self.mmodel.string_substitution(self.text_text))
1607
+
1608
+
1609
+
1610
+ @dataclass
1611
+ class DisplayContainerDef:
1612
+ mmodel : Any = None
1613
+ reports: List[DisplayDef] = field(default_factory=list)
1614
+ name: str = 'Report_test'
1615
+ options: dict = field(default_factory=dict)
1616
+
1617
+
1618
+ def __add__(self, other):
1619
+ """
1620
+ Combines two DisplayDef instances into a new DisplayDef with combined specifications.
1621
+
1622
+ :param other: Another DisplayDef instance to add.
1623
+ :return: A new DisplayDef instance with merged specifications.
1624
+ """
1625
+ if isinstance(other, str):
1626
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= self.reports + [get_DisplayTextDef(self.mmodel,other)])
1627
+
1628
+ else:
1629
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= self.reports + [other])
1630
+
1631
+ # Create a new DisplayDef with the combined specifications
1632
+ return out
1633
+
1634
+
1635
+ def set_name(self,name):
1636
+ self.name = name.replace(' ','_')
1637
+ return self
1638
+
1639
+ def set_scenarios(self,scenarios):
1640
+ rs = [r.set_options(scenarios=scenarios) for r in self.reports]
1641
+ out = DisplayContainerDef(mmodel=self.mmodel,reports= rs)
1642
+
1643
+ return out
1644
+
1645
+
1646
+ @property
1647
+ def latex(self):
1648
+ out = '\n'.join(l.latex for l in self.reports)
1649
+ return out
1650
+
1651
+ def pdf(self,pdfopen=False,show=True,width=WIDTH,height=HEIGHT,typesetter='xelatex -interaction=batchmode -no-shell-escape'):
1652
+ try:
1653
+ repo = LatexRepo(self.latex ,name=self.name)
1654
+ out = repo.pdf(pdfopen,show,width,height,typesetter)
1655
+ except:
1656
+ out= None
1657
+
1658
+ @property
1659
+ def spec_list(self):
1660
+ out = [r.save_spec for r in self.reports]
1661
+ return out
1662
+
1663
+
1664
+ @property
1665
+ def save_spec(self):
1666
+ display_type = self.__class__.__name__
1667
+ out = self.to_json(display_type)
1668
+ return out
1669
+
1670
+
1671
+ def to_json(self,display_type):
1672
+
1673
+ display_spec_dict = {"display_type":display_type, "options": self.options,
1674
+ "name":self.name,
1675
+ "reports": [r.save_spec for r in self.reports] }
1676
+
1677
+ # Serialize the dictionary to a JSON string
1678
+ return json.dumps(display_spec_dict, indent=4)
1679
+
1680
+ @classmethod
1681
+ def reports_restore(cls,mmodel,json_string):
1682
+ reports_json_strings = json.loads(json_string)
1683
+
1684
+ out = cls(mmodel=mmodel,reports = [create_instance_from_json(mmodel,r) for r in reports_json_strings['reports']] )
1685
+ return out
1686
+ # print (*[r for r in reports_json_strings['reports']],sep='\n')
1687
+
1688
+
1689
+ @property
1690
+ def show(self):
1691
+ for r in self.reports:
1692
+ print('\n')
1693
+ r.show
1694
+
1695
+
1696
+ @property
1697
+ def sheetwidget(self):
1698
+ tablist = [ r for r in self.reports for s in r.sheetwidget]
1699
+ return tablist
1700
+
1701
+
1702
+
1703
+ @property
1704
+ def out_html(self):
1705
+ for r in self.reports:
1706
+ for c in r.sheetwidget:
1707
+ display(c)
1708
+ #return out.out_html
1709
+
1710
+ def _ipython_display_(self):
1711
+ _ = self.out_html
1712
+
1713
+
1714
+ DisplayContainerDef.pdf.__doc__ = LatexRepo.pdf.__doc__
1715
+
1716
+
1717
+ @dataclass
1718
+ class DisplayFigWrapDef(DisplayDef):
1719
+
1720
+ figs : Dict = field(default_factory=dict)
1721
+ extensions : List[str] = field(default_factory=lambda: ['svg','pgf'])
1722
+
1723
+
1724
+ def __post_init__(self):
1725
+ super().__post_init__() # Call the parent class's __post_init__
1726
+
1727
+
1728
+ self.titledic = {chart: fig.axes[0].get_title() for chart,fig in self.figs.items() }
1729
+
1730
+ self.newfigs= {chart : fig for chart,fig in self.figs.items() }
1731
+ for fig in self.newfigs.values():
1732
+ fig.axes[0].set_title('')
1733
+
1734
+
1735
+ self.mmodel.savefigs(figs=self.newfigs, location = './latex',
1736
+ experimentname = self.name ,extensions= self.extensions
1737
+ ,xopen=False)
1738
+ self.charts = list(self.newfigs.keys() )
1739
+
1740
+
1741
+ return
1742
+
1743
+ return out
1744
+
1745
+ @property
1746
+ def latex(self):
1747
+ latex_dir = Path(f'../{self.name}')
1748
+
1749
+ out = '\n'.join( self.figwrap(chart) for chart in self.charts)
1750
+ return out
1751
+
1752
+
1753
+
1754
+ @dataclass
1755
+ class HtmlSplitTable:
1756
+ html: str
1757
+
1758
+ def __post_init__(self):
1759
+ self.text_before_tbody, self.tbody, self.text_after_tbody = self.split_html()
1760
+
1761
+ def split_html(self):
1762
+ # Split HTML text into text before tbody, tbody, and text after tbody
1763
+ text_before_tbody = self.html.split('<tbody>')[0]
1764
+ tbody = self.html.split('<tbody>')[1].split('</tbody>')[0]
1765
+ text_after_tbody = self.html.split('</tbody>')[1]
1766
+
1767
+ return text_before_tbody, tbody, text_after_tbody
1768
+
1769
+
1770
+ @dataclass
1771
+ class HTMLSplitData:
1772
+ html: str
1773
+
1774
+ def __post_init__(self):
1775
+ self.text_before_thead, self.thead, self.tbody, self.text_after_tbody = self.split_html()
1776
+
1777
+ def split_html(self):
1778
+ # Split HTML text into parts before <thead>, <thead>, before <tbody>, <tbody>, and after <tbody>
1779
+ parts_before_thead = self.html.split('<thead>')
1780
+ text_before_thead = parts_before_thead[0]
1781
+
1782
+ thead_parts = parts_before_thead[1].split('</thead>')
1783
+ thead = '<thead>' + thead_parts[0] + '</thead>'
1784
+ text_before_tbody = thead_parts[1]
1785
+
1786
+ tbody_parts = text_before_tbody.split('<tbody>')
1787
+ tbody = '<tbody>' + tbody_parts[1].split('</tbody>')[0] + '</tbody>'
1788
+ text_after_tbody = tbody_parts[1].split('</tbody>')[1]
1789
+
1790
+ return text_before_thead, thead, tbody, text_after_tbody
1791
+
1792
+ def center_title_under_years(data, title_row_index=[1],year_row_index = 0):
1793
+ """
1794
+ Center a title (specified by its index in the list) under the years row in a list of strings.
1795
+
1796
+ :param data: List of strings representing the data.
1797
+ :param title_row_index: Index of the title row in the list. Defaults to 1.
1798
+ :return: A new list of strings with the centered title.
1799
+ """
1800
+ # Make a shallow copy of the list to avoid modifying the original list
1801
+ adjusted_data = data.copy()
1802
+
1803
+ # Find the start and end indices of the year values in the first row
1804
+ year_row = adjusted_data[year_row_index]
1805
+ start_index = len(year_row) - len(year_row.lstrip())
1806
+ end_index = len(year_row.rstrip())
1807
+
1808
+ # Calculate the total space available for centering
1809
+ total_space = end_index - start_index
1810
+
1811
+ # Center the title within this space
1812
+ for row_index in title_row_index:
1813
+ title = adjusted_data[row_index].replace('...',' ').strip() # Remove leading and trailing spaces
1814
+ centered_title = title.center(total_space)
1815
+
1816
+ # Replace the original title in the list with the centered title
1817
+ # Ensuring that the centered title is positioned correctly relative to the entire line
1818
+ adjusted_data[row_index] = f"{year_row[:start_index]}{centered_title}{year_row[end_index:]}"
1819
+
1820
+ return adjusted_data
1821
+
1822
+
1823
+ def create_instance_from_json(mmodel,json_str: str):
1824
+
1825
+ def find_classes_matching_pattern():
1826
+ """
1827
+ Finds and returns a dictionary of classes in the current module that match
1828
+ the pattern 'Display*Def', where the keys are the class names and the values
1829
+ are the actual class objects.
1830
+ """
1831
+ pattern = re.compile(r'^Display.*Def$')
1832
+ class_dict = {name: cls for name, cls in globals().items()
1833
+ if re.match(pattern, name) and isinstance(cls, type)}
1834
+ return class_dict
1835
+
1836
+ data = json.loads(json_str)
1837
+ display_type = data['display_type']
1838
+
1839
+
1840
+ class_map = find_classes_matching_pattern()
1841
+ # print(class_map)
1842
+
1843
+ if display_type not in class_map:
1844
+ raise ValueError(f"Unsupported display type: {display_type}")
1845
+
1846
+ if display_type == 'DisplayContainerDef' :
1847
+ instance = DisplayContainerDef.reports_restore(mmodel,json_str)
1848
+ else:
1849
+ # Get the class
1850
+ cls = class_map[display_type]
1851
+
1852
+ # Process options and lines
1853
+ options = Options(**data['options'])
1854
+ lines = [Line(**line) for line in data['lines']]
1855
+
1856
+ # Create DisplaySpec instance
1857
+ spec = DisplaySpec(options=options, lines=lines)
1858
+
1859
+ # Create the display type instance (e.g., DisplayVarTableDef)
1860
+ instance = cls(mmodel,spec=spec)
1861
+
1862
+ return instance
1863
+
1864
+ def create_column_multiindex(df):
1865
+ """
1866
+ Transforms the columns of a DataFrame into a MultiIndex, where the first column's name
1867
+ becomes the top level, and the names of the remaining columns serve as the second level.
1868
+
1869
+ Parameters:
1870
+ df (pd.DataFrame): The DataFrame to convert.
1871
+
1872
+ Returns:
1873
+ pd.DataFrame: A DataFrame with MultiIndex columns.
1874
+ """
1875
+ # Retrieve the name of the first column to be used as the top level of the MultiIndex
1876
+ top_level_name = df.columns[0]
1877
+
1878
+ # Create a MultiIndex from the first column name and the names of the remaining columns
1879
+ multiindex_columns = [(top_level_name, col) for col in df.columns[1:]]
1880
+
1881
+ # Construct a new DataFrame with the MultiIndex columns
1882
+ multiindex_df = pd.DataFrame(df.iloc[:, 1:].values, index=df.index, columns=pd.MultiIndex.from_tuples(multiindex_columns))
1883
+ # print(multiindex_df)
1884
+ return multiindex_df
1885
+
1886
+
1887
+ def center_multiindex_headers(df):
1888
+ style = df.style.set_table_styles([
1889
+ {'selector': 'th.level0', 'props': [('text-align', 'center')]},
1890
+ # {'selector': 'th', 'props': [('font-size', '12pt')]},
1891
+ {
1892
+ 'selector': '.row_heading, .corner',
1893
+ 'props': [
1894
+ ('position', 'sticky'),
1895
+ ('left', '0'),
1896
+ ('z-index', '3'),
1897
+ ('background-color', 'white'),
1898
+ ('width', '300px'), # Set the width of the row headings
1899
+ ('min-width', '200px'), # Ensure the minimum width is respected
1900
+ ('max-width', '400px') # Ensure the maximum width is respected
1901
+ ]
1902
+ },
1903
+ {
1904
+ 'selector': '.col_heading',
1905
+ 'props': [
1906
+ ('position', 'sticky'),
1907
+ ('top', '0'),
1908
+ ('z-index', '2'),
1909
+ ('background-color', 'white')
1910
+ ]
1911
+ },
1912
+ {
1913
+ 'selector': 'th',
1914
+ 'props': [
1915
+ ('text-align', 'left') , # Align text to the left
1916
+ ('background-color', 'white'), # Ensuring headers are not transparent
1917
+ ('z-index', '2') # Headers z-index on par with column headings
1918
+ ]
1919
+ },
1920
+ {
1921
+ 'selector': 'td', # Targeting data cells
1922
+ 'props': [
1923
+ ('z-index', '1'), # Lower z-index than headers
1924
+ ]
1925
+ },
1926
+
1927
+
1928
+ {
1929
+ 'selector': 'caption',
1930
+ 'props': [
1931
+ ('font-size', '16px'), # Make the font larger
1932
+ ('font-weight', 'bold') # Make the font bold
1933
+ ]
1934
+ }
1935
+
1936
+ ])
1937
+ return style
1938
+
1939
+
1940
+ def create_column_multiindex__(df):
1941
+ """
1942
+ Transforms the columns of a DataFrame into a MultiIndex, where the first column's name
1943
+ becomes the top level, and the names of the remaining columns serve as the second level.
1944
+ The top level name will only be shown once in the MultiIndex representation.
1945
+
1946
+ Parameters:
1947
+ df (pd.DataFrame): The DataFrame to convert.
1948
+
1949
+ Returns:
1950
+ pd.DataFrame: A DataFrame with MultiIndex columns.
1951
+ """
1952
+ # Retrieve the name of the first column to be used as the top level of the MultiIndex
1953
+ top_level_name = df.columns[0]
1954
+
1955
+ # Create a list of tuples for the MultiIndex where the first element is the top level name
1956
+ # and only use it once as the top level label for all subsequent columns
1957
+ multiindex_columns = [(top_level_name, col) if col != top_level_name else ('', col) for col in df.columns]
1958
+
1959
+ # Construct a new DataFrame with the MultiIndex columns
1960
+ multiindex_df = pd.DataFrame(df.values, index=df.index, columns=pd.MultiIndex.from_tuples(multiindex_columns))
1961
+
1962
+ return multiindex_df
1963
+
1964
+ def format_list_with_numbers(items):
1965
+ """
1966
+ Format a list of items with numbered prefixes.
1967
+
1968
+ Args:
1969
+ items (list of str): The list of items to be formatted, where each item is a string separated by '|'.
1970
+
1971
+ Returns:
1972
+ list of str: The formatted list with numbered prefixes.
1973
+ """
1974
+ # This dictionary will keep track of the counts of each prefix
1975
+ count_dict = {}
1976
+ # This will be the resulting list with formatted strings
1977
+ formatted_list = []
1978
+
1979
+ # First pass: count occurrences of each prefix
1980
+ for item in items:
1981
+ prefix = item.split('|')[0].strip()
1982
+ if prefix in count_dict:
1983
+ count_dict[prefix] += 1
1984
+ else:
1985
+ count_dict[prefix] = 1
1986
+
1987
+ # Second pass: format based on the count
1988
+ temp_dict = {} # This dictionary keeps track of the running count used for numbering
1989
+ for item in items:
1990
+ prefix = item.split('|')[0].strip()
1991
+
1992
+ # Determine the numbering based on the count
1993
+ if count_dict[prefix] > 1:
1994
+ if prefix in temp_dict:
1995
+ temp_dict[prefix] += 1
1996
+ else:
1997
+ temp_dict[prefix] = 1
1998
+ formatted_item = f"{prefix} {temp_dict[prefix]}"
1999
+ else:
2000
+ formatted_item = f"{prefix}"
2001
+
2002
+ formatted_list.append(formatted_item)
2003
+
2004
+ return formatted_list
2005
+
2006
+ def add_footer_to_styler(styler, footer_text):
2007
+ """
2008
+ Extend a Pandas Styler object with a footer.
2009
+
2010
+ Args:
2011
+ styler (pandas.io.formats.style.Styler): The Pandas Styler object.
2012
+ footer_text (str): The text to be included in the footer.
2013
+
2014
+ Returns:
2015
+ str: The styled HTML with the added footer.
2016
+ """
2017
+ # Convert styler to HTML and append a footer section
2018
+ styled_html = styler.to_html(na_rep='') # or styler.to_html() in older pandas versions
2019
+ footer_html = f"<tfoot><tr><td colspan='{len(styler.data.columns)}' style='text-align: left;'>{footer_text}</td></tr></tfoot>"
2020
+
2021
+ # Insert the footer just before the closing table tag
2022
+ styled_html_with_footer = styled_html.replace('</table>', f'{footer_html}</table>')
2023
+ return styled_html_with_footer
2024
+
2025
+ def split_text_html_latex(other):
2026
+ if other.startswith('<'):
2027
+ html,latex = other.split('<>')
2028
+ # If the left-hand side operand is a string, this method will be called
2029
+ linstance = DisplayTextDef(spec = DisplaySpec(options = Options(html_text=other,name='html Text')))
2030
+
2031
+
2032
+ else:
2033
+ linstance = DisplayTextDef(spec = DisplaySpec(options = Options(html_text=other,name='latex Text')))
2034
+
2035
+ def split_text(input_string):
2036
+ """
2037
+ Split the input string based on the specified terminals.
2038
+
2039
+ Args:
2040
+ input_string (str): The input string to be split.
2041
+
2042
+ Returns:
2043
+ tuple: A tuple containing three substrings:
2044
+ - The first substring before any terminals.
2045
+ - The substring between <latex> and </latex> terminals.
2046
+ - The substring between <html> and </html> terminals.
2047
+ - The substring between <markdown> and </markdown> terminals.
2048
+ """
2049
+ # Find the indices of terminals
2050
+ latex_start = input_string.find("<latex>")
2051
+ html_start = input_string.find("<html>")
2052
+ markdown_start = input_string.find("<markdown>")
2053
+ latex_end = input_string.find("</latex>")
2054
+ html_end = input_string.find("</html>")
2055
+ markdown_end = input_string.find("</markdown>")
2056
+
2057
+ # If terminals are not found, set their indices to the end of the string
2058
+ if latex_start == -1:
2059
+ latex_start = len(input_string)
2060
+ if html_start == -1:
2061
+ html_start = len(input_string)
2062
+ if markdown_start == -1:
2063
+ markdown_start = len(input_string)
2064
+ if latex_end == -1:
2065
+ latex_end = len(input_string)
2066
+ if html_end == -1:
2067
+ html_end = len(input_string)
2068
+ if markdown_end == -1:
2069
+ markdown_end = len(input_string)
2070
+
2071
+ # Extract the content between terminals
2072
+ latex_content = input_string[latex_start + len("<latex>"):latex_end]
2073
+ html_content = input_string[html_start + len("<html>"):html_end]
2074
+ markdown_content = input_string[markdown_start + len("<markdown>"):markdown_end]
2075
+
2076
+ # If no terminals are present, separate the first part as text
2077
+ text_end = min(html_start, markdown_start, latex_start)
2078
+ first_part = input_string[:text_end]
2079
+ return first_part, latex_content, html_content, markdown_content
2080
+
2081
+ def get_DisplayTextDef(mmodel,input_string):
2082
+ """
2083
+ Create a DisplayTextDef object based on the input string.
2084
+
2085
+ Args:
2086
+ input_string (str): The input string to be processed.
2087
+
2088
+ Returns:
2089
+ DisplayTextDef: A DisplayTextDef object with the text, HTML, LaTeX, and Markdown content.
2090
+ """
2091
+ # Find the indices of terminals and extract substrings
2092
+ text_obj = SplitTextResult(input_string)
2093
+
2094
+ # Create a DisplayTextDef object with the extracted content
2095
+
2096
+ out = DisplayTextDef(mmodel=mmodel,spec=DisplaySpec(options=Options(
2097
+ text_text=text_obj.text_text,
2098
+ html_text=text_obj. html_text,
2099
+ latex_text=text_obj.latex_text,
2100
+ markdown_text=text_obj.markdown_text,
2101
+ name='some_text')))
2102
+ return out
2103
+
2104
+ def insert_centered_row(html_text, centered_text, num_columns):
2105
+ # Find the index where the tbody starts
2106
+ tbody_start_index = html_text.find('<tbody>')+len('<tbody>')
2107
+
2108
+ # Construct the new row with centered text
2109
+ col0 = '<tr><td <th class="row_heading level0 row0" > </td>'
2110
+
2111
+ new_row = '\n'+col0 + f"<td colspan='{num_columns}' style='text-align: center;position: sticky; top: 0; background: white; left: 0;'>{centered_text}</td></tr>"
2112
+
2113
+ # Insert the new row at the beginning of the tbody
2114
+
2115
+ modified_html_text = html_text[:tbody_start_index] + new_row + html_text[tbody_start_index:]
2116
+
2117
+ return modified_html_text
2118
+
2119
+
2120
+ from dataclasses import dataclass
2121
+
2122
+ @dataclass
2123
+ class SplitTextResult:
2124
+ input_string: str
2125
+ text_text: str = ""
2126
+ latex_text: str = ""
2127
+ html_text: str = ""
2128
+ markdown_text: str = ""
2129
+
2130
+ def __post_init__(self):
2131
+ self.split_text(self.input_string)
2132
+
2133
+ def split_text(self, input_string):
2134
+ """
2135
+ Split the input string based on the specified terminals.
2136
+
2137
+ Args:
2138
+ input_string (str): The input string to be split.
2139
+
2140
+ Returns:
2141
+ None
2142
+ """
2143
+ # Find the indices of terminals
2144
+ latex_start = input_string.find("<latex>")
2145
+ html_start = input_string.find("<html>")
2146
+ markdown_start = input_string.find("<markdown>")
2147
+ latex_end = input_string.find("</latex>")
2148
+ html_end = input_string.find("</html>")
2149
+ markdown_end = input_string.find("</markdown>")
2150
+
2151
+ # If terminals are not found, set their indices to the end of the string
2152
+ if latex_start == -1:
2153
+ latex_start = len(input_string)
2154
+ if html_start == -1:
2155
+ html_start = len(input_string)
2156
+ if markdown_start == -1:
2157
+ markdown_start = len(input_string)
2158
+ if latex_end == -1:
2159
+ latex_end = len(input_string)
2160
+ if html_end == -1:
2161
+ html_end = len(input_string)
2162
+ if markdown_end == -1:
2163
+ markdown_end = len(input_string)
2164
+
2165
+ # If no terminals are present, separate the first part as text
2166
+ text_end = min(html_start, markdown_start, latex_start)
2167
+ self.text_text = input_string[:text_end]
2168
+
2169
+ # Extract the content between terminals
2170
+ latex_content_ = input_string[latex_start + len("<latex>"):latex_end]
2171
+ html_content_ = input_string[html_start + len("<html>"):html_end]
2172
+ markdown_content_ = input_string[markdown_start + len("<markdown>"):markdown_end]
2173
+ self.latex_text = latex_content_ if latex_content_ else self.text_text
2174
+ self.html_text = html_content_ if html_content_ else self.text_text
2175
+ self.markdown_text = markdown_content_ if markdown_content_ else self.text_text
2176
+
2177
+ # Test the implementation
2178
+ if __name__ == '__main__':
2179
+
2180
+ result = SplitTextResult("<html>Hello</html><latex>Latex Content</latex><markdown>Markdown Content</markdown>")
2181
+ print(result)
2182
+
2183
+
2184
+
2185
+ print(SplitTextResult('test'))
2186
+ print(SplitTextResult('test<latex>notest</latex<html>'))
2187
+ print(get_DisplayTextDef('kk','test'))