owlplanner 2025.5.5__py3-none-any.whl → 2025.5.15__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.
owlplanner/__init__.py CHANGED
@@ -2,5 +2,7 @@ from owlplanner.plan import Plan #
2
2
  from owlplanner.plan import clone # noqa: F401
3
3
  from owlplanner.config import readConfig # noqa: F401
4
4
  from owlplanner.rates import getRatesDistributions # noqa: F401
5
- from owlplanner.rates import showRatesDistributions # noqa: F401
6
5
  from owlplanner.version import __version__ # noqa: F401
6
+
7
+ # Make the package importable as 'owlplanner'
8
+ __all__ = ['Plan', 'clone', 'readConfig', 'getRatesDistributions', '__version__']
owlplanner/config.py CHANGED
@@ -16,7 +16,7 @@ from datetime import date
16
16
  import os
17
17
 
18
18
  from owlplanner import plan
19
- from owlplanner import logging
19
+ from owlplanner import mylogging as log
20
20
  from owlplanner.rates import FROM, TO
21
21
 
22
22
 
@@ -143,7 +143,7 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
143
143
  A new plan is created and returned.
144
144
  Argument file can be a filename, a file, or a stringIO.
145
145
  """
146
- mylog = logging.Logger(verbose, logstreams)
146
+ mylog = log.Logger(verbose, logstreams)
147
147
 
148
148
  accountTypes = ["taxable", "tax-deferred", "tax-free"]
149
149
 
owlplanner/plan.py CHANGED
@@ -16,26 +16,21 @@ Disclaimer: This program comes with no guarantee. Use at your own risk.
16
16
  ###########################################################################
17
17
  import numpy as np
18
18
  import pandas as pd
19
- import matplotlib.pyplot as plt
20
19
  from datetime import date, datetime
21
20
  from functools import wraps
22
21
  from openpyxl import Workbook
23
22
  from openpyxl.utils.dataframe import dataframe_to_rows
24
23
  import time
25
24
 
26
- from owlplanner import utils as u
27
- from owlplanner import tax2025 as tx
28
- from owlplanner import abcapi as abc
29
- from owlplanner import rates
30
- from owlplanner import config
31
- from owlplanner import timelists
32
- from owlplanner import logging
33
- from owlplanner import progress
34
- from owlplanner import plots
35
-
36
-
37
- # This makes all graphs to have the same height.
38
- plt.rcParams.update({'figure.autolayout': True})
25
+ from . import utils as u
26
+ from . import tax2025 as tx
27
+ from . import abcapi as abc
28
+ from . import rates
29
+ from . import config
30
+ from . import timelists
31
+ from . import mylogging as log
32
+ from . import progress
33
+ from .plotting.factory import PlotFactory
39
34
 
40
35
 
41
36
  def _genGamma_n(tau):
@@ -246,6 +241,8 @@ class Plan(object):
246
241
  self._description = ''
247
242
  self.defaultPlots = "nominal"
248
243
  self.defaultSolver = "HiGHS"
244
+ self._plotterName = None
245
+ self.setPlotBackend("matplotlib")
249
246
 
250
247
  self.N_i = len(yobs)
251
248
  if not (0 <= self.N_i <= 2):
@@ -257,7 +254,7 @@ class Plan(object):
257
254
  if inames[0] == "" or (self.N_i == 2 and inames[1] == ""):
258
255
  raise ValueError("Name for each individual must be provided.")
259
256
 
260
- self.filingStatus = ["single", "married"][self.N_i - 1]
257
+ self.filingStatus = ("single", "married")[self.N_i - 1]
261
258
  # Default year TCJA is speculated to expire.
262
259
  self.yTCJA = 2026
263
260
  self.inames = inames
@@ -314,7 +311,7 @@ class Plan(object):
314
311
  self.lambdha = 0
315
312
 
316
313
  # Scenario starts at the beginning of this year and ends at the end of the last year.
317
- s = ["", "s"][self.N_i - 1]
314
+ s = ("", "s")[self.N_i - 1]
318
315
  self.mylog.vprint(f"Preparing scenario of {self.N_n} years for {self.N_i} individual{s}.")
319
316
  for i in range(self.N_i):
320
317
  endyear = thisyear + self.horizons[i] - 1
@@ -349,7 +346,7 @@ class Plan(object):
349
346
  self.mylog = logger
350
347
 
351
348
  def setLogstreams(self, verbose, logstreams):
352
- self.mylog = logging.Logger(verbose, logstreams)
349
+ self.mylog = log.Logger(verbose, logstreams)
353
350
  # self.mylog.vprint(f"Setting logstreams to {logstreams}.")
354
351
 
355
352
  def logger(self):
@@ -403,13 +400,11 @@ class Plan(object):
403
400
  if value is None:
404
401
  return self.defaultPlots
405
402
 
406
- opts = ["nominal", "today"]
407
- if value in opts:
408
- return value
403
+ opts = ("nominal", "today")
404
+ if value not in opts:
405
+ raise ValueError(f"Value type must be one of: {opts}")
409
406
 
410
- raise ValueError(f"Value type must be one of: {opts}")
411
-
412
- return None
407
+ return value
413
408
 
414
409
  def rename(self, newname):
415
410
  """
@@ -454,6 +449,18 @@ class Plan(object):
454
449
  self.defaultPlots = self._checkValue(value)
455
450
  self.mylog.vprint(f"Setting plots default value to {value}.")
456
451
 
452
+ def setPlotBackend(self, backend: str):
453
+ """
454
+ Set plotting backend.
455
+ """
456
+
457
+ if backend not in ("matplotlib", "plotly"):
458
+ raise ValueError(f"Backend {backend} not a valid option.")
459
+
460
+ if backend != self._plotterName:
461
+ self._plotter = PlotFactory.createBackend(backend)
462
+ self.mylog.vprint(f"Setting plotting backend to {backend}.")
463
+
457
464
  def setDividendRate(self, mu):
458
465
  """
459
466
  Set dividend tax rate. Rate is in percent. Default 2%.
@@ -1100,7 +1107,7 @@ class Plan(object):
1100
1107
  return None
1101
1108
 
1102
1109
  def _setup_constraint_shortcuts(self, options):
1103
- # Set up all the local variables as attributes for use in helpers
1110
+ # Set up all the local variables as attributes for use in helpers.
1104
1111
  oppCostX = options.get("oppCostX", 0.)
1105
1112
  self.xnet = 1 - oppCostX / 100.
1106
1113
  self.optionsUnits = u.getUnits(options.get("units", "k"))
@@ -1390,11 +1397,15 @@ class Plan(object):
1390
1397
  """
1391
1398
  Run historical scenarios on plan over a range of years.
1392
1399
  """
1400
+
1393
1401
  if yend + self.N_n > self.year_n[0]:
1394
1402
  yend = self.year_n[0] - self.N_n - 1
1395
1403
  self.mylog.vprint(f"Warning: Upper bound for year range re-adjusted to {yend}.")
1396
- N = yend - ystart + 1
1397
1404
 
1405
+ if yend < ystart:
1406
+ raise ValueError(f"Starting year is too large to support a lifespan of {self.N_n} years.")
1407
+
1408
+ N = yend - ystart + 1
1398
1409
  self.mylog.vprint(f"Running historical range from {ystart} to {yend}.")
1399
1410
 
1400
1411
  self.mylog.setVerbose(verbose)
@@ -1405,7 +1416,7 @@ class Plan(object):
1405
1416
  columns = ["partial", "final"]
1406
1417
  else:
1407
1418
  self.mylog.print(f"Invalid objective {objective}.")
1408
- return None
1419
+ raise ValueError(f"Invalid objective {objective}.")
1409
1420
 
1410
1421
  df = pd.DataFrame(columns=columns)
1411
1422
 
@@ -1429,10 +1440,11 @@ class Plan(object):
1429
1440
  progcall.finish()
1430
1441
  self.mylog.resetVerbose()
1431
1442
 
1443
+ fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
1444
+ self.n_d, self.N_i, self.phi_j)
1445
+ self.mylog.print(description.getvalue())
1446
+
1432
1447
  if figure:
1433
- fig, description = plots.show_histogram_results(objective, df, N, self.year_n,
1434
- self.n_d, self.N_i, self.phi_j)
1435
- self.mylog.print(description.getvalue())
1436
1448
  return fig, description.getvalue()
1437
1449
 
1438
1450
  return N, df
@@ -1442,7 +1454,7 @@ class Plan(object):
1442
1454
  """
1443
1455
  Run Monte Carlo simulations on plan.
1444
1456
  """
1445
- if self.rateMethod not in ["stochastic", "histochastic"]:
1457
+ if self.rateMethod not in ("stochastic", "histochastic"):
1446
1458
  self.mylog.print("It is pointless to run Monte Carlo simulations with fixed rates.")
1447
1459
  return
1448
1460
 
@@ -1486,10 +1498,11 @@ class Plan(object):
1486
1498
  progcall.finish()
1487
1499
  self.mylog.resetVerbose()
1488
1500
 
1501
+ fig, description = self._plotter.plot_histogram_results(objective, df, N, self.year_n,
1502
+ self.n_d, self.N_i, self.phi_j)
1503
+ self.mylog.print(description.getvalue())
1504
+
1489
1505
  if figure:
1490
- fig, description = plots.show_histogram_results(objective, df, N, self.year_n,
1491
- self.n_d, self.N_i, self.phi_j)
1492
- self.mylog.print(description.getvalue())
1493
1506
  return fig, description.getvalue()
1494
1507
 
1495
1508
  return N, df
@@ -1625,7 +1638,8 @@ class Plan(object):
1625
1638
  while True:
1626
1639
  solution, xx, solverSuccess, solverMsg = solverMethod(objective, options)
1627
1640
 
1628
- if not solverSuccess:
1641
+ if not solverSuccess or solution is None:
1642
+ self.mylog.vprint("Solver failed:", solverMsg, solverSuccess)
1629
1643
  break
1630
1644
 
1631
1645
  if not withMedicare:
@@ -1675,8 +1689,13 @@ class Plan(object):
1675
1689
  """
1676
1690
  from scipy import optimize
1677
1691
 
1678
- # mip_rel_gap smaller than 1e-6 can lead to oscillatory solutions.
1679
- milpOptions = {"disp": False, "mip_rel_gap": 1e-7}
1692
+ # Optimize solver parameters
1693
+ milpOptions = {
1694
+ "disp": False,
1695
+ "mip_rel_gap": 1e-7,
1696
+ "presolve": True,
1697
+ "node_limit": 10000 # Limit search nodes for faster solutions
1698
+ }
1680
1699
 
1681
1700
  self._buildConstraints(objective, options)
1682
1701
  Alu, lbvec, ubvec = self.A.arrays()
@@ -2047,10 +2066,10 @@ class Plan(object):
2047
2066
  dic[f"Net spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
2048
2067
  dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
2049
2068
 
2050
- totIncome = np.sum(self.g_n, axis=0)
2051
- totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2052
- dic["Total net spending"] = f"{u.d(totIncomeNow)}"
2053
- dic["[Total net spending]"] = f"{u.d(totIncome)}"
2069
+ totSpending = np.sum(self.g_n, axis=0)
2070
+ totSpendingNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
2071
+ dic["Total net spending"] = f"{u.d(totSpendingNow)}"
2072
+ dic["[Total net spending]"] = f"{u.d(totSpending)}"
2054
2073
 
2055
2074
  totRoth = np.sum(self.x_in, axis=(0, 1))
2056
2075
  totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
@@ -2150,13 +2169,25 @@ class Plan(object):
2150
2169
  self.mylog.vprint(f"Warning: Cannot plot correlations for {self.rateMethod} rate method.")
2151
2170
  return None
2152
2171
 
2153
- fig = plots.show_rates_correlations(self._name, self.tau_kn, self.N_n, self.rateMethod,
2154
- self.rateFrm, self.rateTo, tag, shareRange)
2172
+ fig = self._plotter.plot_rates_correlations(self._name, self.tau_kn, self.N_n, self.rateMethod,
2173
+ self.rateFrm, self.rateTo, tag, shareRange)
2174
+
2175
+ if figure:
2176
+ return fig
2155
2177
 
2178
+ self._plotter.jupyter_renderer(fig)
2179
+ return None
2180
+
2181
+ def showRatesDistributions(self, frm=rates.FROM, to=rates.TO, figure=False):
2182
+ """
2183
+ Plot histograms of the rates distributions.
2184
+ """
2185
+ fig = self._plotter.plot_rates_distributions(frm, to, rates.SP500, rates.BondsBaa,
2186
+ rates.TNotes, rates.Inflation, rates.FROM)
2156
2187
  if figure:
2157
2188
  return fig
2158
2189
 
2159
- plt.show()
2190
+ self._plotter.jupyter_renderer(fig)
2160
2191
  return None
2161
2192
 
2162
2193
  def showRates(self, tag="", figure=False):
@@ -2169,13 +2200,13 @@ class Plan(object):
2169
2200
  self.mylog.vprint("Warning: Rate method must be selected before plotting.")
2170
2201
  return None
2171
2202
 
2172
- fig = plots.show_rates(self._name, self.tau_kn, self.year_n, self.yearFracLeft,
2173
- self.N_k, self.rateMethod, self.rateFrm, self.rateTo, tag)
2203
+ fig = self._plotter.plot_rates(self._name, self.tau_kn, self.year_n, self.yearFracLeft,
2204
+ self.N_k, self.rateMethod, self.rateFrm, self.rateTo, tag)
2174
2205
 
2175
2206
  if figure:
2176
2207
  return fig
2177
2208
 
2178
- plt.show()
2209
+ self._plotter.jupyter_renderer(fig)
2179
2210
  return None
2180
2211
 
2181
2212
  def showProfile(self, tag="", figure=False):
@@ -2187,19 +2218,15 @@ class Plan(object):
2187
2218
  if self.xi_n is None:
2188
2219
  self.mylog.vprint("Warning: Profile must be selected before plotting.")
2189
2220
  return None
2190
-
2191
2221
  title = self._name + "\nSpending Profile"
2192
- if tag != "":
2222
+ if tag:
2193
2223
  title += " - " + tag
2194
-
2195
- style = {"profile": "-"}
2196
- series = {"profile": self.xi_n}
2197
- fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat="$\\xi$")
2224
+ fig = self._plotter.plot_profile(self.year_n, self.xi_n, title, self.inames)
2198
2225
 
2199
2226
  if figure:
2200
2227
  return fig
2201
2228
 
2202
- plt.show()
2229
+ self._plotter.jupyter_renderer(fig)
2203
2230
  return None
2204
2231
 
2205
2232
  @_checkCaseStatus
@@ -2213,28 +2240,15 @@ class Plan(object):
2213
2240
  the default behavior of setDefaultPlots().
2214
2241
  """
2215
2242
  value = self._checkValue(value)
2216
-
2217
2243
  title = self._name + "\nNet Available Spending"
2218
- if tag != "":
2244
+ if tag:
2219
2245
  title += " - " + tag
2220
-
2221
- style = {"net": "-", "target": ":"}
2222
- if value == "nominal":
2223
- series = {"net": self.g_n, "target": (self.g_n[0] / self.xi_n[0]) * self.xiBar_n}
2224
- yformat = "\\$k (nominal)"
2225
- else:
2226
- series = {
2227
- "net": self.g_n / self.gamma_n[:-1],
2228
- "target": (self.g_n[0] / self.xi_n[0]) * self.xi_n,
2229
- }
2230
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2231
-
2232
- fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat)
2233
-
2246
+ fig = self._plotter.plot_net_spending(self.year_n, self.g_n, self.xi_n, self.xiBar_n,
2247
+ self.gamma_n, value, title, self.inames)
2234
2248
  if figure:
2235
2249
  return fig
2236
2250
 
2237
- plt.show()
2251
+ self._plotter.jupyter_renderer(fig)
2238
2252
  return None
2239
2253
 
2240
2254
  @_checkCaseStatus
@@ -2251,41 +2265,37 @@ class Plan(object):
2251
2265
  the default behavior of setDefaultPlots().
2252
2266
  """
2253
2267
  value = self._checkValue(value)
2268
+ figures = self._plotter.plot_asset_distribution(self.year_n, self.inames, self.b_ijkn,
2269
+ self.gamma_n, value, self._name, tag)
2270
+ if figure:
2271
+ return figures
2254
2272
 
2255
- if value == "nominal":
2256
- yformat = "\\$k (nominal)"
2257
- infladjust = 1
2258
- else:
2259
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2260
- infladjust = self.gamma_n
2261
-
2262
- years_n = np.array(self.year_n)
2263
- years_n = np.append(years_n, [years_n[-1] + 1])
2264
- y2stack = {}
2265
- jDic = {"taxable": 0, "tax-deferred": 1, "tax-free": 2}
2266
- kDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
2267
- figures = []
2268
- for jkey in jDic:
2269
- stackNames = []
2270
- for kkey in kDic:
2271
- name = kkey + " / " + jkey
2272
- stackNames.append(name)
2273
- y2stack[name] = np.zeros((self.N_i, self.N_n + 1))
2274
- for i in range(self.N_i):
2275
- y2stack[name][i][:] = self.b_ijkn[i][jDic[jkey]][kDic[kkey]][:] / infladjust
2273
+ for fig in figures:
2274
+ self._plotter.jupyter_renderer(fig)
2275
+ return None
2276
2276
 
2277
- title = self._name + "\nAssets Distribution - " + jkey
2278
- if tag != "":
2279
- title += " - " + tag
2277
+ @_checkCaseStatus
2278
+ def showGrossIncome(self, tag="", value=None, figure=False):
2279
+ """
2280
+ Plot income tax and taxable income over time horizon.
2280
2281
 
2281
- fig, ax = plots.stack_plot(years_n, self.inames, title, range(self.N_i),
2282
- y2stack, stackNames, "upper left", yformat)
2283
- figures.append(fig)
2282
+ A tag string can be set to add information to the title of the plot.
2284
2283
 
2284
+ The value parameter can be set to *nominal* or *today*, overriding
2285
+ the default behavior of setDefaultPlots().
2286
+ """
2287
+ value = self._checkValue(value)
2288
+ tax_brackets = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
2289
+ title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
2290
+ if tag:
2291
+ title += " - " + tag
2292
+ fig = self._plotter.plot_gross_income(
2293
+ self.year_n, self.G_n, self.gamma_n, value, title, tax_brackets
2294
+ )
2285
2295
  if figure:
2286
- return figures
2296
+ return fig
2287
2297
 
2288
- plt.show()
2298
+ self._plotter.jupyter_renderer(fig)
2289
2299
  return None
2290
2300
 
2291
2301
  def showAllocations(self, tag="", figure=False):
@@ -2296,46 +2306,16 @@ class Plan(object):
2296
2306
 
2297
2307
  A tag string can be set to add information to the title of the plot.
2298
2308
  """
2299
- count = self.N_i
2300
- if self.ARCoord == "spouses":
2301
- acList = [self.ARCoord]
2302
- count = 1
2303
- elif self.ARCoord == "individual":
2304
- acList = [self.ARCoord]
2305
- elif self.ARCoord == "account":
2306
- acList = ["taxable", "tax-deferred", "tax-free"]
2307
- else:
2308
- raise ValueError(f"Unknown coordination {self.ARCoord}.")
2309
-
2310
- figures = []
2311
- assetDic = {"stocks": 0, "C bonds": 1, "T notes": 2, "common": 3}
2312
- for i in range(count):
2313
- y2stack = {}
2314
- for acType in acList:
2315
- stackNames = []
2316
- for key in assetDic:
2317
- aname = key + " / " + acType
2318
- stackNames.append(aname)
2319
- y2stack[aname] = np.zeros((count, self.N_n))
2320
- y2stack[aname][i][:] = self.alpha_ijkn[i, acList.index(acType), assetDic[key], : self.N_n]
2321
-
2322
- title = self._name + "\nAsset Allocation (%) - " + acType
2323
- if self.ARCoord == "spouses":
2324
- title += " spouses"
2325
- else:
2326
- title += " " + self.inames[i]
2327
-
2328
- if tag != "":
2329
- title += " - " + tag
2330
-
2331
- fig, ax = plots.stack_plot(self.year_n, self.inames, title, [i],
2332
- y2stack, stackNames, "upper left", "percent")
2333
- figures.append(fig)
2334
-
2309
+ title = self._name + "\nAsset Allocation"
2310
+ if tag:
2311
+ title += " - " + tag
2312
+ figures = self._plotter.plot_allocations(self.year_n, self.inames, self.alpha_ijkn,
2313
+ self.ARCoord, title)
2335
2314
  if figure:
2336
2315
  return figures
2337
2316
 
2338
- plt.show()
2317
+ for fig in figures:
2318
+ self._plotter.jupyter_renderer(fig)
2339
2319
  return None
2340
2320
 
2341
2321
  @_checkCaseStatus
@@ -2349,31 +2329,15 @@ class Plan(object):
2349
2329
  the default behavior of setDefaultPlots().
2350
2330
  """
2351
2331
  value = self._checkValue(value)
2352
-
2353
2332
  title = self._name + "\nSavings Balance"
2354
- if tag != "":
2333
+ if tag:
2355
2334
  title += " - " + tag
2356
-
2357
- stypes = self.savings_in.keys()
2358
- # Add one year for estate.
2359
- year_n = np.append(self.year_n, [self.year_n[-1] + 1])
2360
-
2361
- if value == "nominal":
2362
- yformat = "\\$k (nominal)"
2363
- savings_in = self.savings_in
2364
- else:
2365
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2366
- savings_in = {}
2367
- for key in self.savings_in:
2368
- savings_in[key] = self.savings_in[key] / self.gamma_n
2369
-
2370
- fig, ax = plots.stack_plot(year_n, self.inames, title, range(self.N_i),
2371
- savings_in, stypes, "upper left", yformat)
2372
-
2335
+ fig = self._plotter.plot_accounts(self.year_n, self.savings_in, self.gamma_n,
2336
+ value, title, self.inames)
2373
2337
  if figure:
2374
2338
  return fig
2375
2339
 
2376
- plt.show()
2340
+ self._plotter.jupyter_renderer(fig)
2377
2341
  return None
2378
2342
 
2379
2343
  @_checkCaseStatus
@@ -2387,55 +2351,15 @@ class Plan(object):
2387
2351
  the default behavior of setDefaultPlots().
2388
2352
  """
2389
2353
  value = self._checkValue(value)
2390
-
2391
2354
  title = self._name + "\nRaw Income Sources"
2392
- stypes = self.sources_in.keys()
2393
-
2394
- if tag != "":
2355
+ if tag:
2395
2356
  title += " - " + tag
2396
-
2397
- if value == "nominal":
2398
- yformat = "\\$k (nominal)"
2399
- sources_in = self.sources_in
2400
- else:
2401
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2402
- sources_in = {}
2403
- for key in stypes:
2404
- sources_in[key] = self.sources_in[key] / self.gamma_n[:-1]
2405
-
2406
- fig, ax = plots.stack_plot(self.year_n, self.inames, title, range(self.N_i),
2407
- sources_in, stypes, "upper left", yformat)
2408
-
2357
+ fig = self._plotter.plot_sources(self.year_n, self.sources_in, self.gamma_n,
2358
+ value, title, self.inames)
2409
2359
  if figure:
2410
2360
  return fig
2411
2361
 
2412
- plt.show()
2413
- return None
2414
-
2415
- @_checkCaseStatus
2416
- def _showFeff(self, tag=""):
2417
- """
2418
- Plot income tax paid over time.
2419
-
2420
- A tag string can be set to add information to the title of the plot.
2421
- """
2422
- title = self._name + "\nEff f "
2423
- if tag != "":
2424
- title += " - " + tag
2425
-
2426
- various = ["-", "--", "-.", ":"]
2427
- style = {}
2428
- series = {}
2429
- q = 0
2430
- for t in range(self.N_t):
2431
- key = "f " + str(t)
2432
- series[key] = self.F_tn[t] / self.DeltaBar_tn[t]
2433
- style[key] = various[q % len(various)]
2434
- q += 1
2435
-
2436
- fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat="")
2437
-
2438
- plt.show()
2362
+ self._plotter.jupyter_renderer(fig)
2439
2363
  return None
2440
2364
 
2441
2365
  @_checkCaseStatus
@@ -2449,87 +2373,17 @@ class Plan(object):
2449
2373
  the default behavior of setDefaultPlots().
2450
2374
  """
2451
2375
  value = self._checkValue(value)
2452
-
2453
- style = {"income taxes": "-", "Medicare": "-."}
2454
-
2455
- if value == "nominal":
2456
- series = {"income taxes": self.T_n, "Medicare": self.M_n}
2457
- yformat = "\\$k (nominal)"
2458
- else:
2459
- series = {
2460
- "income taxes": self.T_n / self.gamma_n[:-1],
2461
- "Medicare": self.M_n / self.gamma_n[:-1],
2462
- }
2463
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2464
-
2465
2376
  title = self._name + "\nIncome Tax"
2466
- if tag != "":
2467
- title += " - " + tag
2468
-
2469
- fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat)
2470
-
2471
- if figure:
2472
- return fig
2473
-
2474
- plt.show()
2475
- return None
2476
-
2477
- @_checkCaseStatus
2478
- def showGrossIncome(self, tag="", value=None, figure=False):
2479
- """
2480
- Plot income tax and taxable income over time horizon.
2481
-
2482
- A tag string can be set to add information to the title of the plot.
2483
-
2484
- The value parameter can be set to *nominal* or *today*, overriding
2485
- the default behavior of setDefaultPlots().
2486
- """
2487
- value = self._checkValue(value)
2488
-
2489
- style = {"taxable income": "-"}
2490
-
2491
- if value == "nominal":
2492
- series = {"taxable income": self.G_n}
2493
- yformat = "\\$k (nominal)"
2494
- infladjust = self.gamma_n[:-1]
2495
- else:
2496
- series = {"taxable income": self.G_n / self.gamma_n[:-1]}
2497
- yformat = "\\$k (" + str(self.year_n[0]) + "\\$)"
2498
- infladjust = 1
2499
-
2500
- title = self._name + "\nTaxable Ordinary Income vs. Tax Brackets"
2501
- if tag != "":
2377
+ if tag:
2502
2378
  title += " - " + tag
2503
-
2504
- fig, ax = plots.line_income_plot(self.year_n, series, style, title, yformat)
2505
-
2506
- data = tx.taxBrackets(self.N_i, self.n_d, self.N_n, self.yTCJA)
2507
- for key in data:
2508
- data_adj = data[key] * infladjust
2509
- ax.plot(self.year_n, data_adj, label=key, ls=":")
2510
-
2511
- plt.grid(visible="both")
2512
- ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
2513
-
2379
+ fig = self._plotter.plot_taxes(self.year_n, self.T_n, self.M_n, self.gamma_n,
2380
+ value, title, self.inames)
2514
2381
  if figure:
2515
2382
  return fig
2516
2383
 
2517
- plt.show()
2384
+ self._plotter.jupyter_renderer(fig)
2518
2385
  return None
2519
2386
 
2520
- # @_checkCaseStatus
2521
- def saveConfig(self, basename=None):
2522
- """
2523
- Save parameters in a configuration file.
2524
- """
2525
- if basename is None:
2526
- basename = "case_" + self._name
2527
-
2528
- config.saveConfig(self, basename, self.mylog)
2529
-
2530
- return None
2531
-
2532
- @_checkCaseStatus
2533
2387
  def saveWorkbook(self, overwrite=False, *, basename=None, saveToFile=True):
2534
2388
  """
2535
2389
  Save instance in an Excel spreadsheet.
@@ -2772,6 +2626,17 @@ class Plan(object):
2772
2626
 
2773
2627
  return None
2774
2628
 
2629
+ def saveConfig(self, basename=None):
2630
+ """
2631
+ Save parameters in a configuration file.
2632
+ """
2633
+ if basename is None:
2634
+ basename = "case_" + self._name
2635
+
2636
+ config.saveConfig(self, basename, self.mylog)
2637
+
2638
+ return None
2639
+
2775
2640
 
2776
2641
  def _saveWorkbook(wb, basename, overwrite, mylog):
2777
2642
  """
@@ -0,0 +1,7 @@
1
+ """
2
+ Plotting backends for Owl.
3
+ """
4
+
5
+ from .factory import PlotFactory
6
+
7
+ __all__ = ['PlotFactory']