diff --git a/examples/notebooks/battery_parameterisation/echem_identification_pitfalls.ipynb b/examples/notebooks/battery_parameterisation/echem_identification_pitfalls.ipynb
index 615a12ec0..452763924 100644
--- a/examples/notebooks/battery_parameterisation/echem_identification_pitfalls.ipynb
+++ b/examples/notebooks/battery_parameterisation/echem_identification_pitfalls.ipynb
@@ -32,8 +32,9 @@
"\n",
"import pybop\n",
"\n",
- "go = pybop.plot.PlotlyManager().go\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "go = pybop.plot.backends.PlotlyManager().go\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/ecm_monte_carlo_sampling.ipynb b/examples/notebooks/battery_parameterisation/ecm_monte_carlo_sampling.ipynb
index dce5640a7..9d992aa2d 100644
--- a/examples/notebooks/battery_parameterisation/ecm_monte_carlo_sampling.ipynb
+++ b/examples/notebooks/battery_parameterisation/ecm_monte_carlo_sampling.ipynb
@@ -46,7 +46,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/ecm_multipulse_identification.ipynb b/examples/notebooks/battery_parameterisation/ecm_multipulse_identification.ipynb
index 65f4623b6..df18cdc56 100644
--- a/examples/notebooks/battery_parameterisation/ecm_multipulse_identification.ipynb
+++ b/examples/notebooks/battery_parameterisation/ecm_multipulse_identification.ipynb
@@ -48,7 +48,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/ecm_scipy_constraints.ipynb b/examples/notebooks/battery_parameterisation/ecm_scipy_constraints.ipynb
index 99724f587..0c42cf490 100644
--- a/examples/notebooks/battery_parameterisation/ecm_scipy_constraints.ipynb
+++ b/examples/notebooks/battery_parameterisation/ecm_scipy_constraints.ipynb
@@ -31,7 +31,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/electrode_balancing.ipynb b/examples/notebooks/battery_parameterisation/electrode_balancing.ipynb
index adc764f25..190a5d569 100644
--- a/examples/notebooks/battery_parameterisation/electrode_balancing.ipynb
+++ b/examples/notebooks/battery_parameterisation/electrode_balancing.ipynb
@@ -29,7 +29,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/lgm50_pulse_validation.ipynb b/examples/notebooks/battery_parameterisation/lgm50_pulse_validation.ipynb
index 7cd0c65b0..9509c47cf 100644
--- a/examples/notebooks/battery_parameterisation/lgm50_pulse_validation.ipynb
+++ b/examples/notebooks/battery_parameterisation/lgm50_pulse_validation.ipynb
@@ -32,8 +32,9 @@
"\n",
"import pybop\n",
"\n",
- "go = pybop.plot.PlotlyManager().go\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "go = pybop.plot.backends.PlotlyManager().go\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/pouch_cell_identification.ipynb b/examples/notebooks/battery_parameterisation/pouch_cell_identification.ipynb
index 593acc012..e649fb54b 100644
--- a/examples/notebooks/battery_parameterisation/pouch_cell_identification.ipynb
+++ b/examples/notebooks/battery_parameterisation/pouch_cell_identification.ipynb
@@ -30,8 +30,9 @@
"\n",
"import pybop\n",
"\n",
- "go = pybop.plot.PlotlyManager().go\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "go = pybop.plot.backends.PlotlyManager().go\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/battery_parameterisation/sensitivity_analysis_hessian.ipynb b/examples/notebooks/battery_parameterisation/sensitivity_analysis_hessian.ipynb
index 3cab87a0d..4fc281242 100644
--- a/examples/notebooks/battery_parameterisation/sensitivity_analysis_hessian.ipynb
+++ b/examples/notebooks/battery_parameterisation/sensitivity_analysis_hessian.ipynb
@@ -29,6 +29,7 @@
"\n",
"import pybop\n",
"\n",
+ "pybop.plot.use_backend(\"matplotlib\")\n",
"np.random.seed(8) # users can remove this line"
]
},
@@ -210,708 +211,708 @@
"showlegend": false,
"type": "scatter",
"x": [
- 0.0,
- 10.0,
- 20.0,
- 30.0,
- 40.0,
- 50.0,
- 60.0,
- 70.0,
- 80.0,
- 90.0,
- 100.0,
- 110.0,
- 120.0,
- 130.0,
- 140.0,
- 150.0,
- 160.0,
- 170.0,
- 180.0,
- 190.0,
- 200.0,
- 210.0,
- 220.0,
- 230.0,
- 240.0,
- 250.0,
- 260.0,
- 270.0,
- 280.0,
- 290.0,
- 300.0,
- 310.0,
- 320.0,
- 330.0,
- 340.0,
- 350.0,
- 360.0,
- 370.0,
- 380.0,
- 390.0,
- 400.0,
- 410.0,
- 420.0,
- 430.0,
- 440.0,
- 450.0,
- 460.0,
- 470.0,
- 480.0,
- 490.0,
- 500.0,
- 510.0,
- 520.0,
- 530.0,
- 540.0,
- 550.0,
- 560.0,
- 570.0,
- 580.0,
- 590.0,
- 600.0,
- 610.0,
- 620.0,
- 630.0,
- 640.0,
- 650.0,
- 660.0,
- 670.0,
- 680.0,
- 690.0,
- 700.0,
- 710.0,
- 720.0,
- 730.0,
- 740.0,
- 750.0,
- 760.0,
- 770.0,
- 780.0,
- 790.0,
- 800.0,
- 810.0,
- 820.0,
- 830.0,
- 840.0,
- 850.0,
- 860.0,
- 870.0,
- 880.0,
- 890.0,
- 900.0,
- 910.0,
- 920.0,
- 930.0,
- 940.0,
- 950.0,
- 960.0,
- 970.0,
- 980.0,
- 990.0,
- 1000.0,
- 1010.0,
- 1020.0,
- 1030.0,
- 1040.0,
- 1050.0,
- 1060.0,
- 1070.0,
- 1080.0,
- 1090.0,
- 1100.0,
- 1110.0,
- 1120.0,
- 1130.0,
- 1140.0,
- 1150.0,
- 1160.0,
- 1170.0,
- 1180.0,
- 1190.0,
- 1200.0,
- 1210.0,
- 1220.0,
- 1230.0,
- 1240.0,
- 1250.0,
- 1260.0,
- 1270.0,
- 1280.0,
- 1290.0,
- 1300.0,
- 1310.0,
- 1320.0,
- 1330.0,
- 1340.0,
- 1350.0,
- 1360.0,
- 1370.0,
- 1380.0,
- 1390.0,
- 1400.0,
- 1410.0,
- 1420.0,
- 1430.0,
- 1440.0,
- 1450.0,
- 1460.0,
- 1470.0,
- 1480.0,
- 1490.0,
- 1500.0,
- 1510.0,
- 1520.0,
- 1530.0,
- 1540.0,
- 1550.0,
- 1560.0,
- 1570.0,
- 1580.0,
- 1590.0,
- 1600.0,
- 1610.0,
- 1620.0,
- 1630.0,
- 1640.0,
- 1650.0,
- 1660.0,
- 1670.0,
- 1680.0,
- 1690.0,
- 1700.0,
- 1710.0,
- 1720.0,
- 1730.0,
- 1740.0,
- 1750.0,
- 1760.0,
- 1770.0,
- 1780.0,
- 1790.0,
- 1800.0,
- 1810.0,
- 1820.0,
- 1830.0,
- 1840.0,
- 1850.0,
- 1860.0,
- 1870.0,
- 1880.0,
- 1890.0,
- 1900.0,
- 1910.0,
- 1920.0,
- 1930.0,
- 1940.0,
- 1950.0,
- 1960.0,
- 1970.0,
- 1980.0,
- 1990.0,
- 2000.0,
- 2010.0,
- 2020.0,
- 2030.0,
- 2040.0,
- 2050.0,
- 2060.0,
- 2070.0,
- 2080.0,
- 2090.0,
- 2100.0,
- 2110.0,
- 2120.0,
- 2130.0,
- 2140.0,
- 2150.0,
- 2160.0,
- 2170.0,
- 2180.0,
- 2190.0,
- 2200.0,
- 2210.0,
- 2220.0,
- 2230.0,
- 2240.0,
- 2250.0,
- 2260.0,
- 2270.0,
- 2280.0,
- 2290.0,
- 2300.0,
- 2310.0,
- 2320.0,
- 2330.0,
- 2340.0,
- 2350.0,
- 2360.0,
- 2370.0,
- 2380.0,
- 2390.0,
- 2400.0,
- 2410.0,
- 2420.0,
- 2430.0,
- 2440.0,
- 2450.0,
- 2460.0,
- 2470.0,
- 2480.0,
- 2490.0,
- 2500.0,
- 2510.0,
- 2520.0,
- 2530.0,
- 2540.0,
- 2550.0,
- 2560.0,
- 2570.0,
- 2580.0,
- 2590.0,
- 2600.0,
- 2610.0,
- 2620.0,
- 2630.0,
- 2640.0,
- 2650.0,
- 2660.0,
- 2670.0,
- 2680.0,
- 2690.0,
- 2700.0,
- 2710.0,
- 2720.0,
- 2730.0,
- 2740.0,
- 2750.0,
- 2760.0,
- 2770.0,
- 2780.0,
- 2790.0,
- 2800.0,
- 2810.0,
- 2820.0,
- 2830.0,
- 2840.0,
- 2850.0,
- 2860.0,
- 2870.0,
- 2880.0,
- 2890.0,
- 2900.0,
- 2910.0,
- 2920.0,
- 2930.0,
- 2940.0,
- 2950.0,
- 2960.0,
- 2970.0,
- 2980.0,
- 2990.0,
- 3000.0,
- 3010.0,
- 3020.0,
- 3030.0,
- 3040.0,
- 3050.0,
- 3060.0,
- 3070.0,
- 3080.0,
- 3090.0,
- 3100.0,
- 3110.0,
- 3120.0,
- 3130.0,
- 3140.0,
- 3150.0,
- 3160.0,
- 3170.0,
- 3180.0,
- 3190.0,
- 3200.0,
- 3210.0,
- 3220.0,
- 3230.0,
- 3240.0,
- 3250.0,
- 3260.0,
- 3270.0,
- 3280.0,
- 3290.0,
- 3300.0,
- 3310.0,
- 3320.0,
- 3330.0,
- 3340.0,
- 3350.0,
- 3360.0,
- 3370.0,
- 3380.0,
- 3390.0,
- 3400.0,
- 3410.0,
- 3420.0,
- 3430.0,
- 3440.0,
- 3450.0,
- 3460.0,
- 3470.0,
- 3480.0,
- 3490.0,
- 3500.0,
- 3500.0,
- 3490.0,
- 3480.0,
- 3470.0,
- 3460.0,
- 3450.0,
- 3440.0,
- 3430.0,
- 3420.0,
- 3410.0,
- 3400.0,
- 3390.0,
- 3380.0,
- 3370.0,
- 3360.0,
- 3350.0,
- 3340.0,
- 3330.0,
- 3320.0,
- 3310.0,
- 3300.0,
- 3290.0,
- 3280.0,
- 3270.0,
- 3260.0,
- 3250.0,
- 3240.0,
- 3230.0,
- 3220.0,
- 3210.0,
- 3200.0,
- 3190.0,
- 3180.0,
- 3170.0,
- 3160.0,
- 3150.0,
- 3140.0,
- 3130.0,
- 3120.0,
- 3110.0,
- 3100.0,
- 3090.0,
- 3080.0,
- 3070.0,
- 3060.0,
- 3050.0,
- 3040.0,
- 3030.0,
- 3020.0,
- 3010.0,
- 3000.0,
- 2990.0,
- 2980.0,
- 2970.0,
- 2960.0,
- 2950.0,
- 2940.0,
- 2930.0,
- 2920.0,
- 2910.0,
- 2900.0,
- 2890.0,
- 2880.0,
- 2870.0,
- 2860.0,
- 2850.0,
- 2840.0,
- 2830.0,
- 2820.0,
- 2810.0,
- 2800.0,
- 2790.0,
- 2780.0,
- 2770.0,
- 2760.0,
- 2750.0,
- 2740.0,
- 2730.0,
- 2720.0,
- 2710.0,
- 2700.0,
- 2690.0,
- 2680.0,
- 2670.0,
- 2660.0,
- 2650.0,
- 2640.0,
- 2630.0,
- 2620.0,
- 2610.0,
- 2600.0,
- 2590.0,
- 2580.0,
- 2570.0,
- 2560.0,
- 2550.0,
- 2540.0,
- 2530.0,
- 2520.0,
- 2510.0,
- 2500.0,
- 2490.0,
- 2480.0,
- 2470.0,
- 2460.0,
- 2450.0,
- 2440.0,
- 2430.0,
- 2420.0,
- 2410.0,
- 2400.0,
- 2390.0,
- 2380.0,
- 2370.0,
- 2360.0,
- 2350.0,
- 2340.0,
- 2330.0,
- 2320.0,
- 2310.0,
- 2300.0,
- 2290.0,
- 2280.0,
- 2270.0,
- 2260.0,
- 2250.0,
- 2240.0,
- 2230.0,
- 2220.0,
- 2210.0,
- 2200.0,
- 2190.0,
- 2180.0,
- 2170.0,
- 2160.0,
- 2150.0,
- 2140.0,
- 2130.0,
- 2120.0,
- 2110.0,
- 2100.0,
- 2090.0,
- 2080.0,
- 2070.0,
- 2060.0,
- 2050.0,
- 2040.0,
- 2030.0,
- 2020.0,
- 2010.0,
- 2000.0,
- 1990.0,
- 1980.0,
- 1970.0,
- 1960.0,
- 1950.0,
- 1940.0,
- 1930.0,
- 1920.0,
- 1910.0,
- 1900.0,
- 1890.0,
- 1880.0,
- 1870.0,
- 1860.0,
- 1850.0,
- 1840.0,
- 1830.0,
- 1820.0,
- 1810.0,
- 1800.0,
- 1790.0,
- 1780.0,
- 1770.0,
- 1760.0,
- 1750.0,
- 1740.0,
- 1730.0,
- 1720.0,
- 1710.0,
- 1700.0,
- 1690.0,
- 1680.0,
- 1670.0,
- 1660.0,
- 1650.0,
- 1640.0,
- 1630.0,
- 1620.0,
- 1610.0,
- 1600.0,
- 1590.0,
- 1580.0,
- 1570.0,
- 1560.0,
- 1550.0,
- 1540.0,
- 1530.0,
- 1520.0,
- 1510.0,
- 1500.0,
- 1490.0,
- 1480.0,
- 1470.0,
- 1460.0,
- 1450.0,
- 1440.0,
- 1430.0,
- 1420.0,
- 1410.0,
- 1400.0,
- 1390.0,
- 1380.0,
- 1370.0,
- 1360.0,
- 1350.0,
- 1340.0,
- 1330.0,
- 1320.0,
- 1310.0,
- 1300.0,
- 1290.0,
- 1280.0,
- 1270.0,
- 1260.0,
- 1250.0,
- 1240.0,
- 1230.0,
- 1220.0,
- 1210.0,
- 1200.0,
- 1190.0,
- 1180.0,
- 1170.0,
- 1160.0,
- 1150.0,
- 1140.0,
- 1130.0,
- 1120.0,
- 1110.0,
- 1100.0,
- 1090.0,
- 1080.0,
- 1070.0,
- 1060.0,
- 1050.0,
- 1040.0,
- 1030.0,
- 1020.0,
- 1010.0,
- 1000.0,
- 990.0,
- 980.0,
- 970.0,
- 960.0,
- 950.0,
- 940.0,
- 930.0,
- 920.0,
- 910.0,
- 900.0,
- 890.0,
- 880.0,
- 870.0,
- 860.0,
- 850.0,
- 840.0,
- 830.0,
- 820.0,
- 810.0,
- 800.0,
- 790.0,
- 780.0,
- 770.0,
- 760.0,
- 750.0,
- 740.0,
- 730.0,
- 720.0,
- 710.0,
- 700.0,
- 690.0,
- 680.0,
- 670.0,
- 660.0,
- 650.0,
- 640.0,
- 630.0,
- 620.0,
- 610.0,
- 600.0,
- 590.0,
- 580.0,
- 570.0,
- 560.0,
- 550.0,
- 540.0,
- 530.0,
- 520.0,
- 510.0,
- 500.0,
- 490.0,
- 480.0,
- 470.0,
- 460.0,
- 450.0,
- 440.0,
- 430.0,
- 420.0,
- 410.0,
- 400.0,
- 390.0,
- 380.0,
- 370.0,
- 360.0,
- 350.0,
- 340.0,
- 330.0,
- 320.0,
- 310.0,
- 300.0,
- 290.0,
- 280.0,
- 270.0,
- 260.0,
- 250.0,
- 240.0,
- 230.0,
- 220.0,
- 210.0,
- 200.0,
- 190.0,
- 180.0,
- 170.0,
- 160.0,
- 150.0,
- 140.0,
- 130.0,
- 120.0,
- 110.0,
- 100.0,
- 90.0,
- 80.0,
- 70.0,
- 60.0,
- 50.0,
- 40.0,
- 30.0,
- 20.0,
- 10.0,
- 0.0
+ 0,
+ 10,
+ 20,
+ 30,
+ 40,
+ 50,
+ 60,
+ 70,
+ 80,
+ 90,
+ 100,
+ 110,
+ 120,
+ 130,
+ 140,
+ 150,
+ 160,
+ 170,
+ 180,
+ 190,
+ 200,
+ 210,
+ 220,
+ 230,
+ 240,
+ 250,
+ 260,
+ 270,
+ 280,
+ 290,
+ 300,
+ 310,
+ 320,
+ 330,
+ 340,
+ 350,
+ 360,
+ 370,
+ 380,
+ 390,
+ 400,
+ 410,
+ 420,
+ 430,
+ 440,
+ 450,
+ 460,
+ 470,
+ 480,
+ 490,
+ 500,
+ 510,
+ 520,
+ 530,
+ 540,
+ 550,
+ 560,
+ 570,
+ 580,
+ 590,
+ 600,
+ 610,
+ 620,
+ 630,
+ 640,
+ 650,
+ 660,
+ 670,
+ 680,
+ 690,
+ 700,
+ 710,
+ 720,
+ 730,
+ 740,
+ 750,
+ 760,
+ 770,
+ 780,
+ 790,
+ 800,
+ 810,
+ 820,
+ 830,
+ 840,
+ 850,
+ 860,
+ 870,
+ 880,
+ 890,
+ 900,
+ 910,
+ 920,
+ 930,
+ 940,
+ 950,
+ 960,
+ 970,
+ 980,
+ 990,
+ 1000,
+ 1010,
+ 1020,
+ 1030,
+ 1040,
+ 1050,
+ 1060,
+ 1070,
+ 1080,
+ 1090,
+ 1100,
+ 1110,
+ 1120,
+ 1130,
+ 1140,
+ 1150,
+ 1160,
+ 1170,
+ 1180,
+ 1190,
+ 1200,
+ 1210,
+ 1220,
+ 1230,
+ 1240,
+ 1250,
+ 1260,
+ 1270,
+ 1280,
+ 1290,
+ 1300,
+ 1310,
+ 1320,
+ 1330,
+ 1340,
+ 1350,
+ 1360,
+ 1370,
+ 1380,
+ 1390,
+ 1400,
+ 1410,
+ 1420,
+ 1430,
+ 1440,
+ 1450,
+ 1460,
+ 1470,
+ 1480,
+ 1490,
+ 1500,
+ 1510,
+ 1520,
+ 1530,
+ 1540,
+ 1550,
+ 1560,
+ 1570,
+ 1580,
+ 1590,
+ 1600,
+ 1610,
+ 1620,
+ 1630,
+ 1640,
+ 1650,
+ 1660,
+ 1670,
+ 1680,
+ 1690,
+ 1700,
+ 1710,
+ 1720,
+ 1730,
+ 1740,
+ 1750,
+ 1760,
+ 1770,
+ 1780,
+ 1790,
+ 1800,
+ 1810,
+ 1820,
+ 1830,
+ 1840,
+ 1850,
+ 1860,
+ 1870,
+ 1880,
+ 1890,
+ 1900,
+ 1910,
+ 1920,
+ 1930,
+ 1940,
+ 1950,
+ 1960,
+ 1970,
+ 1980,
+ 1990,
+ 2000,
+ 2010,
+ 2020,
+ 2030,
+ 2040,
+ 2050,
+ 2060,
+ 2070,
+ 2080,
+ 2090,
+ 2100,
+ 2110,
+ 2120,
+ 2130,
+ 2140,
+ 2150,
+ 2160,
+ 2170,
+ 2180,
+ 2190,
+ 2200,
+ 2210,
+ 2220,
+ 2230,
+ 2240,
+ 2250,
+ 2260,
+ 2270,
+ 2280,
+ 2290,
+ 2300,
+ 2310,
+ 2320,
+ 2330,
+ 2340,
+ 2350,
+ 2360,
+ 2370,
+ 2380,
+ 2390,
+ 2400,
+ 2410,
+ 2420,
+ 2430,
+ 2440,
+ 2450,
+ 2460,
+ 2470,
+ 2480,
+ 2490,
+ 2500,
+ 2510,
+ 2520,
+ 2530,
+ 2540,
+ 2550,
+ 2560,
+ 2570,
+ 2580,
+ 2590,
+ 2600,
+ 2610,
+ 2620,
+ 2630,
+ 2640,
+ 2650,
+ 2660,
+ 2670,
+ 2680,
+ 2690,
+ 2700,
+ 2710,
+ 2720,
+ 2730,
+ 2740,
+ 2750,
+ 2760,
+ 2770,
+ 2780,
+ 2790,
+ 2800,
+ 2810,
+ 2820,
+ 2830,
+ 2840,
+ 2850,
+ 2860,
+ 2870,
+ 2880,
+ 2890,
+ 2900,
+ 2910,
+ 2920,
+ 2930,
+ 2940,
+ 2950,
+ 2960,
+ 2970,
+ 2980,
+ 2990,
+ 3000,
+ 3010,
+ 3020,
+ 3030,
+ 3040,
+ 3050,
+ 3060,
+ 3070,
+ 3080,
+ 3090,
+ 3100,
+ 3110,
+ 3120,
+ 3130,
+ 3140,
+ 3150,
+ 3160,
+ 3170,
+ 3180,
+ 3190,
+ 3200,
+ 3210,
+ 3220,
+ 3230,
+ 3240,
+ 3250,
+ 3260,
+ 3270,
+ 3280,
+ 3290,
+ 3300,
+ 3310,
+ 3320,
+ 3330,
+ 3340,
+ 3350,
+ 3360,
+ 3370,
+ 3380,
+ 3390,
+ 3400,
+ 3410,
+ 3420,
+ 3430,
+ 3440,
+ 3450,
+ 3460,
+ 3470,
+ 3480,
+ 3490,
+ 3500,
+ 3500,
+ 3490,
+ 3480,
+ 3470,
+ 3460,
+ 3450,
+ 3440,
+ 3430,
+ 3420,
+ 3410,
+ 3400,
+ 3390,
+ 3380,
+ 3370,
+ 3360,
+ 3350,
+ 3340,
+ 3330,
+ 3320,
+ 3310,
+ 3300,
+ 3290,
+ 3280,
+ 3270,
+ 3260,
+ 3250,
+ 3240,
+ 3230,
+ 3220,
+ 3210,
+ 3200,
+ 3190,
+ 3180,
+ 3170,
+ 3160,
+ 3150,
+ 3140,
+ 3130,
+ 3120,
+ 3110,
+ 3100,
+ 3090,
+ 3080,
+ 3070,
+ 3060,
+ 3050,
+ 3040,
+ 3030,
+ 3020,
+ 3010,
+ 3000,
+ 2990,
+ 2980,
+ 2970,
+ 2960,
+ 2950,
+ 2940,
+ 2930,
+ 2920,
+ 2910,
+ 2900,
+ 2890,
+ 2880,
+ 2870,
+ 2860,
+ 2850,
+ 2840,
+ 2830,
+ 2820,
+ 2810,
+ 2800,
+ 2790,
+ 2780,
+ 2770,
+ 2760,
+ 2750,
+ 2740,
+ 2730,
+ 2720,
+ 2710,
+ 2700,
+ 2690,
+ 2680,
+ 2670,
+ 2660,
+ 2650,
+ 2640,
+ 2630,
+ 2620,
+ 2610,
+ 2600,
+ 2590,
+ 2580,
+ 2570,
+ 2560,
+ 2550,
+ 2540,
+ 2530,
+ 2520,
+ 2510,
+ 2500,
+ 2490,
+ 2480,
+ 2470,
+ 2460,
+ 2450,
+ 2440,
+ 2430,
+ 2420,
+ 2410,
+ 2400,
+ 2390,
+ 2380,
+ 2370,
+ 2360,
+ 2350,
+ 2340,
+ 2330,
+ 2320,
+ 2310,
+ 2300,
+ 2290,
+ 2280,
+ 2270,
+ 2260,
+ 2250,
+ 2240,
+ 2230,
+ 2220,
+ 2210,
+ 2200,
+ 2190,
+ 2180,
+ 2170,
+ 2160,
+ 2150,
+ 2140,
+ 2130,
+ 2120,
+ 2110,
+ 2100,
+ 2090,
+ 2080,
+ 2070,
+ 2060,
+ 2050,
+ 2040,
+ 2030,
+ 2020,
+ 2010,
+ 2000,
+ 1990,
+ 1980,
+ 1970,
+ 1960,
+ 1950,
+ 1940,
+ 1930,
+ 1920,
+ 1910,
+ 1900,
+ 1890,
+ 1880,
+ 1870,
+ 1860,
+ 1850,
+ 1840,
+ 1830,
+ 1820,
+ 1810,
+ 1800,
+ 1790,
+ 1780,
+ 1770,
+ 1760,
+ 1750,
+ 1740,
+ 1730,
+ 1720,
+ 1710,
+ 1700,
+ 1690,
+ 1680,
+ 1670,
+ 1660,
+ 1650,
+ 1640,
+ 1630,
+ 1620,
+ 1610,
+ 1600,
+ 1590,
+ 1580,
+ 1570,
+ 1560,
+ 1550,
+ 1540,
+ 1530,
+ 1520,
+ 1510,
+ 1500,
+ 1490,
+ 1480,
+ 1470,
+ 1460,
+ 1450,
+ 1440,
+ 1430,
+ 1420,
+ 1410,
+ 1400,
+ 1390,
+ 1380,
+ 1370,
+ 1360,
+ 1350,
+ 1340,
+ 1330,
+ 1320,
+ 1310,
+ 1300,
+ 1290,
+ 1280,
+ 1270,
+ 1260,
+ 1250,
+ 1240,
+ 1230,
+ 1220,
+ 1210,
+ 1200,
+ 1190,
+ 1180,
+ 1170,
+ 1160,
+ 1150,
+ 1140,
+ 1130,
+ 1120,
+ 1110,
+ 1100,
+ 1090,
+ 1080,
+ 1070,
+ 1060,
+ 1050,
+ 1040,
+ 1030,
+ 1020,
+ 1010,
+ 1000,
+ 990,
+ 980,
+ 970,
+ 960,
+ 950,
+ 940,
+ 930,
+ 920,
+ 910,
+ 900,
+ 890,
+ 880,
+ 870,
+ 860,
+ 850,
+ 840,
+ 830,
+ 820,
+ 810,
+ 800,
+ 790,
+ 780,
+ 770,
+ 760,
+ 750,
+ 740,
+ 730,
+ 720,
+ 710,
+ 700,
+ 690,
+ 680,
+ 670,
+ 660,
+ 650,
+ 640,
+ 630,
+ 620,
+ 610,
+ 600,
+ 590,
+ 580,
+ 570,
+ 560,
+ 550,
+ 540,
+ 530,
+ 520,
+ 510,
+ 500,
+ 490,
+ 480,
+ 470,
+ 460,
+ 450,
+ 440,
+ 430,
+ 420,
+ 410,
+ 400,
+ 390,
+ 380,
+ 370,
+ 360,
+ 350,
+ 340,
+ 330,
+ 320,
+ 310,
+ 300,
+ 290,
+ 280,
+ 270,
+ 260,
+ 250,
+ 240,
+ 230,
+ 220,
+ 210,
+ 200,
+ 190,
+ 180,
+ 170,
+ 160,
+ 150,
+ 140,
+ 130,
+ 120,
+ 110,
+ 100,
+ 90,
+ 80,
+ 70,
+ 60,
+ 50,
+ 40,
+ 30,
+ 20,
+ 10,
+ 0
],
"y": [
4.0531258862154065,
@@ -1744,7 +1745,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -1780,7 +1781,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -1804,7 +1805,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -1840,7 +1841,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -1867,7 +1868,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -1903,7 +1904,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -1918,7 +1919,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -1954,7 +1955,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -2110,7 +2111,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -2146,7 +2147,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -2237,7 +2238,7 @@
],
"sequential": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -2273,13 +2274,13 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
"sequentialminus": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -2315,7 +2316,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
]
@@ -2503,7 +2504,7 @@
{
"colorscale": [
[
- 0.0,
+ 0,
"#440154"
],
[
@@ -2539,7 +2540,7 @@
"#b5de2b"
],
[
- 1.0,
+ 1,
"#fde725"
]
],
@@ -15963,7 +15964,7 @@
"hoverinfo": "text",
"marker": {
"color": [
- 0.0,
+ 0,
0.001968503937007874,
0.003937007874015748,
0.005905511811023622,
@@ -16474,7 +16475,7 @@
],
"colorscale": [
[
- 0.0,
+ 0,
"rgb(255,255,255)"
],
[
@@ -16506,7 +16507,7 @@
"rgb(37,37,37)"
],
[
- 1.0,
+ 1,
"rgb(0,0,0)"
]
],
@@ -18165,7 +18166,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18201,7 +18202,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -18225,7 +18226,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18261,7 +18262,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -18288,7 +18289,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18324,7 +18325,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -18339,7 +18340,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18375,7 +18376,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -18531,7 +18532,7 @@
},
"colorscale": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18567,7 +18568,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
@@ -18658,7 +18659,7 @@
],
"sequential": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18694,13 +18695,13 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
],
"sequentialminus": [
[
- 0.0,
+ 0,
"#0d0887"
],
[
@@ -18736,7 +18737,7 @@
"#fdca26"
],
[
- 1.0,
+ 1,
"#f0f921"
]
]
diff --git a/examples/notebooks/battery_parameterisation/sensitivity_analysis_salib.ipynb b/examples/notebooks/battery_parameterisation/sensitivity_analysis_salib.ipynb
index e7a1a308c..a106a0ddc 100644
--- a/examples/notebooks/battery_parameterisation/sensitivity_analysis_salib.ipynb
+++ b/examples/notebooks/battery_parameterisation/sensitivity_analysis_salib.ipynb
@@ -58,7 +58,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"matplotlib\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/comparison_examples/comparing_cost_functions.ipynb b/examples/notebooks/comparison_examples/comparing_cost_functions.ipynb
index c15c9b5a1..880745578 100644
--- a/examples/notebooks/comparison_examples/comparing_cost_functions.ipynb
+++ b/examples/notebooks/comparison_examples/comparing_cost_functions.ipynb
@@ -26,8 +26,8 @@
"\n",
"import pybop\n",
"\n",
- "go = pybop.plot.PlotlyManager().go\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "go = pybop.plot.backends.PlotlyManager().go\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/comparison_examples/optimiser_calibration.ipynb b/examples/notebooks/comparison_examples/optimiser_calibration.ipynb
index 1db386e24..358593105 100644
--- a/examples/notebooks/comparison_examples/optimiser_calibration.ipynb
+++ b/examples/notebooks/comparison_examples/optimiser_calibration.ipynb
@@ -32,7 +32,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/design_optimisation/energy_based_electrode_design.ipynb b/examples/notebooks/design_optimisation/energy_based_electrode_design.ipynb
index 6250b85e9..09092ba4d 100644
--- a/examples/notebooks/design_optimisation/energy_based_electrode_design.ipynb
+++ b/examples/notebooks/design_optimisation/energy_based_electrode_design.ipynb
@@ -35,7 +35,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/getting_started/cost_compute_methods.ipynb b/examples/notebooks/getting_started/cost_compute_methods.ipynb
index 125ae9413..3ba1717c4 100644
--- a/examples/notebooks/getting_started/cost_compute_methods.ipynb
+++ b/examples/notebooks/getting_started/cost_compute_methods.ipynb
@@ -28,7 +28,7 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/getting_started/maximum_a_posteriori.ipynb b/examples/notebooks/getting_started/maximum_a_posteriori.ipynb
index 270fb722b..fc93c39b2 100644
--- a/examples/notebooks/getting_started/maximum_a_posteriori.ipynb
+++ b/examples/notebooks/getting_started/maximum_a_posteriori.ipynb
@@ -51,7 +51,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/getting_started/optimising_with_adamw.ipynb b/examples/notebooks/getting_started/optimising_with_adamw.ipynb
index 9248b0b8a..6d1c12b5f 100644
--- a/examples/notebooks/getting_started/optimising_with_adamw.ipynb
+++ b/examples/notebooks/getting_started/optimising_with_adamw.ipynb
@@ -34,7 +34,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/getting_started/setting_optimiser_options.ipynb b/examples/notebooks/getting_started/setting_optimiser_options.ipynb
index a790b26b2..0ae9270db 100644
--- a/examples/notebooks/getting_started/setting_optimiser_options.ipynb
+++ b/examples/notebooks/getting_started/setting_optimiser_options.ipynb
@@ -32,7 +32,7 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/notebooks/getting_started/using_transformations.ipynb b/examples/notebooks/getting_started/using_transformations.ipynb
index b252dc04d..d8087213c 100644
--- a/examples/notebooks/getting_started/using_transformations.ipynb
+++ b/examples/notebooks/getting_started/using_transformations.ipynb
@@ -29,7 +29,8 @@
"\n",
"import pybop\n",
"\n",
- "pybop.plot.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
+ "pybop.plot.use_backend(\"plotly\")\n",
+ "pybop.plot.backends.PlotlyManager().pio.renderers.default = \"notebook_connected\"\n",
"\n",
"np.random.seed(8) # users can remove this line"
]
diff --git a/examples/scripts/battery_parameterisation/bayesian_feature_fitting.py b/examples/scripts/battery_parameterisation/bayesian_feature_fitting.py
index 8c68af73a..b4d5de8f5 100644
--- a/examples/scripts/battery_parameterisation/bayesian_feature_fitting.py
+++ b/examples/scripts/battery_parameterisation/bayesian_feature_fitting.py
@@ -117,8 +117,18 @@
print("True values:", [original_D_n, original_D_p])
# Plot the optimisation result
- result.plot_convergence(yaxis={"type": "log"})
- result.plot_parameters(yaxis={"type": "log"}, yaxis2={"type": "log"})
+ pybop.plot.use_backend("plotly")
+ fig1 = result.plot_convergence(show=False)
+ fig1.update_layout(
+ yaxis={"type": "log"}
+ ) # use ax.set_yscale('log') if using matplotlib (where ax = fig1.gca())
+ fig1.show()
+
+ fig2 = result.plot_parameters(show=False)
+ fig2.update_layout(
+ yaxis={"type": "log"}, yaxis2={"type": "log"}
+ ) # use ax.set_yscale('log') if using matplotlib (for ax in fig2.axes)
+ fig2.show()
# Plot the prior and posterior distributions
pybop.plot.distribution(result.problem.parameters, result.posterior)
diff --git a/examples/scripts/battery_parameterisation/gitt_fitting.py b/examples/scripts/battery_parameterisation/gitt_fitting.py
index 3582f8e5c..6abe016f2 100644
--- a/examples/scripts/battery_parameterisation/gitt_fitting.py
+++ b/examples/scripts/battery_parameterisation/gitt_fitting.py
@@ -91,7 +91,7 @@
fitted_values["Voltage [V]"].data,
solution["Voltage [V]"].data,
],
- trace_names=["Ground truth", "Fitted GITT Model", "Identified Model"],
+ labels=["Ground truth", "Fitted GITT Model", "Identified Model"],
xaxis_title="Time / s",
yaxis_title="Voltage / V",
)
diff --git a/examples/scripts/battery_parameterisation/ocp_averaging.py b/examples/scripts/battery_parameterisation/ocp_averaging.py
index 9760b1672..cbf3479a5 100644
--- a/examples/scripts/battery_parameterisation/ocp_averaging.py
+++ b/examples/scripts/battery_parameterisation/ocp_averaging.py
@@ -110,15 +110,13 @@
charge_dataset["Voltage [V]"],
average_dataset["Voltage [V]"],
]
- trace_names = ["Discharge", "Charge", "Averaged"]
- legend = dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
+ labels = ["Discharge", "Charge", "Averaged"]
fig = pybop.plot.trajectories(
x=stos,
y=volt,
- trace_names=trace_names,
+ labels=labels,
xaxis_title="Stoichiometry",
yaxis_title="Voltage [V]",
- legend=legend,
)
dcap = [
@@ -131,8 +129,7 @@
fig = pybop.plot.trajectories(
x=stos,
y=dcap,
- trace_names=trace_names,
+ labels=labels,
xaxis_title="Stoichiometry",
yaxis_title="Differential capacity [V-1]",
- legend=legend,
)
diff --git a/examples/scripts/battery_parameterisation/stoichiometry_fitting.py b/examples/scripts/battery_parameterisation/stoichiometry_fitting.py
index adbdc09b1..394f0e3f5 100644
--- a/examples/scripts/battery_parameterisation/stoichiometry_fitting.py
+++ b/examples/scripts/battery_parameterisation/stoichiometry_fitting.py
@@ -30,7 +30,7 @@
parameter_values["Positive electrode OCP [V]"](stoichiometry),
fitted_dataset["Voltage [V]"],
],
- trace_names=["Ground truth", "Data vs. stoichiometry"],
+ labels=["Ground truth", "Data vs. stoichiometry"],
xaxis_title="Stoichiometry",
yaxis_title="Voltage / V",
)
diff --git a/examples/scripts/comparison_examples/grouped_SPMe.py b/examples/scripts/comparison_examples/grouped_SPMe.py
index de4c897ab..66092c12b 100644
--- a/examples/scripts/comparison_examples/grouped_SPMe.py
+++ b/examples/scripts/comparison_examples/grouped_SPMe.py
@@ -11,11 +11,9 @@
"""
# Prepare figure
-layout_options = dict(
- xaxis_title="Time / s",
- yaxis_title="Voltage / V",
-)
-plot_dict = pybop.plot.StandardPlot(layout_options=layout_options)
+fig = plt.figure()
+plt.xlabel("Time / s")
+plt.ylabel("Voltage / V")
# Use the Chen2020 parameters
parameter_values = pybamm.ParameterValues("Chen2020")
@@ -46,20 +44,19 @@
)
SPMe_model = pybamm.lithium_ion.SPMe(options=model_options)
grouped_SPMe_model = pybop.lithium_ion.GroupedSPMe(options=model_options)
-for model, param, line_style in zip(
+for model, param, linestyle in zip(
[SPMe_model, grouped_SPMe_model],
[parameter_values, grouped_parameter_values],
- ["solid", "dash"],
+ ["-", "--"],
strict=False,
):
solution = pybamm.Simulation(
model, parameter_values=param, experiment=experiment, cache_esoh=False
).solve(initial_soc=init_soc)
dataset = pybop.import_pybamm_solution(solution)
- plot_dict.add_traces(
- dataset["Time [s]"], dataset["Voltage [V]"], line_dash=line_style
+ plt.plot(
+ dataset["Time [s]"], dataset["Voltage [V]"], label=None, linestyle=linestyle
)
-plot_dict()
# Set up figure
fig, ax = plt.subplots()
diff --git a/papers/joss/param_plots.py b/papers/joss/param_plots.py
index 298589b51..c65e2b08f 100644
--- a/papers/joss/param_plots.py
+++ b/papers/joss/param_plots.py
@@ -57,7 +57,7 @@
simulation_plot_dict = pybop.plot.StandardPlot(
x=solution["Time [s]"].data,
y=[corrupt_values, solution["Battery open-circuit voltage [V]"].data, values],
- trace_names=[
+ labels=[
"Voltage w. noise",
"Open-circuit voltage",
"Voltage",
@@ -244,7 +244,7 @@
convergence_plot_dict = pybop.plot.StandardPlot(
x=iteration_numbers,
y=cost_log,
- trace_names=[cost.name],
+ labels=[cost.name],
trace_options={"line": {"width": 4, "dash": "dash"}},
)
convergence_traces.extend(convergence_plot_dict.traces)
@@ -323,7 +323,7 @@
convergence_plot_dict = pybop.plot.StandardPlot(
x=iteration_numbers,
y=cost_log,
- trace_names=cost.name
+ labels=cost.name
+ " "
+ (
cost.log_likelihood.name if isinstance(cost, pybop.LogPosterior) else ""
diff --git a/pybop/plot/__init__.py b/pybop/plot/__init__.py
index 8c669df92..6e97bc7e5 100644
--- a/pybop/plot/__init__.py
+++ b/pybop/plot/__init__.py
@@ -1,8 +1,21 @@
+# Plotting backend default
+DEFAULT_BACKEND = 'matplotlib'
+current_backend=DEFAULT_BACKEND
+
+from .util import (
+ AxisData,
+ use_backend,
+ get_backend,
+ parse_data,
+ remove_brackets,
+ wrap_text
+)
+
#
# Import plots
#
-from .plotly_manager import PlotlyManager
-from .standard_plots import StandardPlot, StandardSubplot, trajectories
+from .standard_plots import StandardPlot, StandardSubplot
+from .trajectories import trajectories
from .contour import contour
from .dataset import dataset
from .convergence import convergence
@@ -13,3 +26,6 @@
from .samples import trace, chains, posterior, summary_table
from .predictive import predictive
from .distribution import distribution
+
+# Import backend specific plotting functions
+from . import backends
diff --git a/pybop/plot/backends/__init__.py b/pybop/plot/backends/__init__.py
new file mode 100644
index 000000000..bb7b9443d
--- /dev/null
+++ b/pybop/plot/backends/__init__.py
@@ -0,0 +1,4 @@
+from .base import PlotBackend
+from .matplotlib import MatplotlibBackend
+from .plotly_manager import PlotlyManager
+from .plotly import PlotlyBackend
diff --git a/pybop/plot/backends/base.py b/pybop/plot/backends/base.py
new file mode 100644
index 000000000..029cc3c04
--- /dev/null
+++ b/pybop/plot/backends/base.py
@@ -0,0 +1,308 @@
+from abc import ABC, abstractmethod
+
+from pybop.plot.util import AxisData
+
+
+class PlotBackend(ABC):
+ """
+ Abstract base class defining a plotting backend interface.
+
+ Concrete implementations provide plotting functionality for a specific
+ visualization library (e.g. Plotly, Matplotlib) while exposing a common
+ API to the rest of the application.
+
+ Methods in this interface are responsible for creating figures, adding
+ traces and annotations, generating specialised plot types, and rendering
+ results.
+ """
+
+ @abstractmethod
+ def create_figure(
+ self,
+ title: str = None,
+ xaxis_title: str = None,
+ yaxis_title: str = None,
+ traces: list = None,
+ style: dict = None,
+ ):
+ """
+ Create and return a new figure.
+
+ Parameters
+ ----------
+ title : str, optional
+ Figure title.
+ xaxis_title : str, optional
+ X-axis label.
+ yaxis_title : str, optional
+ Y-axis label.
+ traces : list, optional
+ Initial traces to add to the figure.
+ style : dict, optional
+ Backend-specific styling options.
+
+ Returns
+ -------
+ object
+ Backend-specific figure object.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def make_subplots(
+ self,
+ axes: list[AxisData],
+ title=None,
+ xaxis_titles: list[str] | str = None,
+ yaxis_titles: list[str] | str = None,
+ style=None,
+ ):
+ """
+ Create a figure containing multiple subplot axes.
+
+ Parameters
+ ----------
+ axes : list[AxisData]
+ Definitions describing subplot layout and configuration.
+ title : str, optional
+ Figure title.
+ xaxis_titles : str or list[str], optional
+ X-axis titles for each subplot.
+ yaxis_titles : str or list[str], optional
+ Y-axis titles for each subplot.
+ style : dict, optional
+ Backend-specific styling options.
+
+ Returns
+ -------
+ object
+ Backend-specific figure object.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def legend(self, fig, style: dict = None):
+ """
+ Configure or display a legend for a figure.
+
+ Parameters
+ ----------
+ fig : object
+ Figure object.
+ style : dict, optional
+ Legend styling options.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def show_figure(self, fig):
+ """
+ Render or display a figure.
+
+ Parameters
+ ----------
+ fig : object
+ Figure to display.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def plot_trace(self, traces: dict | list[dict], fig, ax=None, color_cycle=None):
+ """
+ Add one or more traces to a figure or subplot.
+
+ Parameters
+ ----------
+ traces : dict or list[dict]
+ Trace definitions to plot.
+ fig : object
+ Target figure.
+ ax : object, optional
+ Target subplot axis.
+ color_cycle : iterable, optional
+ Sequence of colours used when plotting multiple traces.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def sample_color_scale(self, data, scale="viridis", d_min=None, d_max=None):
+ """
+ Map data values onto a colour scale.
+
+ Parameters
+ ----------
+ data : array-like
+ Values to colour-map.
+ scale : str, optional
+ Colour scale name.
+ d_min : float, optional
+ Lower bound for normalisation.
+ d_max : float, optional
+ Upper bound for normalisation.
+
+ Returns
+ -------
+ array-like
+ Colours corresponding to the supplied data.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def colorbar(self, fig, data, colorscale="viridis", label=None):
+ """
+ Add a colour bar representing a colour scale.
+
+ Parameters
+ ----------
+ fig : object
+ Target figure.
+ data : array-like
+ Data used for colour scaling.
+ colorscale : str, optional
+ Colour scale name.
+ label : str, optional
+ Colour bar label.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def contour_plot(self, x, y, z, colorscale="viridis"):
+ """
+ Create a contour plot.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Coordinate values.
+ z : array-like
+ Surface values.
+ colorscale : str, optional
+ Colour scale name.
+
+ Returns
+ -------
+ object
+ Backend-specific contour trace or figure.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def fill(self, x, y, color=None, label=None):
+ """
+ Create a filled region plot.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Coordinates defining the filled area.
+ color : str, optional
+ Fill colour.
+ label : str, optional
+ Legend label.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def fill_between(self, x, y_upper, y_lower, color):
+ """
+ Create a filled region between upper and lower bounds.
+
+ Parameters
+ ----------
+ x : array-like
+ X-axis values.
+ y_upper : array-like
+ Upper boundary values.
+ y_lower : array-like
+ Lower boundary values.
+ color : str
+ Fill colour.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def histogram_plot(self, x, name, style=None):
+ """
+ Create a histogram.
+
+ Parameters
+ ----------
+ x : array-like
+ Data to bin.
+ name : str
+ Histogram label.
+ style : dict, optional
+ Histogram styling options.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def line(self, x=None, y=None, label=None, style=None):
+ """
+ Create a line plot trace.
+
+ Parameters
+ ----------
+ x, y : array-like, optional
+ Coordinates of the line.
+ label : str, optional
+ Trace label.
+ style : dict, optional
+ Line styling options.
+
+ Returns
+ -------
+ object
+ Backend-specific line trace.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def scatter(self, x, y, colors, labels=None, colorscale="Greys"):
+ """
+ Create a scatter plot.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Point coordinates.
+ colors : array-like
+ Values or colours associated with each point.
+ labels : array-like, optional
+ Point labels.
+ colorscale : str, optional
+ Colour scale name.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def show_table(self, header, values, title):
+ """
+ Display tabular data.
+
+ Parameters
+ ----------
+ header : list
+ Column headers.
+ values : list
+ Table contents.
+ title : str
+ Table title.
+ """
+ raise NotImplementedError
+
+ @abstractmethod
+ def vline(self, fig, x, style=None):
+ """
+ Add a vertical reference line to a figure.
+
+ Parameters
+ ----------
+ fig : object
+ Target figure.
+ x : float
+ X-coordinate of the line.
+ style : dict, optional
+ Line styling options.
+ """
+ raise NotImplementedError
diff --git a/pybop/plot/backends/matplotlib.py b/pybop/plot/backends/matplotlib.py
new file mode 100644
index 000000000..872ecc8e5
--- /dev/null
+++ b/pybop/plot/backends/matplotlib.py
@@ -0,0 +1,583 @@
+import numpy as np
+
+from pybop.plot.backends.base import PlotBackend
+from pybop.plot.util import AxisData, wrap_text
+
+
+class MatplotlibBackend(PlotBackend):
+ """
+ Matplotlib implementation of the PlotBackend interface.
+ This backend converts backend-agnostic trace definitions into
+ Matplotlib figures, axes, and artists. Plot objects are represented
+ as dictionaries containing plotting arguments and metadata, allowing
+ higher-level plotting code to remain independent of the underlying
+ plotting library.
+ """
+
+ def __init__(self):
+ # Import matplotlib only when needed
+ import matplotlib as mpl
+ from matplotlib import pyplot as plt
+
+ self.mpl = mpl
+ self.plt = plt
+
+ # Backend identifier used by utility functions and text wrapping.
+ self.name = "matplotlib"
+
+ # Enable automatic colour cycling across subplots when traces do not
+ # explicitly define a colour.
+ self.global_colorcycle = False
+
+ # Matplotlib's default property cycle.
+ self.colorcycle = self.plt.rcParams["axes.prop_cycle"]()
+
+ # Layout rectangle reserved for tight_layout(). This may be adjusted
+ # when legends are placed outside the plotting area.
+ self.rect = [0, 0, 1, 1]
+
+ def _figsize(self, style):
+ """
+ Convert pixel-based width and height values from a style dictionary
+ into a Matplotlib figsize (inches).
+ """
+ return (
+ np.ceil(style.get("width", 800) / 100),
+ np.ceil(style.get("height", 600) / 100),
+ )
+
+ def create_figure(
+ self, title=None, xaxis_title=None, yaxis_title=None, traces=None, style=None
+ ):
+ """
+ Create a single-axis figure and optionally populate it with traces.
+
+ Parameters
+ ----------
+ title : str, optional
+ Figure title.
+ xaxis_title : str, optional
+ X-axis label.
+ yaxis_title : str, optional
+ Y-axis label.
+ traces : list[dict], optional
+ Trace definitions to plot immediately.
+ style : dict, optional
+ Figure styling options.
+ Currently supported options:
+ - width in pixels
+ - heith in pixels
+ - xaxis_range: range of the X-axis
+ - yaxis_range: range of the Y-axis
+ - bg_color: background color of the axis
+
+ Returns
+ -------
+ matplotlib.figure.Figure
+ Configured figure instance.
+ """
+ style = style or {}
+ fig = self.plt.figure(figsize=self._figsize(style), dpi=100)
+
+ if title is not None:
+ self.plt.suptitle(title)
+ if xaxis_title is not None:
+ self.plt.xlabel(xaxis_title)
+ if yaxis_title is not None:
+ self.plt.ylabel(yaxis_title)
+
+ # Apply backend-supported figure styling options.
+ if "xaxis_range" in style:
+ self.plt.xlim(style.get("xaxis_range"))
+ if "yaxis_range" in style:
+ self.plt.ylim(style.get("yaxis_range"))
+ if "bg_color" in style:
+ ax = fig.gca()
+ ax.set_facecolor(style.get("bg_color"))
+ ax.set_axisbelow(True)
+
+ if traces is not None:
+ for trace in traces:
+ self.plot_trace(trace, fig)
+ return fig
+
+ def make_subplots(
+ self,
+ axes: list[AxisData],
+ title=None,
+ xaxis_titles: list[str] | str = None,
+ yaxis_titles: list[str] | str = None,
+ style=None,
+ ):
+ """
+ Create a figure containing a custom subplot layout.
+
+ The layout is defined by a collection of AxisData objects, which
+ specify subplot positions and spans within a grid.
+
+ Parameters
+ ----------
+ title : str, optional
+ Figure title.
+ xaxis_title : str, optional
+ X-axis label.
+ yaxis_title : str, optional
+ Y-axis label.
+ traces : list[dict], optional
+ Trace definitions to plot immediately.
+ style : dict, optional
+ Figure styling options.
+ Currently supported options:
+ - width in pixels
+ - heith in pixels
+ - bg_color: background color of the axis
+
+ Returns
+ -------
+ matplotlib.figure.Figure
+ Configured figure instance.
+ """
+
+ style = style or {}
+
+ # Create figure
+ fig = self.plt.figure(figsize=self._figsize(style), dpi=100)
+ if title is not None:
+ self.plt.suptitle(title)
+
+ # Determine the minimum grid size required to accommodate all subplot spans.
+ num_rows = max(ax.row + ax.row_span - 1 for ax in axes)
+ num_cols = max(ax.col + ax.col_span - 1 for ax in axes)
+
+ axes_dict = {}
+ for ax in axes:
+ # Convert row/column span information into Matplotlib subplot indices.
+ idx_start = (ax.row - 1) * num_cols + ax.col
+ idx_end = (ax.row + ax.row_span - 2) * num_cols + ax.col + ax.col_span - 1
+ axes_dict[(ax.row, ax.col)] = fig.add_subplot(
+ num_rows, num_cols, (idx_start, idx_end)
+ )
+
+ # Helper to support either a shared axis title or per-axis titles.
+ def _get_axis_title(titles, i):
+ if isinstance(titles, str):
+ return titles
+ if isinstance(titles, list) and i < len(titles):
+ return titles[i]
+ return None
+
+ # Reduce wrapping width as the number of subplot rows increases.
+ width = np.floor(50 / num_rows)
+ for i, ax in enumerate(fig.axes):
+ if title := _get_axis_title(xaxis_titles, i):
+ ax.set_xlabel(wrap_text(title, width, self.name))
+ if title := _get_axis_title(yaxis_titles, i):
+ ax.set_ylabel(wrap_text(title, width, self.name))
+ if "bg_color" in style:
+ ax.set_facecolor(style.get("bg_color"))
+ ax.set_axisbelow(True)
+
+ # Use a shared colour cycle across all subplot axes.
+ self.global_colorcycle = True
+
+ return fig, axes_dict, num_rows, num_cols
+
+ def legend(self, fig, style: dict = None):
+ """
+ Create an axis-level or figure-level legend.
+
+ Supports legends positioned outside the plotting area and updates
+ the layout rectangle used by tight_layout() accordingly.
+
+ Parameters
+ ----------
+ fig : matplotlib.figure.Figure
+ The figure object
+ style : dict, optional
+ Legend styling options.
+ Currently supported options:
+ - loc: str
+ - coords: tuple - is translated into bbox_to_anchor
+ - outside: tuple(side : str, offset: float) places
+ the legend outside the plot, where the side (left,
+ right, top, bottom) determines on wich side of the plot
+ the legend is placed and the offset determines the fraction
+ of the figure height or width reserved for the legend.
+ Overrides loc and coords.
+ - fig_legend: if true, one legend is created for the entire figure, otherwise the legend is created
+ for the current axis.
+
+ """
+ style = style or {}
+ lines_labels = []
+ if style.get("fig_legend"):
+ axes = fig.axes
+ else:
+ axes = [fig.gca()]
+
+ # Configure external legend placement and reserve layout space.
+ if "outside" in style.keys():
+ side, offset = style.get("outside")
+ if side == "left":
+ style["loc"] = "upper left"
+ style["coords"] = (0.0, 1.0)
+ self.rect = [offset, 0, 1, 1]
+ elif side == "top":
+ style["loc"] = "lower right"
+ style["coords"] = (1.0, 1.0 - offset)
+ self.rect = [0, 0, 1, 1 - offset]
+ elif side == "bottom":
+ style["loc"] = "lower left"
+ style["coords"] = (0.0, 0.0)
+ self.rect = [0, offset, 1, 1]
+ else:
+ style["loc"] = "upper right"
+ style["coords"] = (1.0, 1.0)
+ self.rect = [0.0, 0, 1 - offset, 1]
+
+ # Collect legend entries from all relevant axes.
+ labels_in_fig = False
+ lines_labels = []
+ opts = {}
+ for ax in axes:
+ # Flatten handles and labels from multiple axes into a single legend.
+ handles, labels = ax.get_legend_handles_labels()
+ if handles:
+ lines_labels.append((handles, labels))
+ labels_in_fig = True
+
+ if labels_in_fig:
+ lines, labels = [sum(lol, []) for lol in zip(*lines_labels, strict=False)]
+ if style.get("horizontal"):
+ opts["ncols"] = len(lines)
+ if "coords" in style.keys():
+ opts["bbox_to_anchor"] = style.get("coords")
+ if style.get("fig_legend"):
+ opts["loc"] = style.get("loc", "upper right")
+ fig.legend(lines, labels, **opts)
+ else:
+ opts["loc"] = style.get("loc", "best")
+ axes[0].legend(lines, labels, **opts)
+
+ def show_figure(self, fig):
+ """
+ Apply final layout adjustments and display the figure.
+
+ Parameters
+ ----------
+ fig : matplotlib.figure.Figure
+ The figure object
+ """
+
+ if isinstance(fig, list):
+ for f in fig:
+ f.tight_layout(rect=self.rect)
+ else:
+ fig.tight_layout(rect=self.rect)
+ self.plt.show()
+
+ def plot_trace(self, traces: dict | list[dict], fig, ax=None):
+ """
+ Convert one or more trace definitions into Matplotlib plotting calls.
+
+ Parameters
+ ----------
+ traces: dict or list[dict]
+ Each trace dictionary specifies a plotting method, positional
+ arguments, and keyword arguments compatible with a Matplotlib Axes
+ method.
+ fig : matplotlib.figure.Figure
+ The figure object
+ ax : matplotlib axis object, optional
+ Specity an axis for plotting. Otherwise current axis is used.
+ """
+
+ traces = traces if isinstance(traces, list) else [traces]
+
+ # Extract plotting keyword arguments while removing backend metadata.
+ ax = ax or fig.gca()
+ for trace in traces:
+ if title := trace.get("xaxis_title"):
+ ax.set_xlabel(title)
+ if title := trace.get("yaxis_title"):
+ ax.set_ylabel(title)
+
+ options = {
+ k: v
+ for k, v in trace.items()
+ if k
+ not in {
+ "plot_type",
+ "positional_args",
+ "xaxis_title",
+ "yaxis_title",
+ }
+ }
+ plot_type = trace.get("plot_type", "plot")
+ args = trace.get("positional_args", ())
+
+ # Apply the global colour cycle when plotting standard line traces.
+ if self.global_colorcycle and plot_type == "plot":
+ options.update(next(self.colorcycle))
+ # Resolve the requested plotting method on the target axis.
+ plot_func = getattr(ax, plot_type)
+
+ obj = plot_func(*args, **options)
+
+ # Automatically attach a colourbar for filled contour plots.
+ if plot_type == "contourf":
+ self.plt.colorbar(obj)
+
+ def sample_color_scale(self, data, scale="viridis", d_min=None, d_max=None):
+ """
+ Map data values to RGBA colours using a Matplotlib colormap.
+
+ Parameters
+ ----------
+ data: ndarray
+ The data to be mapped
+ scale : str
+ Name of the colormap
+ d_min: float, optional
+ Minimum value to be mapped. Otherwise the minimum of the data is used.
+ d_max: float, optional
+ Maximum value to be mapped. Ohterwise maximum of the data is used.
+ """
+ # Normalise values into the range expected by the colormap.
+ d_min = d_min or np.nanmin(data[np.isfinite(data)])
+ d_max = d_max or np.nanmax(data[np.isfinite(data)])
+ norm = self.mpl.colors.Normalize(vmin=d_min, vmax=d_max, clip=True)
+ norm_d = norm(data, clip=True)
+ if np.isscalar(norm_d):
+ norm_d = [norm_d]
+
+ # Sample colours from the requested colormap.
+ cmap = self.mpl.colormaps[scale]
+ return cmap(norm_d)
+
+ def colorbar(self, fig, data, colorscale="viridis", label=None):
+ """
+ Add colourbar to figure
+
+ Parameters
+ ----------
+ fig: matplotlib.figure.Figure
+ The figure.
+ data: array-like
+ The data to be mapped
+ scale : str
+ Name of the colormap
+ label: str, optional
+ label to be displayed alongside colorbar
+ """
+ # Create a normalisation matching the supplied data range.
+ f_min = np.nanmin(data[np.isfinite(data)])
+ f_max = np.nanmax(data[np.isfinite(data)])
+ norm = self.mpl.colors.Normalize(vmin=f_min, vmax=f_max, clip=True)
+
+ # Create and attach a standalone colourbar.
+ cmap = self.mpl.colormaps[colorscale]
+ self.plt.colorbar(
+ self.mpl.cm.ScalarMappable(norm=norm, cmap=cmap), ax=fig.gca(), label=label
+ )
+
+ def contour_plot(self, x, y, z, colorscale="viridis"):
+ """
+ Return trace definitions for a filled contour plot and contour lines.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Coordinate values.
+ z : array-like
+ Surface values.
+ colorscale : str, optional
+ Colour scale name.
+
+ Returns
+ -------
+ object
+ dictionary for contour plot definition and
+ dictionary for contour line definition
+ """
+ contour = dict(positional_args=[x, y, z], plot_type="contourf", cmap=colorscale)
+ contour_lines = dict(
+ positional_args=[x, y, z],
+ colors=("k"),
+ linestyles="solid",
+ linewidths=0.2,
+ plot_type="contour",
+ )
+ return [contour, contour_lines]
+
+ def fill(self, x, y, color=None, label=None):
+ """
+ Return a trace definition for a filled polygon.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Coordinates defining the filled area.
+ color : str, optional
+ Fill colour.
+ label : str, optional
+ Ignored by the matplotlib implementation.
+ """
+ return dict(positional_args=(x, y), plot_type="fill", color=color)
+
+ def fill_between(self, x, y_upper, y_lower, color):
+ """
+ Return a trace definition for a filled region between two curves.
+
+ Parameters
+ ----------
+ x : array-like
+ X-axis values.
+ y_upper : array-like
+ Upper boundary values.
+ y_lower : array-like
+ Lower boundary values.
+ color : str
+ Fill colour.
+ """
+ return {
+ "positional_args": (x, y_upper, y_lower),
+ "plot_type": "fill_between",
+ "color": color,
+ }
+
+ def histogram_plot(self, x, name, style: dict = None):
+ """
+ Return a trace definition for a histogram.
+
+ Parameters
+ ----------
+ x : array-like
+ Data to bin.
+ name : str
+ Histogram label.
+ style : dict, optional
+ Currently only 'alpha' supported for opacity.
+ All other style arguments ignored.
+ """
+ style = style or {}
+
+ return {
+ "positional_args": [x],
+ "label": name,
+ "plot_type": "hist",
+ "alpha": style.get("alpha"),
+ }
+
+ def line(self, x=None, y=None, label=None, style=None):
+ """
+ Return a trace definition for a line plot.
+
+ Parameters
+ ----------
+ x, y : array-like, optional
+ Coordinates of the line.
+ If both x and y are provided, the shorter sequence determines the
+ plotted length.
+ label : str, optional
+ Trace label.
+ style : dict, optional
+ Line styling options.
+
+ Returns
+ -------
+ object
+ dictionary with positional argumetns, label and style arguments
+ """
+ style = style or {}
+ if y is None:
+ raise ValueError("y must be provided")
+
+ args = [y]
+ if x is not None:
+ size = min(len(x), len(y))
+ args = [x[:size], y[:size]]
+
+ return {
+ "positional_args": args,
+ "label": label,
+ **style,
+ }
+
+ def scatter(self, x, y, colors=None, labels=None, colorscale="Greys"):
+ """
+ Return a trace definition for a scatter plot.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Point coordinates.
+ colors : array-like
+ Values or colours associated with each point.
+ labels : array-like, optional
+ Point labels.
+ Point labels are ignored by matplotlib implementation.
+ Argument retained for consistency with plotly.
+ colorscale : str, optional
+ Colour scale name.
+ """
+ scatter = {
+ "positional_args": [x, y],
+ "plot_type": "scatter",
+ "cmap": colorscale,
+ }
+ if colors is not None:
+ scatter["c"] = colors
+ return scatter
+
+ def show_table(self, header, values, title):
+ """
+ Display tabular data in a standalone Matplotlib figure.
+
+ Array-valued entries are converted to comma-separated strings before
+ rendering.
+
+ Parameters
+ ----------
+ header : list
+ Column headers.
+ values : list
+ Table contents.
+ title : str
+ Table title.
+ """
+ for i, val in enumerate(values):
+ values[i] = [val[0], ", ".join(val[1].astype(str))]
+
+ fig, ax = self.plt.subplots(figsize=(6, 2), dpi=100)
+
+ # Remove axis decorations so only the table is displayed.
+ ax.axis("off")
+ ax.axis("tight")
+ ax.table(
+ cellText=values,
+ colLabels=header,
+ loc="center",
+ cellLoc="center",
+ colColours=["lightsteelblue", "lightsteelblue"],
+ )
+ ax.set_title(title)
+ fig.tight_layout()
+ self.plt.show()
+
+ def vline(self, fig, x, style=None):
+ """
+ Add a vertical reference line to the current axis.
+
+ Parameters
+ ----------
+ fig: matplotlib.figure.Figure
+ The figure.
+ x: float
+ The position of the vertical line on the axis
+ style: dict, optional
+ matplotlib arguments for axvline method
+ """
+ fig.gca()
+ style = style or {}
+ self.plt.axvline(x, **style)
diff --git a/pybop/plot/backends/plotly.py b/pybop/plot/backends/plotly.py
new file mode 100644
index 000000000..1d9890165
--- /dev/null
+++ b/pybop/plot/backends/plotly.py
@@ -0,0 +1,683 @@
+import numpy as np
+
+from pybop.plot.backends.base import PlotBackend
+from pybop.plot.backends.plotly_manager import PlotlyManager
+from pybop.plot.util import AxisData, wrap_text
+
+# Mapping from Matplotlib line styles to Plotly dash styles.
+LINESTYLE_MAP = {
+ "solid": "solid",
+ "dashed": "dash",
+ "dotted": "dot",
+ "dashdot": "dashdot",
+}
+
+# Mapping from Matplotlib-style marker definitions to their Plotly
+# equivalents.
+MARKER_MAP = {"o": "circle", "P": "cross", "X": "x", ".": None}
+
+# Translation between Matplotlib legend anchor keywords and Plotly
+# anchor names.
+ANCHOR_MAP = {
+ "lower": "bottom",
+ "upper": "top",
+ "left": "left",
+ "center": "center",
+ "right": "right",
+}
+
+
+class PlotlyBackend(PlotBackend):
+ """
+ Plotly implementation of the PlotBackend interface.
+
+ This backend converts backend-agnostic plot definitions into Plotly
+ figures and traces, providing interactive visualisations while
+ maintaining a common plotting API.
+ """
+
+ def __init__(self):
+ """
+ Initialise the Plotly backend and associated Plotly manager.
+ """
+ self.name = "plotly"
+ self.plotly_manager = PlotlyManager()
+
+ def _figure_layout(self, style, figure_title):
+ axis_layout = dict(
+ title=dict(font={"size": 14}),
+ showexponent="last",
+ exponentformat="e",
+ tickfont=dict(size=12),
+ )
+ return {
+ "title": figure_title,
+ "width": style.get("width"),
+ "height": style.get("height"),
+ "xaxis": axis_layout,
+ "yaxis": axis_layout,
+ "plot_bgcolor": style.get("bg_color"),
+ }
+
+ def create_figure(
+ self,
+ title: str = None,
+ xaxis_title: str = None,
+ yaxis_title: str = None,
+ traces=None,
+ style: dict = None,
+ ):
+ """
+ Create a Plotly figure.
+
+ Parameters
+ ----------
+ title : str, optional
+ Figure title.
+ xaxis_title : str, optional
+ X-axis label.
+ yaxis_title : str, optional
+ Y-axis label.
+ traces : list, optional
+ Plotly traces to add to the figure.
+ style : dict, optional
+ Currently supported options:
+ - width in pixels
+ - heith in pixels
+ - xaxis_range: range of the X-axis
+ - yaxis_range: range of the Y-axis
+ - bg_color: background color of the axis
+
+ Returns
+ -------
+ plotly.graph_objects.Figure
+ Configured Plotly figure.
+ """
+ style = style or {}
+ layout_opts = self._figure_layout(style, title)
+ layout_opts.update(
+ {
+ "xaxis_title": xaxis_title,
+ "yaxis_title": yaxis_title,
+ "xaxis_range": style.get("xaxis_range"),
+ "yaxis_range": style.get("yaxis_range"),
+ "barmode": "overlay",
+ }
+ )
+ layout = self.plotly_manager.go.Layout(layout_opts)
+
+ fig = self.plotly_manager.go.Figure(data=traces, layout=layout)
+ return fig
+
+ def _check_empty(self, specs, row, col):
+ """
+ Validate that a subplot grid location is available.
+
+ Parameters
+ ----------
+ specs : list[list]
+ Plotly subplot specification grid.
+ row : int
+ Row index (1-based).
+ col : int
+ Column index (1-based).
+
+ Raises
+ ------
+ ValueError
+ If the requested subplot location overlaps an existing subplot.
+ """
+ if specs[row - 1][col - 1] is None or len(specs[row - 1][col - 1]) > 0:
+ raise ValueError("Overlapping axes are not supported")
+
+ def make_subplots(
+ self,
+ axes: list[AxisData],
+ title=None,
+ xaxis_titles: list[str] | str = None,
+ yaxis_titles: list[str] | str = None,
+ style=None,
+ ):
+ """
+ Create a figure containing multiple subplots.
+
+ Parameters
+ ----------
+ axes : list[AxisData]
+ Definitions describing subplot positions and spans.
+ title : str, optional
+ Figure title.
+ xaxis_titles : str or list[str], optional
+ X-axis titles.
+ yaxis_titles : str or list[str], optional
+ Y-axis titles.
+ style : dict, optional
+ Figure styling options.
+
+ Returns
+ -------
+ tuple
+ (
+ figure,
+ axes dictionary,
+ number of rows,
+ number of columns
+ )
+ """
+ style = style or {}
+ axes_dict = {}
+
+ # Determine the minimum grid size required to accommodate all subplot spans.
+ num_rows = max(ax.row + ax.row_span - 1 for ax in axes)
+ num_cols = max(ax.col + ax.col_span - 1 for ax in axes)
+
+ # Plotly subplot layouts are defined using a grid of specification
+ # dictionaries. Empty dictionaries denote subplot origins, while None
+ # marks cells occupied by a spanning subplot.
+ specs = [[{}] * num_cols for _ in range(num_rows)]
+
+ # Generate subplots data from axes
+ for ax in axes:
+ # Ensure no subplot occupies the requested grid location.
+ self._check_empty(specs, ax.row, ax.col)
+ axes_dict[(ax.row, ax.col)] = ax
+ specs[ax.row - 1][ax.col - 1] = {
+ "colspan": ax.col_span,
+ "rowspan": ax.row_span,
+ }
+ # Check space available for full row-/col-span
+ # Add spec None to covered grid space
+ for row in range(ax.row, ax.row + ax.row_span - 1):
+ for col in range(ax.col, ax.col + ax.col_span - 1):
+ if row > ax.row or col > ax.col:
+ self._check_empty(specs, row, col)
+ specs[row - 1, col - 1] = None
+
+ # Create figure with supbplots
+ make_subplots = self.plotly_manager.make_subplots
+ fig = make_subplots(
+ rows=num_rows,
+ cols=num_cols,
+ specs=specs,
+ horizontal_spacing=0.2,
+ vertical_spacing=0.15,
+ )
+
+ # Add axis title to each axis in the subplot
+ def _get_axis_title(titles, width, i):
+ if isinstance(titles, str):
+ title = titles
+ elif isinstance(titles, list) and i < len(titles):
+ title = titles[i]
+ else:
+ return None
+
+ return wrap_text(title, width, self.name)
+
+ # Reduce wrapping width as subplot rows increase to avoid overlapping
+ # axis titles in dense layouts.
+ width = np.floor(50 / num_rows)
+ for i, ax in enumerate(axes):
+ if title := _get_axis_title(xaxis_titles, width, i):
+ fig.update_xaxes(
+ title_text=title,
+ row=ax.row,
+ col=ax.col,
+ )
+ if title := _get_axis_title(yaxis_titles, width, i):
+ fig.update_yaxes(
+ title_text=title,
+ row=ax.row,
+ col=ax.col,
+ )
+
+ fig.update_layout(self._figure_layout(style, title))
+
+ return fig, axes_dict, num_rows, num_cols
+
+ def legend(self, fig, style: dict = None):
+ """
+ Configure and display a figure legend.
+
+ Parameters
+ ----------
+ fig : plotly.graph_objects.Figure
+ Target figure.
+ style : dict, optional
+ Legend styling options including orientation,
+ location and anchor coordinates.
+ """
+ style = style or {}
+ opts = {}
+ if style.get("horizontal"):
+ opts["orientation"] = "h"
+ if "loc" in style:
+ anchors = style.get("loc").split(" ")
+ if len(anchors) != 2:
+ raise ValueError("loc property must consist of 2 keywords")
+ opts["xanchor"] = ANCHOR_MAP.get(anchors[1], "auto")
+ opts["yanchor"] = ANCHOR_MAP.get(anchors[0], "auto")
+ if "coords" in style:
+ coords = style.get("coords")
+ opts["x"] = coords[0]
+ opts["y"] = coords[1]
+
+ fig.update_layout(showlegend=True, legend=opts)
+
+ def show_figure(self, fig):
+ """
+ Display one or more Plotly figures.
+
+ Parameters
+ ----------
+ fig : Figure or iterable[Figure]
+ Figure or collection of figures to display.
+ """
+ # Support displaying either a single figure or a collection of figures.
+ if hasattr(fig, "__len__") and len(fig) > 0:
+ for f in fig:
+ f.show()
+ else:
+ fig.show()
+
+ def plot_trace(self, traces, fig, ax=None):
+ """
+ Add one or more traces to a figure or subplot.
+
+ Parameters
+ ----------
+ traces : Trace or list[Trace]
+ Plotly trace objects to add.
+ fig : plotly.graph_objects.Figure
+ Target figure.
+ ax : AxisData, optional
+ Subplot location. If provided, traces are added
+ to the specified subplot.
+
+ Returns
+ -------
+ None
+ """
+ for trace in np.atleast_1d(traces):
+ if ax is None:
+ fig.add_trace(trace)
+ else:
+ fig.add_trace(trace, row=ax.row, col=ax.col)
+
+ def sample_color_scale(self, data, scale="viridis", d_min=None, d_max=None):
+ """
+ Sample colours from a Plotly colour scale.
+
+ Parameters
+ ----------
+ data : array-like
+ Values to map onto the colour scale.
+ scale : str, optional
+ Plotly colour scale name.
+ d_min : float, optional
+ Minimum value used for normalisation.
+ d_max : float, optional
+ Maximum value used for normalisation.
+
+ Returns
+ -------
+ list
+ Colours corresponding to the supplied values.
+ """
+ px = self.plotly_manager.px
+ # normalise and clip data
+ d_min = d_min or np.nanmin(data[np.isfinite(data)])
+ d_max = d_max or np.nanmax(data[np.isfinite(data)])
+
+ d = (data - d_min) / (d_max - d_min)
+ if np.isscalar(d):
+ d = np.array([d])
+ np.clip(np.asarray(d), 0, 1.0, out=d)
+ return px.colors.sample_colorscale(scale, list(d))
+
+ def colorbar(self, fig, data, colorscale="viridis", label=None):
+ """
+ Add a standalone colour bar to a figure.
+
+ Parameters
+ ----------
+ fig : plotly.graph_objects.Figure
+ Target figure.
+ data : array-like
+ Values defining the colour range.
+ colorscale : str, optional
+ Plotly colour scale name.
+ label : str, optional
+ Colour bar title.
+
+ Returns
+ -------
+ None
+ """
+ d_min = np.nanmin(data[np.isfinite(data)])
+ d_max = np.nanmax(data[np.isfinite(data)])
+
+ colorbar = dict(thickness=25, outlinewidth=1)
+ if label is not None:
+ colorbar.update({"title": {"text": label, "side": "right"}})
+
+ # Plotly requires a trace to render a standalone colour bar, so an
+ # invisible scatter trace is added solely to display the scale.
+ fig.add_trace(
+ self.plotly_manager.go.Scatter(
+ x=[None],
+ y=[None],
+ mode="markers",
+ marker=dict(
+ colorscale=colorscale,
+ showscale=True,
+ cmin=d_min,
+ cmax=d_max,
+ colorbar=colorbar,
+ ),
+ showlegend=False,
+ hoverinfo="none",
+ )
+ )
+
+ def contour_plot(self, x, y, z, colorscale="viridis"):
+ """
+ Create a contour plot trace.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Coordinate values.
+ z : array-like
+ Contour values.
+ colorscale : str, optional
+ Plotly colour scale.
+
+ Returns
+ -------
+ plotly.graph_objects.Contour
+ Contour trace.
+ """
+ # Use connectgaps=True to ill small gaps in the input grid to avoid breaks in contour regions.
+ return self.plotly_manager.go.Contour(
+ x=x, y=y, z=z, colorscale=colorscale, connectgaps=True
+ )
+
+ def fill(self, x, y, color=None, label=None):
+ """
+ Create a filled polygon trace.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Polygon coordinates.
+ color : str, optional
+ Fill colour.
+ label : str, optional
+ Legend label.
+
+ Returns
+ -------
+ plotly.graph_objects.Scatter
+ Filled polygon trace.
+ """
+ opts = {}
+ if color is not None:
+ opts["fillcolor"] = color
+ if label is not None:
+ opts["name"] = label
+
+ return self.plotly_manager.go.Scatter(
+ x=x, y=y, fill="toself", mode="text", showlegend=False, **opts
+ )
+
+ def fill_between(self, x, y_upper, y_lower, color):
+ """
+ Create a filled region between two curves.
+
+ Parameters
+ ----------
+ x : array-like
+ X values.
+ y_upper : array-like
+ Upper boundary.
+ y_lower : array-like
+ Lower boundary.
+ color : str
+ Fill colour.
+
+ Returns
+ -------
+ plotly.graph_objects.Scatter
+ Filled area trace.
+ """
+
+ # Construct a closed polygon by traversing the upper curve forwards
+ # and the lower curve in reverse.
+ return self.plotly_manager.go.Scatter(
+ x=x + x[::-1],
+ y=y_upper + y_lower[::-1],
+ fill="toself",
+ line=dict(color="rgba(255,255,255,0)"),
+ hoverinfo="skip",
+ showlegend=False,
+ fillcolor=color,
+ )
+
+ def histogram_plot(self, x, name, style=None):
+ """
+ Create a histogram trace.
+
+ Parameters
+ ----------
+ x : array-like
+ Data values.
+ name : str
+ Histogram label.
+ style : dict, optional
+ Histogram styling options.
+
+ Returns
+ -------
+ plotly.graph_objects.Histogram
+ Histogram trace.
+ """
+ style = style or {}
+
+ return self.plotly_manager.go.Histogram(
+ x=x, name=name, opacity=style.get("alpha")
+ )
+
+ def _get_line_options(self, style, opts):
+ """
+ Populate Plotly line styling options.
+
+ Parameters
+ ----------
+ style : dict
+ User-supplied style options.
+ opts : dict
+ Trace options dictionary updated in-place.
+
+ Returns
+ -------
+ None
+ """
+ linestyle = style.get("linestyle", "solid")
+ color = style.get("color")
+ opts["line"] = dict(
+ width=style.get("linewidth", 4), dash=LINESTYLE_MAP.get(linestyle, "solid")
+ )
+ if color is not None:
+ opts["line"].update(color=color)
+
+ def line(self, x=None, y=None, label=None, style=None):
+ """
+ Create a line and/or marker trace.
+
+ Parameters
+ ----------
+ x : array-like, optional
+ X values.
+ y : array-like
+ Y values.
+ label : str, optional
+ Legend label.
+ style : dict, optional
+ Line and marker styling options.
+ Currently supported:
+ - linestyle: see LINESTYLE_MAP
+ - linewidth
+ - color
+ - marker: the marker symbol see MARKER_MAP
+ - markerfacecolor
+ - markeredgecolor
+ - markeredgewidth
+ - fillstyle (for marker)
+
+ Returns
+ -------
+ plotly.graph_objects.Scatter
+ Scatter trace configured as a line, marker plot,
+ or combined line-marker plot.
+ """
+
+ style = style or {}
+ linestyle = style.get("linestyle", "solid")
+ marker = style.get("marker", "none")
+ opts = {}
+ if linestyle.lower() == "none":
+ mode = "markers"
+ elif marker.lower() == "none":
+ mode = "lines"
+ else:
+ mode = "markers+lines"
+
+ opts["mode"] = mode
+ if linestyle.lower() != "none":
+ self._get_line_options(style, opts)
+
+ if marker.lower() != "none":
+ opts["marker"] = dict(
+ size=style.get("markersize", 8),
+ symbol=MARKER_MAP.get(marker),
+ )
+ fillstyle = style.get("fillstyle", "full")
+ markerfacecolor = style.get("markerfacecolor")
+ markeredgecolor = style.get("markeredgecolor")
+ markeredgewidth = style.get("markeredgewidth")
+
+ # Plotly uses "-open" marker variants to represent unfilled markers.
+ if fillstyle.lower() == "none":
+ opts["marker"].update(symbol=MARKER_MAP.get(marker) + "-open")
+ if markeredgecolor is not None:
+ opts["marker"].update(color=markeredgecolor)
+
+ if markerfacecolor is not None:
+ opts["marker"].update(color=markerfacecolor)
+ if markeredgecolor is not None:
+ opts["marker"].update(
+ line_color=markeredgecolor, line_width=markeredgewidth or 1
+ )
+ elif markeredgewidth is not None:
+ opts["marker"].update(line_width=markeredgewidth)
+
+ # Avoid creating empty legend entries for unnamed traces.
+ if label is None:
+ opts["showlegend"] = False
+
+ kwargs = {"y": y, "name": label, **opts}
+ if x is not None:
+ kwargs["x"] = x
+
+ return self.plotly_manager.go.Scatter(**kwargs)
+
+ def scatter(self, x, y, colors, labels=None, colorscale="Greys"):
+ """
+ Create a scatter plot trace.
+
+ Parameters
+ ----------
+ x, y : array-like
+ Point coordinates.
+ colors : array-like
+ Values used to colour markers.
+ labels : array-like, optional
+ Hover labels.
+ colorscale : str, optional
+ Plotly colour scale.
+
+ Returns
+ -------
+ plotly.graph_objects.Scatter
+ Scatter trace.
+ """
+
+ opts = dict(
+ mode="markers",
+ marker=dict(
+ color=colors,
+ colorscale=colorscale,
+ size=8,
+ showscale=False,
+ ),
+ showlegend=False,
+ )
+ if labels is not None:
+ opts.update({"text": labels, "hoverinfo": "text"})
+ return self.plotly_manager.go.Scatter(x=x, y=y, **opts)
+
+ def show_table(self, header, values, title):
+ """
+ Display tabular data as a Plotly table.
+
+ Parameters
+ ----------
+ header : list
+ Column headers.
+ values : list
+ Table contents.
+ title : str
+ Table title.
+
+ Returns
+ -------
+ None
+ """
+ # Import plotly only when needed
+
+ fig = self.plotly_manager.go.Figure(
+ data=[
+ self.plotly_manager.go.Table(
+ header=dict(values=header),
+ cells=dict(
+ values=[[row[0] for row in values], [row[1] for row in values]]
+ ),
+ )
+ ]
+ )
+
+ fig.update_layout(title=title)
+ fig.show()
+
+ def vline(self, fig, x, style=None):
+ """
+ Add a vertical reference line to a figure.
+
+ Parameters
+ ----------
+ fig : plotly.graph_objects.Figure
+ Target figure.
+ x : float
+ X-coordinate of the line.
+ style : dict, optional
+ Line styling options.
+
+ Returns
+ -------
+ None
+ """
+ style = style or {}
+ opts = {}
+ self._get_line_options(style, opts)
+ fig.add_vline(x=x, **opts)
diff --git a/pybop/plot/plotly_manager.py b/pybop/plot/backends/plotly_manager.py
similarity index 100%
rename from pybop/plot/plotly_manager.py
rename to pybop/plot/backends/plotly_manager.py
diff --git a/pybop/plot/contour.py b/pybop/plot/contour.py
index da2d6a1a6..5ca8712e6 100644
--- a/pybop/plot/contour.py
+++ b/pybop/plot/contour.py
@@ -4,7 +4,7 @@
import numpy as np
-from pybop.plot.plotly_manager import PlotlyManager
+from pybop.plot.util import get_backend
from pybop.problems.problem import Problem
if TYPE_CHECKING:
@@ -17,8 +17,9 @@ def contour(
bounds: np.ndarray | None = None,
transformed: bool = False,
steps: int = 10,
+ title="Cost Landscape",
show: bool = True,
- **layout_kwargs,
+ backend: str = None,
):
"""
Plot a 2D visualisation of a cost landscape using Plotly.
@@ -32,6 +33,8 @@ def contour(
Either:
- the cost function to be evaluated. Must accept a list of parameter values and return a cost value.
- an optimiser result which provides a specific optimisation trace overlaid on the cost landscape.
+ title: str, optional
+ The title of the figure (default: "Cost Landscape")
gradient : bool, optional
If True, the gradient is shown (default: False).
bounds : numpy.ndarray | list[list[float]], optional
@@ -43,21 +46,21 @@ def contour(
The number of grid points to divide the parameter space into along each dimension (default: 10).
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time [s]"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
+ backend: str, optional
+ The plotting backend to be used.
Returns
-------
- plotly.graph_objs.Figure
- The Plotly figure object containing the cost landscape plot.
+ fig : plotly.graph_objs.Figure or matplotlib.figure.Figure
+ The figure object containing the cost landscape plot.
Raises
------
ValueError
If the cost function does not return a valid cost when called with a parameter list.
"""
+ backend_module = get_backend(backend)
+
plot_optim = False
problem = call_object
@@ -143,117 +146,105 @@ def transform_array_of_values(list_of_values, parameter):
bounds[0] = transform_array_of_values(bounds[0], parameters[names[0]])
bounds[1] = transform_array_of_values(bounds[1], parameters[names[1]])
- # Import plotly only when needed
- go = PlotlyManager().go
-
- # Set default layout properties
- layout_options = dict(
- title="Cost Landscape",
- title_x=0.5,
- title_y=0.905,
- width=600,
- height=600,
- xaxis=dict(range=bounds[0], showexponent="last", exponentformat="e"),
- yaxis=dict(range=bounds[1], showexponent="last", exponentformat="e"),
- legend=dict(orientation="h", yanchor="bottom", y=1, xanchor="right", x=1),
- )
- layout_options["xaxis_title"] = (
- "Transformed " + names[0] if transformed else names[0]
+ figure_style = {
+ "width": 600,
+ "height": 600,
+ "xaxis_range": bounds[0],
+ "yaxis_range": bounds[1],
+ }
+
+ fig = backend_module.create_figure(
+ title=title,
+ xaxis_title="Transformed " + names[0] if transformed else names[0],
+ yaxis_title="Transformed " + names[1] if transformed else names[1],
+ style=figure_style,
)
- layout_options["yaxis_title"] = (
- "Transformed " + names[1] if transformed else names[1]
- )
- layout = go.Layout(layout_options)
# Create contour plot and update the layout
- fig = go.Figure(
- data=[go.Contour(x=x, y=y, z=costs, colorscale="Viridis", connectgaps=True)],
- layout=layout,
- )
+ backend_module.plot_trace(backend_module.contour_plot(x=x, y=y, z=costs), fig)
if plot_optim:
# Plot the optimisation trace
optim_trace = np.asarray([item[:2] for item in result.x_model])
optim_trace = optim_trace.reshape(-1, 2)
-
- fig.add_trace(
- go.Scatter(
- x=transform_array_of_values(optim_trace[:, 0], parameters[names[0]]),
- y=transform_array_of_values(optim_trace[:, 1], parameters[names[1]]),
- mode="markers",
- marker=dict(
- color=[i / len(optim_trace) for i in range(len(optim_trace))],
- colorscale="Greys",
- size=8,
- showscale=False,
- ),
- showlegend=False,
- )
+ backend_module.plot_trace(
+ backend_module.scatter(
+ transform_array_of_values(optim_trace[:, 0], parameters[names[0]]),
+ transform_array_of_values(optim_trace[:, 1], parameters[names[1]]),
+ [i / optim_trace.shape[0] for i in range(optim_trace.shape[0])],
+ ),
+ fig,
)
# Plot the initial guess
if len(result.x_model) > 0:
x0 = result.x_model[0]
- fig.add_trace(
- go.Scatter(
+ backend_module.plot_trace(
+ backend_module.line(
x=transform_array_of_values([x0[0]], parameters[names[0]]),
y=transform_array_of_values([x0[1]], parameters[names[1]]),
- mode="markers",
- marker_symbol="x",
- marker=dict(
- color="white",
- line_color="black",
- line_width=1,
- size=14,
- showscale=False,
+ label="Initial values",
+ style=dict(
+ marker="X",
+ markersize=14,
+ markerfacecolor="white",
+ markeredgecolor="black",
+ linestyle="None",
+ zorder=2.6,
),
- name="Initial values",
- )
+ ),
+ fig,
)
# Plot optimised value
if result.x is not None:
x_best = result.x
- fig.add_trace(
- go.Scatter(
+ backend_module.plot_trace(
+ backend_module.line(
x=transform_array_of_values([x_best[0]], parameters[names[0]]),
y=transform_array_of_values([x_best[1]], parameters[names[1]]),
- mode="markers",
- marker_symbol="cross",
- marker=dict(
- color="black",
- line_color="white",
- line_width=1,
- size=14,
- showscale=False,
+ style=dict(
+ marker="P",
+ markersize=14,
+ markerfacecolor="black",
+ markeredgecolor="white",
+ linestyle="None",
+ zorder=2.6,
),
- name="Final values",
- )
+ label="Final values",
+ ),
+ fig,
)
- # Update the layout and display the figure
- fig.update_layout(**layout_kwargs)
+ backend_module.legend(
+ fig,
+ style={
+ "horizontal": True,
+ "loc": "lower right",
+ "coords": (1, 1),
+ },
+ )
+ # display the figure
if show:
- fig.show()
+ backend_module.show_figure(fig)
if gradient:
grad_figs = []
for i, grad_costs in enumerate(grad_parameter_costs):
- # Update title for gradient plots
- updated_layout_options = layout_options.copy()
- updated_layout_options["title"] = f"Gradient for Parameter: {i + 1}"
-
- # Create contour plot with updated layout options
- grad_layout = go.Layout(updated_layout_options)
-
# Create fig
- grad_fig = go.Figure(
- data=[go.Contour(x=x, y=y, z=grad_costs)], layout=grad_layout
+ grad_fig = backend_module.create_figure(
+ title=f"Gradient for Parameter: {i + 1}",
+ xaxis_title="Transformed " + names[0] if transformed else names[0],
+ yaxis_title="Transformed " + names[1] if transformed else names[1],
+ style=figure_style,
+ )
+
+ backend_module.plot_trace(
+ backend_module.contour_plot(x=x, y=y, z=grad_costs), grad_fig
)
- grad_fig.update_layout(**layout_kwargs)
if show:
- grad_fig.show()
+ backend_module.show_figure(grad_fig)
# append grad_fig to list
grad_figs.append(grad_fig)
diff --git a/pybop/plot/convergence.py b/pybop/plot/convergence.py
index deabf66d5..439bf9e5c 100644
--- a/pybop/plot/convergence.py
+++ b/pybop/plot/convergence.py
@@ -1,12 +1,12 @@
from typing import TYPE_CHECKING
-from pybop.plot.standard_plots import StandardPlot
+from pybop.plot.util import get_backend
if TYPE_CHECKING:
from pybop._result import Result
-def convergence(result: "Result", show=True, **layout_kwargs):
+def convergence(result: "Result", show: bool = True, backend: str = None):
"""
Plot the convergence of the optimisation algorithm.
@@ -16,15 +16,13 @@ def convergence(result: "Result", show=True, **layout_kwargs):
Optimisation result containing the history of parameter values and associated cost.
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time [s]"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
+ backend : str, optional
+ Select a plotting backend. If None, the current default backend is used.
Returns
---------
- fig : plotly.graph_objs.Figure
- The Plotly figure object for the convergence plot.
+ fig : plotly.graph_objs.Figure or matplotlib.figure.Figure
+ The figure object for the convergence plot.
"""
# Extract log from the optimisation object
@@ -33,22 +31,28 @@ def convergence(result: "Result", show=True, **layout_kwargs):
# Generate a list of iteration numbers
iteration_numbers = list(range(1, len(cost_log) + 1))
- # Create a plot dictionary
- plot_dict = StandardPlot(
- x=iteration_numbers,
- y=cost_log,
- layout_options=dict(
- xaxis_title="Evaluation",
- yaxis_title="Cost",
- title="Convergence",
+ backend = get_backend(backend)
+
+ # Create figure
+ fig = backend.create_figure(
+ xaxis_title="Evaluation",
+ yaxis_title="Cost",
+ title="Convergence",
+ style={"bg_color": "white", "width": 600, "height": 600},
+ )
+
+ # Add line plot
+ backend.plot_trace(
+ backend.line(
+ x=iteration_numbers,
+ y=cost_log,
+ label=result.method_name,
),
- trace_names=result.method_name,
+ fig,
)
- # Generate and display the figure
- fig = plot_dict(show=False)
- fig.update_layout(**layout_kwargs)
+ # Display or return figure
if show:
- fig.show()
-
- return fig
+ backend.show_figure(fig)
+ else:
+ return fig
diff --git a/pybop/plot/dataset.py b/pybop/plot/dataset.py
index 24257a732..2c00f4b35 100644
--- a/pybop/plot/dataset.py
+++ b/pybop/plot/dataset.py
@@ -1,7 +1,8 @@
-from pybop.plot.standard_plots import StandardPlot, trajectories
+from pybop.plot.trajectories import trajectories
+from pybop.plot.util import get_backend, remove_brackets
-def dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs):
+def dataset(dataset, signal=None, labels=None, show=True, backend=None):
"""
Quickly plot a PyBOP Dataset using Plotly.
@@ -11,19 +12,15 @@ def dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs):
A PyBOP dataset.
signal : list or str, optional
The name of the time series to plot (default: "Voltage [V]").
- trace_names : list or str, optional
+ labels : list or str, optional
Name(s) for the trace(s) (default: "Data").
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time / s"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
Returns
-------
- plotly.graph_objs.Figure
- The Plotly figure object for the scatter plot.
+ fig : plotly.graph_objs.Figure or matplotlib.figure.Figure
+ The figure object for the scatter plot.
"""
# Get data dictionary
@@ -34,25 +31,27 @@ def dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs):
# Compile ydata and labels or legend
y = [dataset[s] for s in signal]
if len(signal) == 1:
- yaxis_title = StandardPlot.remove_brackets(signal[0])
- if trace_names is None:
- trace_names = ["Data"]
+ yaxis_title = remove_brackets(signal[0])
+ if labels is None:
+ labels = ["Data"]
else:
yaxis_title = "Output"
- if trace_names is None:
- trace_names = StandardPlot.remove_brackets(signal)
+ if labels is None:
+ labels = remove_brackets(signal)
# Create the figure
fig = trajectories(
x=dataset[dataset.domain],
y=y,
- trace_names=trace_names,
+ labels=labels,
show=False,
- xaxis_title=StandardPlot.remove_brackets(dataset.domain),
+ xaxis_title=remove_brackets(dataset.domain),
yaxis_title=yaxis_title,
+ backend=backend,
)
- fig.update_layout(**layout_kwargs)
+
+ backend_module = get_backend(backend)
if show:
- fig.show()
+ backend_module.show_figure(fig)
return fig
diff --git a/pybop/plot/distribution.py b/pybop/plot/distribution.py
index e0e0430dc..6ac6c0529 100644
--- a/pybop/plot/distribution.py
+++ b/pybop/plot/distribution.py
@@ -2,6 +2,7 @@
from pybop.parameters.parameter import Parameters
from pybop.plot.standard_plots import StandardSubplot
+from pybop.plot.util import get_backend
def distribution(
@@ -10,22 +11,18 @@ def distribution(
n_samples: int = 100,
transformed: bool = False,
show: bool = True,
- **layout_kwargs,
+ backend: str = None,
):
"""
Plot the posterior on top of the prior distribution for a Bayesian optimisation result.
"""
# Create lists of axis titles and trace names
- axis_titles = []
- trace_names = (
- parameters.names
- if posterior is None
- else ["Prior"] * len(parameters) + ["Posterior"] * len(parameters)
- )
+ xaxis_titles = []
+ yaxis_titles = []
+ labels = parameters.names if posterior is None else ["Prior"] * len(parameters)
for name in parameters.names:
- axis_titles.append(
- (name + " (transformed)" if transformed else name, "Probability density")
- )
+ xaxis_titles.append(name + " (transformed)" if transformed else name)
+ yaxis_titles.append("Probability density")
# Evaluate marginal distributions for each parameter
values = []
@@ -37,22 +34,20 @@ def distribution(
values.append(parameter_range)
probability.append([d.pdf(s) for s in values[-1]])
- # Set subplot layout options
- layout_options = dict(
- width=1024,
- height=576,
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
- )
+ # Get plotting backend
+ backend = get_backend(backend)
# Create a plot dictionary
plot_dict = StandardSubplot(
x=values,
y=probability,
- axis_titles=axis_titles,
- layout_options=layout_options,
- trace_names=trace_names,
- trace_name_width=50,
+ xaxis_titles=xaxis_titles,
+ yaxis_titles=yaxis_titles,
+ labels=labels,
+ style={"width": 1024, "height": 576},
+ backend=backend,
)
+
fig = plot_dict(show=False)
if posterior is not None:
@@ -63,15 +58,21 @@ def distribution(
values.append(parameter_range)
probability.append([d.pdf(s) for s in values[-1]])
- trace = plot_dict.create_trace(
- values[-1], probability[-1], **plot_dict.trace_options
- )
+ line = backend.line(values[-1], probability[-1], label="Posterior")
row = (idx // plot_dict.num_cols) + 1
col = (idx % plot_dict.num_cols) + 1
- fig.add_trace(trace, row=row, col=col)
-
- fig.update_layout(**layout_kwargs)
+ backend.plot_trace(line, fig, ax=plot_dict.axes[row, col])
+ backend.legend(
+ fig,
+ style=dict(
+ horizontal=True,
+ outside=("top", 0.1),
+ loc="lower right",
+ coords=(1, 1.02),
+ fig_legend=True,
+ ),
+ )
if show:
- fig.show()
-
- return fig
+ backend.show_figure(fig)
+ else:
+ return fig
diff --git a/pybop/plot/nyquist.py b/pybop/plot/nyquist.py
index 80f7eb77a..a4f5ed04d 100644
--- a/pybop/plot/nyquist.py
+++ b/pybop/plot/nyquist.py
@@ -1,8 +1,10 @@
from pybop.parameters.parameter import Inputs
-from pybop.plot.standard_plots import StandardPlot
+from pybop.plot.util import get_backend
-def nyquist(problem, inputs: Inputs = None, show=True, **layout_kwargs):
+def nyquist(
+ problem, inputs: Inputs = None, show=True, title="Nyquist Plot", backend=None
+):
"""
Generates Nyquist plots for the given problem by evaluating the model's output and target values.
@@ -11,19 +13,20 @@ def nyquist(problem, inputs: Inputs = None, show=True, **layout_kwargs):
problem : pybop.Problem
An instance of a problem class that contains the parameters and methods
for evaluation and target retrieval.
+ title: str, optional
+ The title of the figure
inputs : Inputs, optional
Input parameters for the problem. If not provided, the default parameters from the problem
instance will be used. These parameters are verified before use (default is None).
show : bool, optional
If True, the plots will be displayed.
- **layout_kwargs : dict, optional
- Additional keyword arguments for customising the plot layout. These arguments are passed to
- `fig.update_layout()`.
+ backend: str, optional
+ The plotting backend to be used.
Returns
-------
list
- A list of plotly `Figure` objects, each representing a Nyquist plot for the model's output and target values.
+ A list of plotly or matplotlib `Figure` objects, each representing a Nyquist plot for the model's output and target values.
Notes
-----
@@ -31,7 +34,6 @@ def nyquist(problem, inputs: Inputs = None, show=True, **layout_kwargs):
of the impedance from the target output.
- For each signal in the problem, a Nyquist plot is created with the model's impedance plotted as a scatter plot.
- An additional trace for the reference (target output) is added to the plot.
- - The plot layout can be customised using `layout_kwargs`.
Example
-------
@@ -39,81 +41,56 @@ def nyquist(problem, inputs: Inputs = None, show=True, **layout_kwargs):
>>> nyquist_figures = nyquist(problem, show=True, title="Nyquist Plot", xaxis_title="Real(Z)", yaxis_title="Imag(Z)")
>>> # The plots will be displayed and nyquist_figures will contain the list of figure objects.
"""
+
+ trace_style_model = dict(
+ linewidth=2,
+ color="#00CC96",
+ marker="o",
+ markerfacecolor="#00CC96",
+ )
+ trace_style_reference = dict(
+ linestyle="none", marker="o", fillstyle="none", markeredgecolor="#636EFA"
+ )
+
if not isinstance(inputs, dict):
inputs = problem.parameters.to_dict(inputs)
model_output = problem.simulate(inputs)
domain_data = model_output["Impedance"].data.real
target_output = problem.target_data
-
figure_list = []
+ backend = get_backend(backend)
for var in problem.target:
- default_layout_options = dict(
- title="Nyquist Plot",
- font=dict(family="Arial", size=14),
- plot_bgcolor="white",
- paper_bgcolor="white",
- xaxis=dict(
- title=dict(text="Zre / Ω", font=dict(size=16), standoff=15),
- showline=True,
- linewidth=2,
- linecolor="black",
- mirror=True,
- ticks="outside",
- tickwidth=2,
- tickcolor="black",
- ticklen=5,
- ),
- yaxis=dict(
- title=dict(text="-Zim / Ω", font=dict(size=16), standoff=15),
- showline=True,
- linewidth=2,
- linecolor="black",
- mirror=True,
- ticks="outside",
- tickwidth=2,
- tickcolor="black",
- ticklen=5,
- scaleanchor="x",
- scaleratio=1,
- ),
- legend=dict(
- orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1
- ),
- width=600,
- height=600,
- )
-
- plot_dict = StandardPlot(
- x=domain_data,
- y=-model_output[var].data.imag,
- layout_options=default_layout_options,
- trace_names="Model",
+ fig = backend.create_figure(
+ xaxis_title=r"$Z_{re} / \Omega$",
+ yaxis_title=r"$-Z_{im} / \Omega$",
+ title=title,
+ style={"width": 600, "height": 600, "bg_color": "white"},
)
- plot_dict.traces[0].update(
- mode="lines+markers",
- line=dict(color="#00CC96", width=2),
- marker=dict(size=8, color="#00CC96", symbol="circle"),
+ backend.plot_trace(
+ backend.line(
+ x=domain_data,
+ y=-model_output[var].data.imag,
+ label="Model",
+ style=trace_style_model,
+ ),
+ fig,
)
- target_trace = plot_dict.create_trace(
- x=target_output[var].real,
- y=-target_output[var].imag,
- name="Reference",
- mode="markers",
- marker=dict(size=8, color="#636EFA", symbol="circle-open"),
- showlegend=True,
+ backend.plot_trace(
+ backend.line(
+ x=target_output[var].real,
+ y=-target_output[var].imag,
+ label="Reference",
+ style=trace_style_reference,
+ ),
+ fig,
)
- plot_dict.traces.append(target_trace)
-
- fig = plot_dict(show=False)
-
- # Overwrite with user-kwargs
- fig.update_layout(**layout_kwargs)
- if show:
- fig.show()
-
+ backend.legend(fig)
figure_list.append(fig)
+ if show:
+ backend.show_figure(fig)
+
return figure_list
diff --git a/pybop/plot/parameters.py b/pybop/plot/parameters.py
index a13d9e3fb..ca171170d 100644
--- a/pybop/plot/parameters.py
+++ b/pybop/plot/parameters.py
@@ -2,12 +2,13 @@
from pybop.costs.log_likelihoods import GaussianLogLikelihood
from pybop.plot.standard_plots import StandardSubplot
+from pybop.plot.util import get_backend
if TYPE_CHECKING:
from pybop._result import Result
-def parameters(result: "Result", show=True, **layout_kwargs):
+def parameters(result: "Result", show: bool = True, backend: str = None):
"""
Plot the evolution of parameters during the optimisation process using Plotly.
@@ -17,15 +18,13 @@ def parameters(result: "Result", show=True, **layout_kwargs):
Optimisation result containing the history of parameter values and associated cost.
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time [s]"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
+ backend: str, optional
+ The plotting backend to be used
Returns
-------
- plotly.graph_objs.Figure
- A Plotly figure object showing the parameter evolution over iterations.
+ plotly.graph_objs.Figure or matplotlib.figure.Figure
+ A figure object showing the parameter evolution over iterations.
"""
# Extract parameters and log from the optimisation object
@@ -34,37 +33,44 @@ def parameters(result: "Result", show=True, **layout_kwargs):
y = [list(item) for item in zip(*result.x_model, strict=False)]
# Create lists of axis titles and trace names
- axis_titles = []
- trace_names = parameters.names
- for name in trace_names:
- axis_titles.append(("Evaluation", name))
-
+ xaxis_titles = []
+ yaxis_titles = []
+ labels = parameters.names
if isinstance(result.problem, GaussianLogLikelihood):
- axis_titles.append(("Evaluation", "Sigma"))
- trace_names.append("Sigma")
+ labels.append("Sigma")
- # Set subplot layout options
- layout_options = dict(
- title="Parameter Convergence",
- width=1024,
- height=576,
- legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
- )
+ for name in labels:
+ xaxis_titles.append("Evaluation")
+ yaxis_titles.append(name)
+
+ # import plotting backend
+ backend = get_backend(backend)
# Create a plot dictionary
plot_dict = StandardSubplot(
- x=x,
- y=y,
- axis_titles=axis_titles,
- layout_options=layout_options,
- trace_names=trace_names,
- trace_name_width=50,
+ x,
+ y,
+ title="Parameter Convergence",
+ xaxis_titles=xaxis_titles,
+ yaxis_titles=yaxis_titles,
+ style=dict(bg_color="white", width=1600, height=800),
+ labels=labels,
+ backend=backend,
)
- # Generate the figure and update the layout
fig = plot_dict(show=False)
- fig.update_layout(**layout_kwargs)
+
+ # add legend
+ backend.legend(
+ fig,
+ style={
+ "fig_legend": True,
+ "outside": ("right", 0.18),
+ },
+ )
+
+ # Generate the figure and update the layout
if show:
- fig.show()
+ backend.show_figure(fig)
return fig
diff --git a/pybop/plot/predictive.py b/pybop/plot/predictive.py
index 25211d6d5..cb2848524 100644
--- a/pybop/plot/predictive.py
+++ b/pybop/plot/predictive.py
@@ -2,8 +2,7 @@
import numpy as np
-from pybop.plot.plotly_manager import PlotlyManager
-from pybop.plot.standard_plots import StandardPlot
+from pybop.plot.util import get_backend, remove_brackets
from pybop.problems.meta_problem import MetaProblem
from pybop.simulators.failed_solution import FailedSolution
@@ -21,13 +20,11 @@ def predictive(
pdf_label: str = "PDF",
colour_scale="viridis",
show: bool = True,
- **layout_kwargs,
+ backend: str | None = None,
):
"""
Plot the predictive posterior of a Bayesian optimisation result.
"""
- # Import plotly only when needed
- px = PlotlyManager().px
posterior_samples = result.posterior.sample_from_distribution(
n_samples=number_of_traces
@@ -44,16 +41,22 @@ def predictive(
else [result.problem]
)
figure_list = []
+ backend_module = get_backend(backend)
for problem in problems:
- plot_dict = StandardPlot(
- x=problem.domain_data,
- y=problem.target_data[problem.target[0]],
- layout_options=dict(
- xaxis_title=StandardPlot.remove_brackets(problem.domain),
- yaxis_title=StandardPlot.remove_brackets(problem.target[0]),
+ fig = backend_module.create_figure(
+ xaxis_title=remove_brackets(problem.domain),
+ yaxis_title=remove_brackets(problem.target[0]),
+ style={"bg_color": "white", "width": 600, "height": 600},
+ )
+
+ backend_module.plot_trace(
+ backend_module.line(
+ x=problem.domain_data,
+ y=problem.target_data[problem.target[0]],
+ label=data_legend_entry,
),
- trace_names=data_legend_entry,
+ fig,
)
# Simulate the samples and add to plot
@@ -61,43 +64,33 @@ def predictive(
simulations = problem.simulate_batch(inputs=inputs)
for pdf, sim in zip(posterior_samples_pdf, simulations, strict=False):
if not isinstance(sim, FailedSolution):
- plot_dict.add_traces(
- x=problem.domain_data,
- y=sim[problem.target[0]].data,
- line={
- "dash": "dot",
- "color": px.colors.sample_colorscale(
- colour_scale,
- (pdf - pdf_range[0]) / (pdf_range[1] - pdf_range[0]),
- )[0],
- },
+ colors = backend_module.sample_color_scale(
+ pdf, d_min=pdf_range[0], d_max=pdf_range[1]
+ )
+ backend_module.plot_trace(
+ backend_module.line(
+ x=problem.domain_data,
+ y=sim[problem.target[0]].data,
+ style=dict(color=colors[0], linestyle="dotted"),
+ ),
+ fig,
)
# Add the colourbar
- plot_dict.add_traces(
- x=[None],
- y=[None],
- mode="markers",
- marker={
- "size": 0,
- "color": pdf_range,
- "colorscale": colour_scale,
- "showscale": True,
- "colorbar": {"title": {"text": "Posterior PDF", "side": "right"}},
- },
+ backend_module.colorbar(
+ fig, pdf_range, colorscale=colour_scale, label="Posterior PDF"
)
if pdf_plot is not None:
- plot_dict.add_traces(
- x=pdf_plot[0],
- y=pdf_plot[1],
- trace_names=pdf_label,
+ backend_module.plot_trace(
+ backend_module.line(
+ x=pdf_plot[0],
+ y=pdf_plot[1],
+ labels=pdf_label,
+ )
)
-
- fig = plot_dict(show=False)
- fig.update_layout(**layout_kwargs)
if show:
- fig.show()
+ backend_module.show_figure(fig)
figure_list.append(fig)
diff --git a/pybop/plot/problem.py b/pybop/plot/problem.py
index df1d6703d..658812cfa 100644
--- a/pybop/plot/problem.py
+++ b/pybop/plot/problem.py
@@ -3,7 +3,7 @@
from pybop.costs.design_cost import DesignCost
from pybop.costs.error_measures import ErrorMeasure
from pybop.parameters.parameter import Inputs
-from pybop.plot.standard_plots import StandardPlot
+from pybop.plot.util import get_backend, remove_brackets
from pybop.problems.meta_problem import MetaProblem
from pybop.problems.problem import Problem
from pybop.simulators.solution import Solution
@@ -12,8 +12,9 @@
def problem(
problem: Problem,
inputs: Inputs = None,
+ title="Scatter Plot",
show: bool = True,
- **layout_kwargs,
+ backend: str = None,
):
"""
Produce a quick plot of the target dataset against optimised model output.
@@ -27,12 +28,12 @@ def problem(
Problem object with dataset and targets attributes.
inputs : Inputs
Optimised (or example) parameter values.
+ title: str, optional:
+ The title of the plot (default: "Scatter Plot")
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time / s"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
+ backend: str, optional
+ The plotting backend to be used.
Returns
-------
@@ -67,65 +68,64 @@ def problem(
model_domain = target_domain[: len(model_output[target].data)]
# Create a plot for each output
+ backend_module = get_backend(backend)
figure_list = []
for var in problem.target:
# Create a plot dictionary
- plot_dict = StandardPlot(
- layout_options=dict(
- title="Scatter Plot",
- xaxis_title=StandardPlot.remove_brackets(domain),
- yaxis_title=StandardPlot.remove_brackets(var),
- )
+ fig = backend_module.create_figure(
+ title=title,
+ xaxis_title=remove_brackets(domain),
+ yaxis_title=remove_brackets(var),
+ style={"bg_color": "white", "width": 600, "height": 600},
)
+ traces = []
- model_trace = plot_dict.create_trace(
+ model_trace = backend_module.line(
x=model_domain,
y=model_output[var].data,
- name="Optimised" if isinstance(problem.cost, DesignCost) else "Model",
- mode="markers" if isinstance(problem, MetaProblem) else "lines",
- showlegend=True,
+ label="Optimised" if isinstance(problem.cost, DesignCost) else "Model",
+ style={
+ "linestyle": "none" if isinstance(problem, MetaProblem) else "solid",
+ "marker": "." if isinstance(problem, MetaProblem) else "none",
+ },
)
- plot_dict.traces.append(model_trace)
+ traces.append(model_trace)
- target_trace = plot_dict.create_trace(
+ target_trace = backend_module.line(
x=target_domain,
y=target_output[var].data,
- name="Reference",
- mode="markers",
- showlegend=True,
+ label="Reference",
+ style={"linestyle": "none", "marker": "."},
)
- plot_dict.traces.append(target_trace)
+ traces.append(target_trace)
if isinstance(problem.cost, ErrorMeasure) and len(
model_output[var].data
) == len(target_output[var].data):
# Compute the standard deviation as proxy for uncertainty
- plot_dict.sigma = np.std(model_output[var].data - target_output[var].data)
+ sigma = np.std(model_output[var].data - target_output[var].data)
# Convert x and upper and lower limits into lists to create a filled trace
x = target_domain.tolist()
- y_upper = (model_output[var].data + plot_dict.sigma).tolist()
- y_lower = (model_output[var].data - plot_dict.sigma).tolist()
-
- fill_trace = plot_dict.create_trace(
- x=x + x[::-1],
- y=y_upper + y_lower[::-1],
- fill="toself",
- fillcolor="rgba(255,229,204,0.8)",
- line=dict(color="rgba(255,255,255,0)"),
- hoverinfo="skip",
- showlegend=False,
+ y_upper = (model_output[var].data + sigma).tolist()
+ y_lower = (model_output[var].data - sigma).tolist()
+
+ fill_trace = backend_module.fill_between(
+ x, y_upper, y_lower, color="#FFE5CC"
)
- plot_dict.traces.append(fill_trace)
+ traces.append(fill_trace)
# Reverse the order of the traces to put the model on top
- plot_dict.traces = plot_dict.traces[::-1]
+ traces = traces[::-1]
+
+ for trace in traces:
+ backend_module.plot_trace(trace, fig)
+
+ backend_module.legend(fig)
# Generate the figure and update the layout
- fig = plot_dict(show=False)
- fig.update_layout(**layout_kwargs)
if show:
- fig.show()
+ backend_module.show_figure(fig)
figure_list.append(fig)
diff --git a/pybop/plot/samples.py b/pybop/plot/samples.py
index 55ee77cd0..d240bc5d5 100644
--- a/pybop/plot/samples.py
+++ b/pybop/plot/samples.py
@@ -1,109 +1,109 @@
from typing import TYPE_CHECKING
-from pybop.plot import PlotlyManager
+from pybop.plot.util import get_backend
if TYPE_CHECKING:
from pybop.samplers.base_pints_sampler import SamplingResult
-def trace(result: "SamplingResult", **kwargs):
- """
- Plot trace plots for the posterior samples.
- """
- # Import plotly only when needed
- go = PlotlyManager().go
-
- for i in range(result.n_parameters):
- fig = go.Figure()
-
- for j, chain in enumerate(result.chains):
- fig.add_trace(go.Scatter(y=chain[:, i], mode="lines", name=f"Chain {j}"))
-
- fig.update_layout(
- title=f"Parameter {i} Trace Plot",
- xaxis_title="Sample Index",
- yaxis_title="Value",
- )
- fig.update_layout(**kwargs)
- fig.show()
-
-
-def chains(result: "SamplingResult", **kwargs):
+def chains(result: "SamplingResult", show=True, backend=None):
"""
Plot posterior distributions for each chain.
"""
- # Import plotly only when needed
- go = PlotlyManager().go
+ # Import backend
+ backend = get_backend(backend)
- fig = go.Figure()
+ fig = backend.create_figure(
+ title="Posterior Distribution",
+ xaxis_title="Value",
+ yaxis_title="Density",
+ )
for i, chain in enumerate(result.chains):
for j in range(chain.shape[1]):
- fig.add_trace(
- go.Histogram(
+ backend.plot_trace(
+ backend.histogram_plot(
x=chain[:, j],
name=f"Chain {i} - Parameter {j}",
- opacity=0.75,
- )
+ style=dict(alpha=0.75),
+ ),
+ fig,
)
- fig.add_shape(
- type="line",
- x0=result.mean[j],
- y0=0,
- x1=result.mean[j],
- y1=result.max[j],
- name=f"Mean - Parameter {j}",
- line=dict(color="Black", width=1.5, dash="dash"),
+ backend.vline(
+ fig,
+ result.mean[j],
+ style=dict(linewidth=3, linestyle="dashed", color="black"),
)
- fig.update_layout(
- barmode="overlay",
- title="Posterior Distribution",
- xaxis_title="Value",
- yaxis_title="Density",
- )
- fig.update_layout(**kwargs)
- fig.show()
+ backend.legend(fig)
+
+ if show:
+ backend.show_figure(fig)
-def posterior(result: "SamplingResult", **kwargs):
+def trace(result: "SamplingResult", show=True, backend=None):
"""
- Plot the summed posterior distribution across chains.
+ Plot trace plots for the posterior samples.
"""
- # Import plotly only when needed
- go = PlotlyManager().go
-
- fig = go.Figure()
+ # Import plotting backend
+ backend = get_backend(backend)
- for j in range(result.all_samples.shape[1]):
- histogram = go.Histogram(
- x=result.all_samples[:, j],
- name=f"Parameter {j}",
- opacity=0.75,
- )
- fig.add_trace(histogram)
- fig.add_vline(
- x=result.mean[j], line_width=3, line_dash="dash", line_color="black"
+ figlist = []
+ for i in range(result.n_parameters):
+ fig = backend.create_figure(
+ title=f"Parameter {i} Trace Plot",
+ xaxis_title="Sample Index",
+ yaxis_title="Value",
)
+ for j, chain in enumerate(result.chains):
+ backend.plot_trace(backend.line(y=chain[:, i], label=f"Chain {j}"), fig)
+ backend.legend(fig)
+ figlist.append(fig)
+
+ if show:
+ backend.show_figure(figlist)
+
+ return figlist
+
- fig.update_layout(
- barmode="overlay",
+def posterior(result: "SamplingResult", backend=None, show=True):
+ """
+ Plot the summed posterior distribution across chains.
+ """
+ # Import backend
+ backend = get_backend(backend)
+ fig = backend.create_figure(
title="Posterior Distribution",
xaxis_title="Value",
yaxis_title="Density",
)
- fig.update_layout(**kwargs)
- fig.show()
- return fig
+
+ for j in range(result.all_samples.shape[1]):
+ backend.plot_trace(
+ backend.histogram_plot(
+ x=result.all_samples[:, j],
+ name=f"Parameter {j}",
+ style=dict(alpha=0.75),
+ ),
+ fig,
+ )
+ backend.vline(
+ fig,
+ result.mean[j],
+ style=dict(linewidth=3, linestyle="dashed", color="black"),
+ )
+
+ backend.legend(fig)
+
+ if show:
+ backend.show_figure(fig)
-def summary_table(result: "SamplingResult"):
+def summary_table(result: "SamplingResult", backend=None):
"""
Display summary statistics in a table.
"""
- # Import plotly only when needed
- go = PlotlyManager().go
summary_stats = result.get_summary_statistics()
@@ -116,16 +116,9 @@ def summary_table(result: "SamplingResult"):
["95% CI Upper", summary_stats["ci_upper"]],
]
- fig = go.Figure(
- data=[
- go.Table(
- header=dict(values=header),
- cells=dict(
- values=[[row[0] for row in values], [row[1] for row in values]]
- ),
- )
- ]
+ backend = get_backend(backend)
+ backend.show_table(
+ header=header,
+ values=values,
+ title="Summary Statistics",
)
-
- fig.update_layout(title="Summary Statistics")
- fig.show()
diff --git a/pybop/plot/standard_plots.py b/pybop/plot/standard_plots.py
index 962f9a5f8..462257eaa 100644
--- a/pybop/plot/standard_plots.py
+++ b/pybop/plot/standard_plots.py
@@ -1,43 +1,12 @@
import math
-import textwrap
-import numpy as np
-
-from pybop.plot.plotly_manager import PlotlyManager
-
-DEFAULT_LAYOUT_OPTIONS = dict(
- title=None,
- title_x=0.5,
- xaxis=dict(
- title=dict(font={"size": 14}),
- showexponent="last",
- exponentformat="e",
- tickfont=dict(size=12),
- ),
- yaxis=dict(
- title=dict(font={"size": 14}),
- showexponent="last",
- exponentformat="e",
- tickfont=dict(size=12),
- ),
- legend=dict(x=1, y=1, xanchor="right", yanchor="top", font_size=12),
- showlegend=True,
- autosize=False,
- width=600,
- height=600,
- margin=dict(l=10, r=10, b=10, t=75, pad=4),
- plot_bgcolor="white",
-)
-DEFAULT_SUBPLOT_OPTIONS = dict(
- start_cell="bottom-left",
-)
-DEFAULT_TRACE_OPTIONS = dict(line=dict(width=4), mode="lines")
-DEFAULT_SUBPLOT_TRACE_OPTIONS = dict(line=dict(width=2), mode="lines")
+from pybop.plot.backends import PlotBackend
+from pybop.plot.util import AxisData, get_backend, parse_data, wrap_text
class StandardPlot:
"""
- A class for creating and displaying interactive Plotly figures.
+ A class for creating and displaying figures.
Parameters
----------
@@ -45,58 +14,53 @@ class StandardPlot:
X-axis data points.
y : list or np.ndarray, optional
Primary Y-axis data points for simulated model output.
- layout : Plotly layout, optional
- A layout for the figure, overrides the layout options (default: None).
- layout_options : dict, optional
- Settings to modify the default layout (default: DEFAULT_LAYOUT_OPTIONS).
- trace_options : dict, optional
+ title: str, optional
+ The title of the figure
+ xaxis_title: str, optional
+ Sets the title/label of the x-axis
+ yaxis_title: str, optional
+ Sets the title/label of the y-axis
Settings to modify the default trace type (default: DEFAULT_TRACE_OPTIONS).
- trace_names : str, optional
+ labels : str, optional
Name(s) for the primary trace(s) (default: None).
- trace_name_width : int, optional
- Maximum length of the trace names before text wrapping is used (default: 40).
+ label_width : int, optional
+ Maximum length of the labels before text wrapping is used (default: 40).
+ style: dict, optional
+ legend_style: dict, optional
+ backend: str or pybop.backends.PlotBackend, optional
+ Plotting backend to be used to create plot
Returns
-------
- plotly.graph_objs.Figure
+ plotly.graph_objs.Figure or matplotlib.figure.Figure
The generated Plotly figure.
"""
def __init__(
self,
- x=None,
- y=None,
- layout=None,
- layout_options=None,
- trace_options=None,
- trace_names=None,
- trace_name_width=40,
+ x,
+ y,
+ title: str = None,
+ xaxis_title: str = None,
+ yaxis_title: str = None,
+ labels: list[str] = None,
+ label_width=40,
+ style: dict = None,
+ legend_style: dict = None,
+ backend=None,
):
- self.traces = []
- self.layout = layout
- self.trace_name_width = trace_name_width
-
- # Set default layout options and update if provided
- if self.layout is None:
- self.layout_options = DEFAULT_LAYOUT_OPTIONS.copy()
- if layout_options:
- self.layout_options.update(layout_options)
-
- # Set default trace options and update if provided
- self.trace_options = DEFAULT_TRACE_OPTIONS.copy()
- if trace_options:
- self.trace_options.update(trace_options)
-
- # Attempt to import plotly when an instance is created
- self.go = PlotlyManager().go
+ self.lines = []
+ self.backend = backend
+ self.title = title
+ self.xaxis_title = xaxis_title
+ self.yaxis_title = yaxis_title
+ self.style = style
+ self.legend_style = legend_style
+ if not isinstance(self.backend, PlotBackend):
+ self.backend = get_backend(backend)
- # Create layout
- if self.layout is None:
- self.layout = self.go.Layout(**self.layout_options)
-
- # Add traces
if x is not None and y is not None:
- self.add_traces(x, y, trace_names)
+ self.add_lines(x, y, labels, label_width)
def __call__(self, show=True):
"""
@@ -107,15 +71,25 @@ def __call__(self, show=True):
show : bool, optional
If True, the figure is shown upon creation (default: True).
"""
- fig = self.go.Figure(data=self.traces, layout=self.layout)
- if show:
- fig.show()
+ fig = self.backend.create_figure(
+ title=self.title,
+ xaxis_title=self.xaxis_title,
+ yaxis_title=self.yaxis_title,
+ style=self.style,
+ traces=self.lines,
+ )
- return fig
+ if self.legend_style is not None:
+ self.backend.legend(fig, style=self.legend_style)
- def add_traces(self, x, y, trace_names=None, **trace_options):
+ if show:
+ self.backend.show_figure(fig)
+ else:
+ return fig
+
+ def add_lines(self, x, y, labels=None, labelwidth=40):
"""
- Add a set of traces to the plot dictionary.
+ Add a set of lines.
Parameters
----------
@@ -123,126 +97,28 @@ def add_traces(self, x, y, trace_names=None, **trace_options):
X-axis data points.
y : list or np.ndarray
Primary Y-axis data points for simulated model output.
- trace_names : str or list[str], optional
- Name(s) for the primary trace(s) (default: None).
+ labels : str or list[str], optional
+ Name(s) for the primary line(s) (default: None).
+ label_width : int, optional
+ Maximum length of the labels before text wrapping is used (default: 40).
"""
- options = self.trace_options.copy()
- options.update(trace_options)
-
- # Check and wrap trace names
- if trace_names is not None:
- if isinstance(trace_names, str):
- trace_names = [trace_names]
- for i, name in enumerate(trace_names):
- trace_names[i] = self.wrap_text(name, width=self.trace_name_width)
-
# Parse the data
- x, y = self.parse_data(x, y)
-
- # Create a trace for each trajectory
+ x, y = parse_data(x, y)
xi = x[0]
for i in range(0, len(y)):
- trace_options = options.copy()
if len(x) > 1:
xi = x[i]
- if trace_names is not None:
- trace_options["name"] = trace_names[i]
- else:
- trace_options["showlegend"] = False
- trace = self.create_trace(xi, y[i], **trace_options)
- self.traces.append(trace)
-
- def parse_data(self, x, y):
- """
- Check the type and dimensions of the data and convert if necessary to a list
- of 'things plotly can take', e.g. numpy arrays or lists of numbers.
-
- Parameters
- ----------
- x : list or np.ndarray, optional
- X-axis data points.
- y : list or np.ndarray, optional
- Primary Y-axis data points for simulated model output.
- """
- if isinstance(x, list):
- # If it's a list of numpy arrays, it's fine
- # If it's a list of lists, it's fine
- # If it's neither, it's a list of numbers that we need to wrap
- if not isinstance(x[0], np.ndarray) and not isinstance(x[0], list):
- x = [x]
- elif isinstance(x, np.ndarray):
- x = np.squeeze(x)
- if x.ndim == 1:
- x = [x]
- else:
- x = x.tolist()
- if isinstance(y, list):
- if not isinstance(y[0], np.ndarray) and not isinstance(y[0], list):
- y = [y]
- if isinstance(y, np.ndarray):
- y = np.squeeze(y)
- if y.ndim == 1:
- y = [y]
- else:
- y = y.tolist()
- if len(x) > 1 and len(x) != len(y):
- raise ValueError(
- "Input x should have either one data series or the same number as y."
- )
- return x, y
-
- def create_trace(self, x, y, **trace_options):
- """
- Create a trace for the Plotly figure.
-
- Returns
- -------
- plotly.graph_objs.Scatter
- A trace for a Plotly figure.
- """
- return self.go.Scatter(x=x, y=y, **trace_options)
-
- @staticmethod
- def wrap_text(text, width):
- """
- Wrap text to a specified width with HTML line breaks.
+ label = None
+ if labels is not None:
+ label = wrap_text(labels[i], 30, backend=self.backend.name)
- Parameters
- ----------
- text : str
- The text to wrap.
- width : int
- The width to wrap the text to.
-
- Returns
- -------
- str
- The wrapped text.
- """
- wrapped_text = textwrap.fill(text, width=width, break_long_words=False)
- return wrapped_text.replace("\n", "
")
-
- @staticmethod
- def remove_brackets(s):
- """
- Remove square brackets from a string and replace with forward slashes
- as per section 7.1 of the SI Handbook
- """
- # If s is an iterable (but not a string), apply the function recursively to each element
- if hasattr(s, "__iter__") and not isinstance(s, str):
- return type(s)(StandardPlot.remove_brackets(i) for i in s)
- elif isinstance(s, str):
- start = s.find("[")
- end = s.find("]")
- if start != -1 and end != -1:
- char_in_brackets = s[start + 1 : end]
- return s[:start] + " / " + char_in_brackets + s[end + 1 :]
- return s
+ line = self.backend.line(xi, y[i], label)
+ self.lines.append(line)
class StandardSubplot(StandardPlot):
"""
- A class for creating and displaying a set of interactive Plotly figures in a grid layout.
+ A class for creating and displaying a set of figures in a grid layout.
Parameters
----------
@@ -254,59 +130,70 @@ class StandardSubplot(StandardPlot):
Number of rows of subplots, can be set automatically (default: None).
num_cols : int, optional
Number of columns of subplots, can be set automatically (default: None).
- layout : Plotly layout, optional
- A layout for the figure, overrides the layout options (default: None).
- layout_options : dict, optional
- Settings to modify the default layout (default: DEFAULT_LAYOUT_OPTIONS).
- trace_options : dict, optional
- Settings to modify the default trace type (default: DEFAULT_TRACE_OPTIONS).
- trace_names : str, optional
+ title: str, optional
+ Title of the Figure
+ xaxis_titles: str or list of str, optional
+ titles for the x-axes (default: None)
+ yaxis_titles: str or list of str, optional
+ titles for the x-axes (default: None)
+
+ labels : str, optional
Name(s) for the primary trace(s) (default: None).
- trace_name_width : int, optional
+ label_width : int, optional
Maximum length of the trace names before text wrapping is used (default: 40).
+ style: dict, optional
+ Options for figure layout
+ backend: str or pybop.plot.backends.PlotBackend
Returns
-------
- plotly.graph_objs.Figure
- The generated Plotly figure.
+ plotly.graph_objs.Figure or matplotlib.figure.Figure
+ The generated figure.
"""
def __init__(
self,
x,
y,
- num_rows=None,
- num_cols=None,
- axis_titles=None,
- layout=None,
- layout_options=DEFAULT_LAYOUT_OPTIONS,
- subplot_options=DEFAULT_SUBPLOT_OPTIONS,
- trace_options=DEFAULT_SUBPLOT_TRACE_OPTIONS,
- trace_names=None,
- trace_name_width=40,
+ num_rows: int = None,
+ num_cols: int = None,
+ title: str = None,
+ xaxis_titles: list[str] | str = None,
+ yaxis_titles: list[str] | str = None,
+ labels: list[str] = None,
+ label_width: int = 40,
+ style: dict = None,
+ backend=None,
):
super().__init__(
- x, y, layout, layout_options, trace_options, trace_names, trace_name_width
+ x,
+ y,
+ title=title,
+ xaxis_title=xaxis_titles,
+ yaxis_title=yaxis_titles,
+ labels=labels,
+ label_width=label_width,
+ style=style,
+ backend=backend,
)
- self.num_traces = len(self.traces)
+
+ self.num_lines = len(self.lines)
self.num_rows = num_rows
self.num_cols = num_cols
if self.num_rows is None and self.num_cols is None:
# Work out the number of subplots
- self.num_cols = int(math.ceil(math.sqrt(self.num_traces)))
- self.num_rows = int(math.ceil(self.num_traces / self.num_cols))
+ self.num_cols = int(math.ceil(math.sqrt(self.num_lines)))
+ self.num_rows = int(math.ceil(self.num_lines / self.num_cols))
elif self.num_rows is None:
- self.num_rows = int(math.ceil(self.num_traces / self.num_cols))
+ self.num_rows = int(math.ceil(self.num_lines / self.num_cols))
elif self.num_cols is None:
- self.num_cols = int(math.ceil(self.num_traces / self.num_rows))
- self.axis_titles = axis_titles
- self.subplot_options = subplot_options.copy()
- if subplot_options is not None:
- for arg, value in subplot_options.items():
- self.subplot_options[arg] = value
+ self.num_cols = int(math.ceil(self.num_lines / self.num_rows))
- # Attempt to import plotly when an instance is created
- self.make_subplots = PlotlyManager().make_subplots
+ self.axes_data = []
+ for idx in range(self.num_lines):
+ row = (idx // self.num_cols) + 1
+ col = (idx % self.num_cols) + 1
+ self.axes_data.append(AxisData(row, col))
def __call__(self, show):
"""
@@ -317,70 +204,21 @@ def __call__(self, show):
show : bool, optional
If True, the figure is shown upon creation (default: True).
"""
- fig = self.make_subplots(
- rows=self.num_rows,
- cols=self.num_cols,
- horizontal_spacing=0.1,
- vertical_spacing=0.15,
- **self.subplot_options,
+
+ fig, self.axes, self.num_rows, self.num_cols = self.backend.make_subplots(
+ self.axes_data,
+ title=self.title,
+ xaxis_titles=self.xaxis_title,
+ yaxis_titles=self.yaxis_title,
+ style=self.style,
)
- fig.update_layout(self.layout_options)
- for idx, trace in enumerate(self.traces):
+ for idx, line in enumerate(self.lines):
row = (idx // self.num_cols) + 1
col = (idx % self.num_cols) + 1
- fig.add_trace(trace, row=row, col=col)
-
- if self.axis_titles and idx < len(self.axis_titles):
- x_title, y_title = self.axis_titles[idx]
- fig.update_xaxes(title_text=x_title, row=row, col=col)
- fig.update_yaxes(
- title_text=y_title,
- row=row,
- col=col,
- showexponent="last",
- exponentformat="e",
- )
+ self.backend.plot_trace(line, fig, self.axes[(row, col)])
if show:
- fig.show()
-
- return fig
-
-
-def trajectories(x, y, trace_names=None, show=True, **layout_kwargs):
- """
- Quickly plot one or more trajectories using Plotly.
-
- Parameters
- ----------
- x : list or np.ndarray
- X-axis data points.
- y : list or np.ndarray
- Y-axis data points for each trajectory.
- trace_names : list or str, optional
- Name(s) for the trace(s) (default: None).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time / s"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
-
- Returns
- -------
- plotly.graph_objs.Figure
- The Plotly figure object for the scatter plot.
- """
- # Create a plot dictionary
- plot_dict = StandardPlot(
- x=x,
- y=y,
- trace_names=trace_names,
- )
-
- # Generate the figure and update the layout
- fig = plot_dict(show=False)
- fig.update_layout(**layout_kwargs)
- if show:
- fig.show()
-
- return fig
+ self.backend.show_figure(fig)
+ else:
+ return fig
diff --git a/pybop/plot/trajectories.py b/pybop/plot/trajectories.py
new file mode 100644
index 000000000..0c6291777
--- /dev/null
+++ b/pybop/plot/trajectories.py
@@ -0,0 +1,60 @@
+from pybop.plot.standard_plots import StandardPlot
+
+
+def trajectories(
+ x,
+ y,
+ title: str = None,
+ xaxis_title: str = None,
+ yaxis_title: str = None,
+ labels=None,
+ label_width=20,
+ show=True,
+ backend=None,
+):
+ """
+ Quickly plot one or more trajectories using Plotly.
+
+ Parameters
+ ----------
+ x : list or np.ndarray
+ X-axis data points.
+ y : list or np.ndarray
+ Y-axis data points for each trajectory.
+ title: str, optional
+ The title of the figure
+ xaxis_title: str, optional
+ Sets the title/label of the x-axis
+ yaxis_title: str, optional
+ Sets the title/label of the y-axis
+ Settings to modify the default trace type (default: DEFAULT_TRACE_OPTIONS).
+ labels : list or str, optional
+ Name(s) for the trace(s) (default: None).
+ label_width : int, optional
+ Maximum length of the labels before text wrapping is used (default: 20).
+ show : bool, optional
+ If True, the figure is shown upon creation (default: True).
+ backend: str, optional
+ The plotting backend to be used.
+
+ Returns
+ -------
+ plotly.graph_objs.Figure
+ The Plotly figure object for the scatter plot.
+ """
+ plot_dict = StandardPlot(
+ x,
+ y,
+ title=title,
+ xaxis_title=xaxis_title,
+ yaxis_title=yaxis_title,
+ labels=labels,
+ label_width=label_width,
+ style={"height": 600, "width": 600, "bg_color": "white"},
+ legend_style={},
+ backend=backend,
+ )
+
+ fig = plot_dict(show=show)
+
+ return fig
diff --git a/pybop/plot/util.py b/pybop/plot/util.py
new file mode 100644
index 000000000..b0059e159
--- /dev/null
+++ b/pybop/plot/util.py
@@ -0,0 +1,139 @@
+import textwrap
+from dataclasses import dataclass
+
+import numpy as np
+
+import pybop.plot
+
+
+def use_backend(backend):
+ """
+ Select a plotting backend to be used for all subsequent plots.
+
+ Parameters
+ ----------
+ backend : str
+ The plotting backend to be used.
+ """
+ err_msg = (
+ f"Plotting backend {backend} is not available. The current backend has not been updated. \n"
+ f"The current backend is set to {pybop.plot.current_backend}"
+ )
+ if backend.lower() in ["matplotlib", "plotly"]:
+ pybop.plot.current_backend = backend
+
+ else:
+ raise ModuleNotFoundError(err_msg)
+
+
+def get_backend(backend=None):
+ """
+ Get instance of PlotBackend class for a given plotting backend
+
+ Parameters
+ ----------
+ backend : str, optional
+ The plotting backend to be used (default: pybop.plot.current_backend)
+ """
+ if backend is None:
+ backend = pybop.plot.current_backend
+ err_msg = f"Plotting backend {backend} is not available."
+ if backend.lower() == "matplotlib":
+ return pybop.plot.backends.MatplotlibBackend()
+ elif backend.lower() == "plotly":
+ return pybop.plot.backends.PlotlyBackend()
+ else:
+ raise ModuleNotFoundError(err_msg)
+
+
+@dataclass
+class AxisData:
+ """
+ Simple dataclass to store info needed to construct
+ subplots from specifying size and location of individual axes.
+ """
+
+ row: int = 1
+ col: int = 1
+ row_span: int = 1
+ col_span: int = 1
+
+
+def parse_data(x, y):
+ """
+ Check the type and dimensions of the data and convert if necessary to a list
+ of 'things plotly can take', e.g. numpy arrays or lists of numbers.
+
+ Parameters
+ ----------
+ x : list or np.ndarray, optional
+ X-axis data points.
+ y : list or np.ndarray, optional
+ Primary Y-axis data points for simulated model output.
+ """
+ if isinstance(x, list):
+ # If it's a list of numpy arrays, it's fine
+ # If it's a list of lists, it's fine
+ # If it's neither, it's a list of numbers that we need to wrap
+ if not isinstance(x[0], np.ndarray) and not isinstance(x[0], list):
+ x = [x]
+ elif isinstance(x, np.ndarray):
+ x = np.squeeze(x)
+ if x.ndim == 1:
+ x = [x]
+ else:
+ x = x.tolist()
+ if isinstance(y, list):
+ if not isinstance(y[0], np.ndarray) and not isinstance(y[0], list):
+ y = [y]
+ if isinstance(y, np.ndarray):
+ y = np.squeeze(y)
+ if y.ndim == 1:
+ y = [y]
+ else:
+ y = y.tolist()
+ if len(x) > 1 and len(x) != len(y):
+ raise ValueError(
+ "Input x should have either one data series or the same number as y."
+ )
+ return x, y
+
+
+def remove_brackets(s):
+ """
+ Remove square brackets from a string and replace with forward slashes
+ as per section 7.1 of the SI Handbook
+ """
+ # If s is an iterable (but not a string), apply the function recursively to each element
+ if hasattr(s, "__iter__") and not isinstance(s, str):
+ return type(s)(remove_brackets(i) for i in s)
+ elif isinstance(s, str):
+ start = s.find("[")
+ end = s.find("]")
+ if start != -1 and end != -1:
+ char_in_brackets = s[start + 1 : end]
+ return s[:start] + " / " + char_in_brackets + s[end + 1 :]
+ return s
+
+
+def wrap_text(text, width, backend="matplotlib"):
+ """
+ Wrap text to a specified width with HTML line breaks.
+
+ Parameters
+ ----------
+ text : str
+ The text to wrap.
+ width : int
+ The width to wrap the text to.
+
+ Returns
+ -------
+ str
+ The wrapped text.
+ """
+ wrapped_text = textwrap.fill(text, width=width, break_long_words=False)
+ if backend == "plotly":
+ return wrapped_text.replace("\n", "
")
+ else:
+ return wrapped_text
diff --git a/pybop/plot/voronoi.py b/pybop/plot/voronoi.py
index 29be3c096..f79bc882b 100644
--- a/pybop/plot/voronoi.py
+++ b/pybop/plot/voronoi.py
@@ -1,11 +1,12 @@
from typing import TYPE_CHECKING
import numpy as np
-from scipy.spatial import Voronoi, cKDTree
+from scipy.spatial import Voronoi
+
+from pybop.plot.util import get_backend
if TYPE_CHECKING:
from pybop._result import Result
-from pybop.plot.plotly_manager import PlotlyManager
def _voronoi_regions(x, y, f, xlim, ylim):
@@ -195,44 +196,13 @@ def interpolate_point(p, q, axis, boundary_val):
return np.array([boundary_val, s]) if axis == 0 else np.array([s, boundary_val])
-def assign_nearest_value(x, y, f, xi, yi):
- """
- Computes an array of values given by the score of the nearest point.
-
- Parameters
- ----------
- x : array-like
- The x coordinates of points with known scores.
- y : array-like
- The y coordinates of points with known scores.
- f : array-like
- The score function at the given x and y coordinates.
- xi : array-like
- The x coordinates of grid points.
- yi : array-like
- The y coordinates of grid points.
-
- Returns
- -------
- A numpy array containing the scores corresponding to the grid points.
- """
- # Create a KD-tree for efficient nearest neighbor search
- tree = cKDTree(np.column_stack((x, y)))
-
- # Find the nearest point for each grid point
- _, indices = tree.query(np.column_stack((xi.ravel(), yi.ravel())))
- zi = f[indices].reshape(xi.shape)
-
- return zi
-
-
def surface(
result: "Result",
+ title="Voronoi Cost Landscape",
bounds=None,
normalise=True,
- resolution=250,
show=True,
- **layout_kwargs,
+ backend=None,
):
"""
Plot a 2D representation of the Voronoi diagram with color-coded regions.
@@ -241,21 +211,20 @@ def surface(
-----------
result : pybop.Result
Optimisation result containing the history of parameter values and associated cost.
+ title: str, optional
+ The title of the plot (default: "Voronoi Cost Landscape")
bounds : numpy.ndarray, optional
A 2x2 array specifying the [min, max] bounds for each parameter. If None, uses
`cost.parameters.get_bounds_for_plotly`.
normalise : bool, optional
If True, the voronoi regions are computed using the Euclidean distance between
points normalised with respect to the bounds (default: True).
- resolution : int, optional
- Resolution of the plot. Default is 500.
show : bool, optional
If True, the figure is shown upon creation (default: True).
- **layout_kwargs : optional
- Valid Plotly layout keys and their values,
- e.g. `xaxis_title="Time [s]"` or
- `xaxis={"title": "Time [s]", font={"size":14}}`
+ backend: str, optional
+ The plotting backend to be used.
"""
+ backend = get_backend(backend)
points = result.x_model
parameters = result.problem.parameters
@@ -270,11 +239,6 @@ def surface(
bounds if bounds is not None else [param.bounds for param in parameters]
)[:2]
- # Create a grid for plot
- xi = np.linspace(xlim[0], xlim[1], resolution)
- yi = np.linspace(ylim[0], ylim[1], resolution)
- xi, yi = np.meshgrid(xi, yi)
-
if normalise:
if xlim[1] <= xlim[0] or ylim[1] <= ylim[0]:
raise ValueError("Lower bounds must be strictly less than upper bounds.")
@@ -290,13 +254,6 @@ def surface(
norm_x_optim, norm_y_optim, f, (0, 1), (0, 1)
)
- # Create a normalised grid
- norm_xi = np.linspace(0, 1, resolution)
- norm_xi, norm_yi = np.meshgrid(norm_xi, norm_xi)
-
- # Assign a value to each point in the grid
- zi = assign_nearest_value(norm_x, norm_y, f, norm_xi, norm_yi)
-
# Rescale for plotting
regions = []
for norm_region in norm_regions:
@@ -309,116 +266,97 @@ def surface(
# Compute regions
x, y, f, regions = _voronoi_regions(x_optim, y_optim, f, xlim, ylim)
- # Assign a value to each point in the grid
- zi = assign_nearest_value(x, y, f, xi, yi)
-
- # Calculate the size of each Voronoi region
- region_sizes = np.array([len(region) for region in regions])
- relative_sizes = (region_sizes - region_sizes.min()) / (
- region_sizes.max() - region_sizes.min()
- )
-
# Construct figure
- go = PlotlyManager().go
- fig = go.Figure()
-
- # Heatmap
- fig.add_trace(
- go.Heatmap(
- x=xi[0],
- y=yi[:, 0],
- z=zi,
- colorscale="Viridis",
- zsmooth="best",
- )
+ names = parameters.names
+ fig = backend.create_figure(
+ title=title,
+ xaxis_title=names[0],
+ yaxis_title=names[1],
+ style={
+ "width": 600,
+ "height": 600,
+ "xaxis_range": xlim,
+ "yaxis_range": ylim,
+ },
)
- # Add Voronoi edges
- for region, size in zip(regions, relative_sizes, strict=False):
+ # Add Voronoi edges and fill Voronoi regions
+ colors = backend.sample_color_scale(f)
+ for j, region in enumerate(regions):
x_region = region[:, 0].tolist() + [region[0, 0]]
y_region = region[:, 1].tolist() + [region[0, 1]]
- fig.add_trace(
- go.Scatter(
- x=x_region,
- y=y_region,
- mode="lines",
- line=dict(color="white", width=0.5 + size * 0.1),
- showlegend=False,
- )
+ backend.plot_trace(
+ backend.fill(x_region, y_region, color=colors[j], label=f"f={f[j]:.2f}"),
+ fig,
+ )
+
+ backend.plot_trace(
+ backend.line(x_region, y_region, style=dict(color="white", linewidth=0.5)),
+ fig,
)
+ backend.colorbar(fig, f)
+
# Add original points
- fig.add_trace(
- go.Scatter(
+ backend.plot_trace(
+ backend.scatter(
x=x_optim,
y=y_optim,
- mode="markers",
- marker=dict(
- color=[i / len(x_optim) for i in range(len(x_optim))],
- colorscale="Greys",
- size=8,
- showscale=False,
- ),
- text=[f"f={val:.2f}" for val in f],
- hoverinfo="text",
- showlegend=False,
- )
+ colors=[i / len(x_optim) for i in range(len(x_optim))],
+ labels=[f"f={val:.2f}" for val in f],
+ ),
+ fig,
)
# Plot the initial guess
if len(result.x_model) > 0:
x0 = result.x_model[0]
- fig.add_trace(
- go.Scatter(
+ backend.plot_trace(
+ backend.line(
x=[x0[0]],
y=[x0[1]],
- mode="markers",
- marker_symbol="x",
- marker=dict(
- color="white",
- line_color="black",
- line_width=1,
- size=14,
- showscale=False,
+ label="Initial values",
+ style=dict(
+ marker="X",
+ markersize=14,
+ markerfacecolor="white",
+ markeredgecolor="black",
+ linestyle="None",
+ zorder=2.6,
),
- name="Initial values",
- )
+ ),
+ fig,
)
# Plot optimised value
if result.x is not None:
x_best = result.x
- fig.add_trace(
- go.Scatter(
+ backend.plot_trace(
+ backend.line(
x=[x_best[0]],
y=[x_best[1]],
- mode="markers",
- marker_symbol="cross",
- marker=dict(
- color="black",
- line_color="white",
- line_width=1,
- size=14,
- showscale=False,
+ label="Final values",
+ style=dict(
+ marker="P",
+ markersize=14,
+ markerfacecolor="black",
+ markeredgecolor="white",
+ linestyle="None",
+ zorder=2.6,
),
- name="Final values",
- )
+ ),
+ fig,
)
- names = parameters.names
- fig.update_layout(
- title="Voronoi Cost Landscape",
- title_x=0.5,
- title_y=0.905,
- xaxis_title=names[0],
- yaxis_title=names[1],
- width=600,
- height=600,
- xaxis=dict(range=xlim, showexponent="last", exponentformat="e"),
- yaxis=dict(range=ylim, showexponent="last", exponentformat="e"),
- legend=dict(orientation="h", yanchor="bottom", y=1, xanchor="right", x=1),
+ backend.legend(
+ fig,
+ style={
+ "horizontal": True,
+ "loc": "lower right",
+ "coords": (1, 1),
+ },
)
- fig.update_layout(**layout_kwargs)
+
if show:
- fig.show()
+ backend.show_figure(fig)
diff --git a/tests/plotting/test_plotly_manager.py b/tests/plotting/test_plotly_manager.py
index 7050c1425..d5dfb9093 100644
--- a/tests/plotting/test_plotly_manager.py
+++ b/tests/plotting/test_plotly_manager.py
@@ -8,7 +8,7 @@
import pytest
import pybop
-from pybop.plot import PlotlyManager
+from pybop.plot.backends import PlotlyManager
# Find the Python executable
python_executable = which("python")
@@ -130,6 +130,9 @@ def dataset(plotly_installed):
@pytest.mark.unit
def test_standard_plot(dataset, plotly_installed):
+ # Set plotting backend
+ pybop.plot.use_backend("plotly")
+
# Check the StandardPlot class
pybop.plot.StandardPlot(dataset["Time [s]"], dataset["Voltage [V]"])
@@ -169,6 +172,9 @@ def test_standard_plot(dataset, plotly_installed):
@pytest.mark.unit
def test_plot_dataset(dataset, plotly_installed):
+ # Set plotting backend
+ pybop.plot.use_backend("plotly")
+
# Test plot of a dataset
pybop.plot.dataset(dataset, signal=["Voltage [V]"])
pybop.plot.dataset(dataset, signal=["Voltage [V]", "Current [A]"])
diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py
index 6f9c19d1d..1b57dd5c5 100644
--- a/tests/unit/test_plots.py
+++ b/tests/unit/test_plots.py
@@ -6,6 +6,7 @@
import pybop
+@pytest.mark.parametrize("backend", ["plotly", "matplotlib"])
class TestPlots:
"""
A class to test the plot classes.
@@ -13,15 +14,14 @@ class TestPlots:
pytestmark = pytest.mark.unit
- def test_standard_plot(self):
+ def test_standard_plot(self, backend):
# Test standard plot
- trace_names = pybop.plot.StandardPlot.remove_brackets(
- ["Trace [1]", "Trace [2]"]
- )
+ labels = pybop.plot.remove_brackets(["Trace [1]", "Trace [2]"])
plot_dict = pybop.plot.StandardPlot(
x=np.ones((2, 10)),
y=np.ones((2, 10)),
- trace_names=trace_names,
+ labels=labels,
+ backend=backend,
)
plot_dict()
@@ -69,12 +69,13 @@ def dataset(self, model):
solution = pybamm.Simulation(model).solve(t_eval=t_eval, t_interp=t_eval)
return pybop.import_pybamm_solution(solution)
- def test_dataset_plots(self, dataset):
+ def test_dataset_plots(self, dataset, backend):
+ pybop.plot.use_backend(backend)
# Test plot of Dataset objects
pybop.plot.trajectories(
dataset["Time [s]"],
dataset["Voltage [V]"],
- trace_names=["Time [s]", "Voltage [V]"],
+ labels=["Time [s]", "Voltage [V]"],
)
pybop.plot.dataset(dataset)
@@ -112,7 +113,8 @@ def design_problem(self, model, parameters, experiment):
)
return pybop.Problem(simulator)
- def test_problem_plots(self, fitting_problem, design_problem):
+ def test_problem_plots(self, fitting_problem, design_problem, backend):
+ pybop.plot.use_backend(backend)
# Test plot of Problem objects
pybop.plot.problem(fitting_problem, title="Optimised Comparison")
pybop.plot.problem(design_problem)
@@ -122,7 +124,8 @@ def test_problem_plots(self, fitting_problem, design_problem):
fitting_problem, inputs=fitting_problem.parameters.to_dict([0.6, 0.6])
)
- def test_cost_plots(self, fitting_problem, fitting_problem_no_bounds):
+ def test_cost_plots(self, fitting_problem, fitting_problem_no_bounds, backend):
+ pybop.plot.use_backend(backend)
# Test plot of Cost objects
pybop.plot.contour(fitting_problem, gradient=True, steps=5)
@@ -143,7 +146,8 @@ def result(self, fitting_problem):
optim = pybop.XNES(fitting_problem)
return optim.run()
- def test_optim_plots(self, result):
+ def test_optim_plots(self, result, backend):
+ pybop.plot.use_backend(backend)
bounds = np.asarray([[0.5, 0.8], [0.4, 0.7]])
# Plot convergence
@@ -185,7 +189,8 @@ def sampling_result(self, model, parameters, dataset):
sampler = pybop.SliceStepoutMCMC(log_pdf, options=options)
return sampler.run()
- def test_posterior_plots(self, sampling_result):
+ def test_posterior_plots(self, sampling_result, backend):
+ pybop.plot.use_backend(backend)
sampling_result.get_summary_statistics()
# Plot trace
@@ -208,9 +213,11 @@ def test_posterior_plots(self, sampling_result):
sampling_result.problem.parameters, sampling_result.posterior
)
- def test_with_ipykernel(self, dataset, fitting_problem, result):
+ def test_with_ipykernel(self, dataset, fitting_problem, result, backend):
import ipykernel
+ pybop.plot.use_backend(backend)
+
assert version.parse(ipykernel.__version__) >= version.parse("0.6")
pybop.plot.dataset(dataset, signal=["Voltage [V]"])
pybop.plot.contour(fitting_problem, gradient=True, steps=5)
@@ -218,7 +225,8 @@ def test_with_ipykernel(self, dataset, fitting_problem, result):
result.plot_parameters()
result.plot_contour(steps=5)
- def test_contour_incorrect_number_of_parameters(self, model, dataset):
+ def test_contour_incorrect_number_of_parameters(self, model, dataset, backend):
+ pybop.plot.use_backend(backend)
parameter_values = model.default_parameter_values
# Test with less than two paramters
@@ -259,7 +267,8 @@ def test_contour_incorrect_number_of_parameters(self, model, dataset):
fitting_problem = pybop.Problem(simulator, cost)
pybop.plot.contour(fitting_problem)
- def test_nyquist(self):
+ def test_nyquist(self, backend):
+ pybop.plot.use_backend(backend)
# Define model
model = pybamm.lithium_ion.SPM(options={"surface form": "differential"})
parameter_values = model.default_parameter_values