battery interactive charts
<style>
.ignis-battery-chart { border-radius: 8px; overflow: hidden; }
html.theme-dark .ignis-battery-chart { border: 2px solid #a89984; }
html.theme-light .ignis-battery-chart { border: 2px solid #504945; }
</style>
<script>
/* Interactive battery charts — custom hover, theme-aware legend + frame. */
(function () {
if (typeof window.IGNIS_CHARTS_DONE !== "undefined") return;
var PLOTLY_SRC = "api/notes/I9ZVK5qMTYMk/download";
var D = {"soc":[0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100],"temps":[-10,0,10,20,30,40],"soc10":[0,10,20,30,40,50,60,70,80,90,100],"temps10":[-10,0,10,20,30,40],"ocv_p":[[3.3557,3.4574,3.4943,3.5299,3.5607,3.5807,3.5961,3.611,3.6271,3.6474,3.6752,3.7131,3.7629,3.8058,3.8502,3.8966,3.9447,3.9952,4.0488,4.105,4.1653],[3.2482,3.4153,3.4508,3.5005,3.5412,3.5672,3.5833,3.5988,3.6148,3.6329,3.6577,3.6973,3.7513,3.7956,3.8428,3.8923,3.9433,3.9969,4.0539,4.1138,4.178],[3.2572,3.4098,3.4421,3.4935,3.54,3.567,3.5839,3.5997,3.616,3.6342,3.6585,3.7018,3.7533,3.7974,3.8456,3.8961,3.9482,4.0031,4.0613,4.1223,4.182],[3.2849,3.4117,3.444,3.4968,3.543,3.5716,3.59,3.6061,3.6219,3.6394,3.6627,3.7074,3.7512,3.7942,3.8414,3.891,3.9424,3.9965,4.0535,4.1132,4.1759],[3.1824,3.3982,3.4352,3.4836,3.5325,3.5646,3.5871,3.604,3.6201,3.6377,3.6604,3.7026,3.7494,3.7927,3.8407,3.8913,3.9436,3.9986,4.0567,4.1175,4.1802],[3.0798,3.3847,3.4264,3.4703,3.522,3.5575,3.5841,3.6018,3.6183,3.636,3.6581,3.6979,3.7475,3.7911,3.8401,3.8917,3.9448,4.0007,4.06,4.1218,4.1845]],"ocv_m":[[2.9627,3.1784,3.4015,3.4565,3.517,3.5559,3.5907,3.6169,3.6419,3.6826,3.7264,3.7915,3.8541,3.8905,3.9338,3.9855,4.035,4.0615,4.0798,4.118,4.1629],[2.9659,3.1819,3.3961,3.4551,3.514,3.5514,3.595,3.6171,3.6471,3.6845,3.7243,3.7894,3.8598,3.8926,3.931,3.9839,4.0364,4.0624,4.0861,4.1201,4.1586],[2.9717,3.1832,3.4007,3.4582,3.5125,3.5563,3.591,3.6221,3.649,3.6848,3.7255,3.7944,3.8591,3.8927,3.93,3.9863,4.0397,4.0627,4.0831,4.1216,4.1635],[2.9762,3.1853,3.4005,3.4591,3.513,3.5531,3.5976,3.6235,3.6498,3.69,3.7273,3.7946,3.8564,3.8928,3.9347,3.9879,4.0373,4.0603,4.0877,4.1265,4.1649],[2.9787,3.1863,3.4001,3.4538,3.5156,3.5565,3.5942,3.6204,3.6477,3.6932,3.734,3.7916,3.8568,3.8979,3.9317,3.9871,4.0375,4.0638,4.0839,4.1214,4.1607],[2.9816,3.192,3.3993,3.4543,3.5159,3.554,3.5989,3.6237,3.6559,3.6913,3.7351,3.7942,3.8594,3.8939,3.9317,3.9888,4.0417,4.0613,4.0894,4.1222,4.1621]],"disc_p":[[0.0,20.0,42.0,48.0,58.0,64.0,68.0,70.0,70.0,70.0,70.0],[0.0,76.0,108.0,144.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,108.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,116.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,108.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,76.0,86.0,96.0,108.0,108.0,108.0,108.0,108.0,108.0,108.0]],"disc_m":[[0.0,1.6,232.8,337.6,379.9,400.6,568.7,631.2,649.5,670.1,691.3],[0.0,40.1,397.2,505.2,568.8,673.1,757.2,820.3,862.3,904.7,946.9],[0.0,127.6,526.4,565.7,712.5,796.9,880.6,946.8,988.9,1027.3,1048.1],[0.0,274.8,568.5,715.3,820.3,902.0,988.3,1066.3,1105.9,1192.7,1213.2],[0.0,408.7,636.3,810.4,894.2,977.8,1059.0,1142.8,1185.1,1300.4,1330.2],[0.0,526.1,715.7,859.6,943.9,1030.2,1114.5,1195.8,1240.6,1366.7,1408.1]],"chg_p":[[5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,3.0,2.0,0.0],[15.0,15.0,15.0,15.0,15.0,15.0,15.0,15.0,11.0,6.0,0.0],[46.0,46.0,46.0,46.0,46.0,46.0,46.0,46.0,32.0,20.0,0.0],[86.0,86.0,86.0,86.0,86.0,86.0,86.0,66.0,46.0,28.0,0.0],[100.0,100.0,100.0,100.0,100.0,100.0,100.0,100.0,74.0,46.0,0.0],[86.0,86.0,86.0,86.0,86.0,86.0,86.0,66.0,46.0,28.0,0.0]],"chg_m":[[232.7,229.2,208.9,187.9,187.1,148.8,106.7,40.7,41.0,2.0,0.0],[337.8,337.6,316.6,292.8,274.9,232.2,169.4,127.4,40.8,40.1,0.0],[439.7,418.6,398.0,376.7,337.8,271.1,211.5,166.3,85.5,43.2,0.0],[559.9,508.1,475.0,454.3,410.0,316.8,256.7,199.0,115.9,43.8,0.0],[631.5,568.0,518.5,497.2,446.9,350.6,281.9,216.8,127.9,67.2,0.0],[673.9,610.1,547.1,523.8,463.7,379.9,295.3,233.0,124.4,106.6,0.0]],"disc_m_der":[[0.0,0.0,70.0,70.0,70.0,70.0,70.0,70.0,70.0,70.0,70.0],[0.0,42.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,125.1,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0,150.0],[0.0,108.0,108.0,108.0,108.0,108.0,108.0,108.0,108.0,108.0,108.0]],"chg_m_der":[[5.0,5.0,5.0,5.0,5.0,5.0,4.9,2.1,2.1,0.0,0.0],[15.0,15.0,15.0,15.0,15.0,15.0,15.0,15.0,6.3,6.3,0.0],[46.0,46.0,46.0,46.0,46.0,46.0,46.0,46.0,38.5,19.3,0.0],[86.0,86.0,86.0,86.0,86.0,86.0,86.0,86.0,85.5,36.1,0.0],[100.0,100.0,100.0,100.0,100.0,100.0,100.0,100.0,99.9,65.6,0.0],[86.0,86.0,86.0,86.0,86.0,86.0,86.0,86.0,85.9,84.1,0.0]]};
function isDark() {
return document.documentElement.classList.contains("theme-dark");
}
function palette() {
return isDark()
? { paper: "#282828", plot: "#282828", font: "#ebdbb2",
grid: "#504945", bg: "#3c3836", legendBg: "#3c3836", legendBorder: "#504945" }
: { paper: "#fbf1c7", plot: "#fbf1c7", font: "#3c3836",
grid: "#d5c4a1", bg: "#ebdbb2", legendBg: "#ebdbb2", legendBorder: "#a89984" };
}
function buildFigure(x, y, z, name, color, unit) {
return {
type: "surface", name: name, x: x, y: y, z: z,
colorscale: [[0, color], [1, color]], opacity: 0.85,
showscale: false, hoverinfo: "none",
showlegend: true, // surfaces hide their legend by default
contours: { z: { show: true, usecolormap: false, color: "#ebdbb2", width: 1 } }
};
}
function clipPlane(x, y, zval, color, label) {
var zz = [];
for (var i = 0; i < y.length; i++) {
var row = [];
for (var j = 0; j < x.length; j++) row.push(zval);
zz.push(row);
}
return { type: "surface", name: label, x: x, y: y, z: zz,
colorscale: [[0, color], [1, color]], opacity: 0.30,
showscale: false, hoverinfo: "none",
showlegend: true };
}
function xyz(data, rows, soc) {
var X = [], Y = [], Z = [];
for (var ti = 0; ti < data.temps.length; ti++)
for (var si = 0; si < soc.length; si++) {
X.push(soc[si]); Y.push(data.temps[ti]); Z.push(rows[ti][si]);
}
return { x: X, y: Y, z: Z };
}
function xyz10(data, rows, soc) {
var X = [], Y = [], Z = [];
for (var ti = 0; ti < data.temps10.length; ti++)
for (var si = 0; si < soc.length; si++) {
X.push(soc[si]); Y.push(data.temps10[ti]); Z.push(rows[ti][si]);
}
return { x: X, y: Y, z: Z };
}
function surfaceFrom(xyz) {
var xs = [], ys = [], zgrid = [], i;
xyz.x.forEach(function (v) { if (xs.indexOf(v) < 0) xs.push(v); });
xyz.y.forEach(function (v) { if (ys.indexOf(v) < 0) ys.push(v); });
xs.sort(function (a, b) { return a - b; });
ys.sort(function (a, b) { return a - b; });
for (var yi = 0; yi < ys.length; yi++) {
var row = [];
for (var xi = 0; xi < xs.length; xi++) {
var val = null;
for (i = 0; i < xyz.x.length; i++)
if (xyz.x[i] === xs[xi] && xyz.y[i] === ys[yi]) { val = xyz.z[i]; break; }
row.push(val);
}
zgrid.push(row);
}
return { x: xs, y: ys, z: zgrid };
}
function zAt(surf, x, y) {
var xi = surf.x.indexOf(x);
var yi = surf.y.indexOf(y);
if (xi < 0 || yi < 0) return null;
return surf.z[yi][xi];
}
function layout(pal) {
return {
paper_bgcolor: pal.paper, plot_bgcolor: pal.plot,
font: { color: pal.font, size: 11 },
legend: {
x: 0, y: 0, xanchor: "left", yanchor: "bottom",
bgcolor: pal.legendBg, bordercolor: pal.legendBorder, borderwidth: 1,
font: { color: pal.font, size: 11 },
orientation: "v"
},
scene: {
xaxis: { title: "SoC [%]", gridcolor: pal.grid, backgroundcolor: pal.bg },
yaxis: { title: "Temp [°C]", gridcolor: pal.grid, backgroundcolor: pal.bg },
zaxis: { title: "", gridcolor: pal.grid, backgroundcolor: pal.bg },
camera: { eye: { x: 1.4, y: -1.4, z: 0.9 } }
},
margin: { l: 0, r: 0, t: 40, b: 0 }
};
}
var charts = []; // { el, hoverTraces }
function render(containerId, traces, title, ztitle, height, hoverTraces) {
var el = document.getElementById(containerId);
if (!el) return;
var l = layout(palette());
l.title = { text: title, font: { color: palette().font }, x: 0.5 };
l.scene.zaxis.title = ztitle;
if (height) l.height = height;
Plotly.newPlot(el, traces, l, { displaylogo: false, responsive: true });
charts.push({ el: el, hoverTraces: hoverTraces });
attachHover(el, hoverTraces);
}
function applyTheme() {
charts.forEach(function (c) {
var pal = palette();
Plotly.relayout(c.el, {
"paper_bgcolor": pal.paper,
"plot_bgcolor": pal.plot,
"font.color": pal.font,
"legend.bgcolor": pal.legendBg,
"legend.bordercolor": pal.legendBorder,
"legend.font.color": pal.font,
"scene.xaxis.gridcolor": pal.grid,
"scene.xaxis.backgroundcolor": pal.bg,
"scene.yaxis.gridcolor": pal.grid,
"scene.yaxis.backgroundcolor": pal.bg,
"scene.zaxis.gridcolor": pal.grid,
"scene.zaxis.backgroundcolor": pal.bg
});
});
}
// Plotly traces are created in exactly the order of hoverTraces, so trace
// index i corresponds to hoverTraces[i] — update the legend name every hover.
function updateLegend(el, hoverTraces, x, y) {
if (!el.data) return;
var names = [], indices = [];
for (var i = 0; i < hoverTraces.length; i++) {
var z = zAt(hoverTraces[i].surf, x, y);
if (z === null) continue;
names.push(hoverTraces[i].name + ": " + z.toFixed(2) + " " + hoverTraces[i].unit);
indices.push(i);
}
if (names.length) Plotly.restyle(el, { name: names }, indices);
}
function attachHover(el, hoverTraces) {
if (!el || !hoverTraces || !hoverTraces.length) return;
el.style.position = "relative";
var tip = document.createElement("div");
tip.style.cssText =
"position:absolute;pointer-events:none;display:none;z-index:1000;" +
"background:#3c3836;color:#ebdbb2;border:1px solid #504945;border-radius:4px;" +
"padding:6px 9px;font-size:12px;line-height:1.5;white-space:nowrap;" +
"box-shadow:0 2px 8px rgba(0,0,0,.5);";
el.appendChild(tip);
function moveTip(evt) {
if (!evt) return;
var mx = evt.clientX, my = evt.clientY;
if ((mx === undefined || my === undefined) && evt.points && evt.points[0] && evt.points[0].event) {
mx = evt.points[0].event.clientX; my = evt.points[0].event.clientY;
}
if (mx === undefined || my === undefined) return;
var rect = el.getBoundingClientRect();
tip.style.left = (mx - rect.left + 14) + "px";
tip.style.top = (my - rect.top + 14) + "px";
}
el.on("plotly_hover", function (evt) {
if (!evt.points || !evt.points.length) return;
var pt = evt.points[0];
var x = pt.x, y = pt.y;
if ((x === undefined || y === undefined) && pt.pointNumber !== undefined) {
var t0 = hoverTraces[0].surf;
var n = pt.pointNumber;
var cols = t0.x.length;
var xi = n % cols, yi = Math.floor(n / cols);
if (xi < t0.x.length && yi < t0.y.length) { x = t0.x[xi]; y = t0.y[yi]; }
}
if (x === undefined || y === undefined) return;
var lines = ["<b>SoC</b>: " + x + " %", "<b>Temp</b>: " + y + " °C"];
for (var i = 0; i < hoverTraces.length; i++) {
var z = zAt(hoverTraces[i].surf, x, y);
if (z !== null) lines.push("<b>" + hoverTraces[i].name + "</b>: " + z.toFixed(2) + " " + hoverTraces[i].unit);
}
tip.innerHTML = lines.join("<br>");
tip.style.display = "block";
moveTip(evt);
updateLegend(el, hoverTraces, x, y);
});
el.on("plotly_unhover", function () { tip.style.display = "none"; });
}
function replaceSvg(title, containerId) {
var found = null;
document.querySelectorAll("figure.image img").forEach(function (img) {
if (img.src && img.src.indexOf(title) !== -1) found = img;
});
if (!found) return false;
var fig = found.closest("figure");
var div = document.createElement("div");
div.id = containerId;
div.className = "ignis-battery-chart";
fig.parentNode.replaceChild(div, fig);
return true;
}
function hasAnyChart() {
return document.querySelector('figure.image img[src*="ocv_3d"], figure.image img[src*="plut_discharge_3d"], figure.image img[src*="plut_charge_3d"], figure.image img[src*="plut_discharge_derated_3d"], figure.image img[src*="plut_charge_derated_3d"]');
}
function loadPlotly(cb) {
if (typeof Plotly !== "undefined") { cb(); return; }
var s = document.createElement("script");
s.src = PLOTLY_SRC;
s.onload = cb;
s.onerror = function () { console.error("Plotly load failed"); };
document.head.appendChild(s);
}
function build() {
if (replaceSvg("ocv_3d", "plotly-ocv")) {
var p = surfaceFrom(xyz(D, D.ocv_p, D.soc));
var m = surfaceFrom(xyz(D, D.ocv_m, D.soc));
render("plotly-ocv",
[ Object.assign(buildFigure(p.x, p.y, p.z, "PHEV1", "#3c6a75", "V"),
{ colorscale: [[0, "#3c6a75"], [1, "#83a598"]] }),
Object.assign(buildFigure(m.x, m.y, m.z, "M50L", "#b4530e", "V"),
{ colorscale: [[0, "#b4530e"], [1, "#fe8019"]] }) ],
"OCV vs SoC and temperature (per cell)", "OCV [V]", 500,
[ { name: "PHEV1", surf: p, unit: "V" },
{ name: "M50L", surf: m, unit: "V" } ]);
}
if (replaceSvg("plut_discharge_3d", "plotly-discharge")) {
var p = surfaceFrom(xyz10(D, D.disc_p, D.soc10));
var m = surfaceFrom(xyz10(D, D.disc_m, D.soc10));
render("plotly-discharge",
[ Object.assign(buildFigure(p.x, p.y, p.z, "PHEV1 6S2P", "#3c6a75", "A"),
{ colorscale: [[0, "#3c6a75"], [1, "#83a598"]] }),
Object.assign(buildFigure(m.x, m.y, m.z, "M50L 6S21P (30 s)", "#b4530e", "A"),
{ colorscale: [[0, "#b4530e"], [1, "#fe8019"]] }),
clipPlane(p.x, p.y, 125, "#fabd2f", "Fuse 125 A"),
clipPlane(p.x, p.y, 150, "#fb4934", "BMS clip 150 A") ],
"Max discharge current — PHEV1 vs M50L (30 s), with fuse/BMS limits",
"Max discharge [A]", 520,
[ { name: "PHEV1 6S2P", surf: p, unit: "A" },
{ name: "M50L 6S21P (30 s)", surf: m, unit: "A" },
{ name: "Fuse 125 A", surf: clipPlaneSurf(p, 125), unit: "A" },
{ name: "BMS clip 150 A", surf: clipPlaneSurf(p, 150), unit: "A" } ]);
}
if (replaceSvg("plut_charge_3d", "plotly-charge")) {
var p = surfaceFrom(xyz10(D, D.chg_p, D.soc10));
var m = surfaceFrom(xyz10(D, D.chg_m, D.soc10));
render("plotly-charge",
[ Object.assign(buildFigure(p.x, p.y, p.z, "PHEV1 6S2P", "#3c6a75", "A"),
{ colorscale: [[0, "#3c6a75"], [1, "#83a598"]] }),
Object.assign(buildFigure(m.x, m.y, m.z, "M50L 6S21P (30 s)", "#b4530e", "A"),
{ colorscale: [[0, "#b4530e"], [1, "#fe8019"]] }),
clipPlane(p.x, p.y, 100, "#b8bb26", "BMS charge 100 A") ],
"Max charge current — PHEV1 vs M50L (30 s), with BMS 100 A",
"Max charge [A]", 520,
[ { name: "PHEV1 6S2P", surf: p, unit: "A" },
{ name: "M50L 6S21P (30 s)", surf: m, unit: "A" },
{ name: "BMS charge 100 A", surf: clipPlaneSurf(p, 100), unit: "A" } ]);
}
if (replaceSvg("plut_discharge_derated_3d", "plotly-discharge-derated")) {
var p = surfaceFrom(xyz10(D, D.disc_p, D.soc10));
var m = surfaceFrom(xyz10(D, D.disc_m_der, D.soc10));
render("plotly-discharge-derated",
[ Object.assign(buildFigure(p.x, p.y, p.z, "PHEV1 6S2P", "#3c6a75", "A"),
{ colorscale: [[0, "#3c6a75"], [1, "#83a598"]] }),
Object.assign(buildFigure(m.x, m.y, m.z, "M50L 6S21P (derated)", "#b4530e", "A"),
{ colorscale: [[0, "#b4530e"], [1, "#fe8019"]] }),
clipPlane(p.x, p.y, 125, "#fabd2f", "Fuse 125 A"),
clipPlane(p.x, p.y, 150, "#fb4934", "BMS clip 150 A") ],
"Max discharge — M50L soft-clipped to BMS limits (150 A) + temp derating",
"Max discharge [A]", 520,
[ { name: "PHEV1 6S2P", surf: p, unit: "A" },
{ name: "M50L 6S21P (derated)", surf: m, unit: "A" },
{ name: "Fuse 125 A", surf: clipPlaneSurf(p, 125), unit: "A" },
{ name: "BMS clip 150 A", surf: clipPlaneSurf(p, 150), unit: "A" } ]);
}
if (replaceSvg("plut_charge_derated_3d", "plotly-charge-derated")) {
var p = surfaceFrom(xyz10(D, D.chg_p, D.soc10));
var m = surfaceFrom(xyz10(D, D.chg_m_der, D.soc10));
render("plotly-charge-derated",
[ Object.assign(buildFigure(p.x, p.y, p.z, "PHEV1 6S2P", "#3c6a75", "A"),
{ colorscale: [[0, "#3c6a75"], [1, "#83a598"]] }),
Object.assign(buildFigure(m.x, m.y, m.z, "M50L 6S21P (derated)", "#b4530e", "A"),
{ colorscale: [[0, "#b4530e"], [1, "#fe8019"]] }),
clipPlane(p.x, p.y, 100, "#b8bb26", "BMS charge 100 A") ],
"Max charge — M50L soft-clipped to BMS limits (100 A) + temp derating",
"Max charge [A]", 520,
[ { name: "PHEV1 6S2P", surf: p, unit: "A" },
{ name: "M50L 6S21P (derated)", surf: m, unit: "A" },
{ name: "BMS charge 100 A", surf: clipPlaneSurf(p, 100), unit: "A" } ]);
}
// Theme changes: recolour all charts.
var observer = new MutationObserver(function () { applyTheme(); });
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
window.IGNIS_CHARTS_DONE = true;
}
function clipPlaneSurf(base, zval) {
var zz = [];
for (var i = 0; i < base.y.length; i++) {
var row = [];
for (var j = 0; j < base.x.length; j++) row.push(zval);
zz.push(row);
}
return { x: base.x, y: base.y, z: zz };
}
function ready(fn) {
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", fn);
else fn();
}
ready(function () {
if (!hasAnyChart()) return; // lazy: nothing to do on other pages
loadPlotly(build); // lazy: load Plotly only now
});
})();
</script>