@@ -90,7 +90,7 @@ const CARD = "jkbms-battery-console-card";
9090import { cardTip } from "./card-tip.js?v=1.0.0" ;
9191import { ramp , rampCss } from "./card-ramp.js?v=1.0.0" ;
9292
93- const VERSION = "1.6.1 " ;
93+ const VERSION = "1.6.3 " ;
9494
9595/*
9696 * The design's palette, literal. Names are what the design calls them: two
@@ -324,7 +324,7 @@ const HELP = {
324324 + "Charging a LiFePO4 pack below 0 °C damages it." ,
325325 range : "Sets the chart's window, and brings a dragged or zoomed chart back to live.\n"
326326 + "E.g. 24h shows the night charge as a climb from 23:00 to 07:00.\n"
327- + "Windows starting over 36 h back are drawn from hourly statistics ." ,
327+ + "Past 36 h: hourly means, each hour's min–max shaded around them ." ,
328328 chart : "The series on the chart below; tap the name to open its entity.\n"
329329 + "E.g. drag the chart back to 23:00, scroll or pinch to zoom, double-click for live." ,
330330 s_soc : "Charts the BMS state of charge, in its band colour.\n"
@@ -2378,7 +2378,7 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
23782378 end_time : new Date ( end ) . toISOString ( ) ,
23792379 statistic_ids : [ entity ] ,
23802380 period : "hour" ,
2381- types : [ "mean" ] ,
2381+ types : [ "mean" , "min" , "max" ] ,
23822382 } ) ;
23832383 raw = this . _parseStats ( stats , entity , spec ) ;
23842384 fromStats = raw . length > 0 ;
@@ -2435,6 +2435,12 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
24352435 * Hourly means from recorder/statistics_during_period, in the [ms, value]
24362436 * shape _parse() produces. Plotted at the start of each hour, as the History
24372437 * panel does; `start` is epoch ms on current releases, ISO on older ones.
2438+ *
2439+ * Each row also carries the hour's own min and max as [ms, mean, min, max].
2440+ * Only the mean is drawn, but Min/Max in the stats row read these: the
2441+ * highest hourly MEAN of a week of house load is a few hundred watts, while
2442+ * the kettle that ran inside that hour drew 1.8 kW. Reporting the former as
2443+ * "Max" made every 7d/14d window look calmer than any 24h one inside it.
24382444 */
24392445 _parseStats ( reply , entity , spec ) {
24402446 const rows = reply && reply [ entity ] ;
@@ -2445,7 +2451,15 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
24452451 const v = parseFloat ( r . mean ) ;
24462452 if ( ! Number . isFinite ( v ) ) continue ;
24472453 const t = typeof r . start === "number" ? r . start : Date . parse ( r . start ) ;
2448- if ( Number . isFinite ( t ) ) out . push ( [ t , this . _scale ( spec , v ) ] ) ;
2454+ if ( ! Number . isFinite ( t ) ) continue ;
2455+ const row = [ t , this . _scale ( spec , v ) ] ;
2456+ const lo = parseFloat ( r . min ) , hi = parseFloat ( r . max ) ;
2457+ if ( Number . isFinite ( lo ) && Number . isFinite ( hi ) ) {
2458+ // A negative multiplier swaps which end is which.
2459+ const a = this . _scale ( spec , lo ) , b = this . _scale ( spec , hi ) ;
2460+ row . push ( Math . min ( a , b ) , Math . max ( a , b ) ) ;
2461+ }
2462+ out . push ( row ) ;
24492463 }
24502464 out . sort ( ( a , b ) => a [ 0 ] - b [ 0 ] ) ;
24512465 return out ;
@@ -2509,6 +2523,29 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
25092523 return span > 0 ? acc / span : pts [ pts . length - 1 ] [ 1 ] ;
25102524 }
25112525
2526+ /**
2527+ * The window's true Min and Max, from the rows that cover it. A raw row
2528+ * holds its value until the next one, so the row just before the window
2529+ * counts and the one just after does not. An hourly statistics row counts
2530+ * when its hour overlaps the window, with the hour's own min and max.
2531+ */
2532+ _extremes ( pts , p , t0 , t1 ) {
2533+ let min = Infinity , max = - Infinity ;
2534+ pts . forEach ( ( q , i ) => {
2535+ if ( q . length >= 4 ) {
2536+ if ( q [ 0 ] + 3600000 <= t0 || q [ 0 ] >= t1 ) return ;
2537+ if ( q [ 2 ] < min ) min = q [ 2 ] ;
2538+ if ( q [ 3 ] > max ) max = q [ 3 ] ;
2539+ return ;
2540+ }
2541+ const next = pts [ i + 1 ] ;
2542+ if ( q [ 0 ] > t1 || ( next && next [ 0 ] <= t0 ) ) return ;
2543+ if ( q [ 1 ] < min ) min = q [ 1 ] ;
2544+ if ( q [ 1 ] > max ) max = q [ 1 ] ;
2545+ } ) ;
2546+ return min <= max ? { min, max } : { min : p . min , max : p . max } ;
2547+ }
2548+
25122549 /**
25132550 * Recorder samples, thinned to what the plot can resolve -- KEEPING EVERY
25142551 * EXTREME.
@@ -2579,8 +2616,13 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
25792616 * reading against the time axis under it.
25802617 */
25812618 _path ( pts , w , h , pad , t0 , t1 ) {
2619+ // An hourly statistics row is [t, mean, min, max]; its band is part of
2620+ // what is drawn, so it is part of what the axis has to hold.
2621+ const ranged = pts . some ( ( q ) => q . length >= 4 ) ;
2622+ const bot = ( q ) => ( q . length >= 4 ? q [ 2 ] : q [ 1 ] ) ;
2623+ const top = ( q ) => ( q . length >= 4 ? q [ 3 ] : q [ 1 ] ) ;
25822624 let min = Infinity , max = - Infinity ;
2583- for ( const q of pts ) { if ( q [ 1 ] < min ) min = q [ 1 ] ; if ( q [ 1 ] > max ) max = q [ 1 ] ; }
2625+ for ( const q of pts ) { if ( bot ( q ) < min ) min = bot ( q ) ; if ( top ( q ) > max ) max = top ( q ) ; }
25842626 let lo = min , hi = max ;
25852627
25862628 /*
@@ -2595,7 +2637,12 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
25952637 * Max, the window label counts what went off-scale, and hovering the spike
25962638 * gives its exact recorded value.
25972639 */
2598- const sorted = pts . map ( ( q ) => q [ 1 ] ) . sort ( ( a , b ) => a - b ) ;
2640+ const vals = [ ] ;
2641+ for ( const q of pts ) {
2642+ vals . push ( q [ 1 ] ) ;
2643+ if ( q . length >= 4 ) vals . push ( q [ 2 ] , q [ 3 ] ) ;
2644+ }
2645+ const sorted = vals . sort ( ( a , b ) => a - b ) ;
25992646 const at = ( f ) => sorted [ Math . min ( sorted . length - 1 ,
26002647 Math . max ( 0 , Math . round ( f * ( sorted . length - 1 ) ) ) ) ] ;
26012648 const band = at ( 0.99 ) - at ( 0.01 ) ;
@@ -2609,19 +2656,27 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
26092656 if ( hi - lo < 1e-9 ) { hi += 0.5 ; lo -= 0.5 ; }
26102657 const span = hi - lo ;
26112658 const tspan = ( t1 - t0 ) || 1 ;
2612- const xy = pts . map ( ( [ t , v ] ) => [
2613- Math . max ( 0 , Math . min ( w , ( t - t0 ) / tspan * w ) ) ,
2614- h - pad - ( v - lo ) / span * ( h - pad * 2 ) ,
2615- ] ) ;
2659+ const X = ( t ) => Math . max ( 0 , Math . min ( w , ( t - t0 ) / tspan * w ) ) ;
2660+ const Y = ( v ) => h - pad - ( v - lo ) / span * ( h - pad * 2 ) ;
2661+ const xy = pts . map ( ( [ t , v ] ) => [ X ( t ) , Y ( v ) ] ) ;
26162662 const line = xy . map ( ( q , i ) => ( i ? "L" : "M" ) + q [ 0 ] . toFixed ( 1 ) + " " + q [ 1 ] . toFixed ( 1 ) ) . join ( " " ) ;
26172663 // Close the area under the data it actually has, not the whole frame.
2618- const area = xy . length
2619- ? line + " L" + xy [ xy . length - 1 ] [ 0 ] . toFixed ( 1 ) + " " + h
2620- + " L" + xy [ 0 ] [ 0 ] . toFixed ( 1 ) + " " + h + " Z"
2621- : "" ;
2664+ // Hourly statistics shade each hour's min-max band instead: the line is
2665+ // only the hours' means, and without the band the stats row's Min/Max
2666+ // (the hours' real extremes) would sit far outside anything drawn.
2667+ let area = "" ;
2668+ if ( ranged ) {
2669+ const f = ( q ) => X ( q [ 0 ] ) . toFixed ( 1 ) + " " ;
2670+ area = pts . map ( ( q , i ) => ( i ? "L" : "M" ) + f ( q ) + Y ( top ( q ) ) . toFixed ( 1 ) ) . join ( " " )
2671+ + " " + pts . slice ( ) . reverse ( ) . map ( ( q ) => "L" + f ( q ) + Y ( bot ( q ) ) . toFixed ( 1 ) ) . join ( " " )
2672+ + " Z" ;
2673+ } else if ( xy . length ) {
2674+ area = line + " L" + xy [ xy . length - 1 ] [ 0 ] . toFixed ( 1 ) + " " + h
2675+ + " L" + xy [ 0 ] [ 0 ] . toFixed ( 1 ) + " " + h + " Z" ;
2676+ }
26222677 let off = 0 ;
2623- for ( const q of pts ) if ( q [ 1 ] < lo || q [ 1 ] > hi ) off ++ ;
2624- return { line, area, lo, hi, min, max, off, xy } ;
2678+ for ( const q of pts ) if ( bot ( q ) < lo || top ( q ) > hi ) off ++ ;
2679+ return { line, area, ranged , lo, hi, min, max, off, xy } ;
26252680 }
26262681
26272682 /**
@@ -2769,6 +2824,7 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
27692824 const p = this . _path ( pts , CH_W , CH_H , CH_PAD , d . t0 , d . t1 ) ;
27702825 el . chLine . setAttribute ( "d" , p . line ) ;
27712826 el . chArea . setAttribute ( "d" , p . area ) ;
2827+ el . chArea . setAttribute ( "fill-opacity" , p . ranged ? "0.22" : "0.10" ) ;
27722828 this . _hv = { spec : spec , span : d . t1 - d . t0 , pts : pts , xy : p . xy , col : col } ;
27732829
27742830 const span = p . hi - p . lo ;
@@ -2788,9 +2844,10 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
27882844 const raw = this . _num ( this . _config [ spec . cfg ] ) ;
27892845 const now = raw === null ? null : this . _scale ( spec , raw ) ;
27902846 const mean = this . _mean ( pts ) ;
2847+ const ext = this . _extremes ( pts , p , d . t0 , d . t1 ) ;
27912848 el . stats . innerHTML = [
27922849 [ "Now" , now === null ? pts [ pts . length - 1 ] [ 1 ] : now ] ,
2793- [ "Min" , p . min ] , [ "Max" , p . max ] , [ "Mean" , mean ] ,
2850+ [ "Min" , ext . min ] , [ "Max" , ext . max ] , [ "Mean" , mean ] ,
27942851 ] . map ( ( [ label , v ] ) => {
27952852 const val = v . toFixed ( spec . dec ) + " " + spec . unit ;
27962853 const tip = this . _tip ( label + " — " + val , "stat_" + label . toLowerCase ( ) ) ;
@@ -2856,7 +2913,11 @@ class JkbmsBatteryConsoleCard extends HTMLElement {
28562913 el . hvDot . classList . add ( "on" ) ;
28572914
28582915 el . tip . innerHTML = "<b>" + this . _esc ( hv . pts [ best ] [ 1 ] . toFixed ( hv . spec . dec ) + " " + hv . spec . unit )
2859- + "</b><i>" + this . _esc ( this . _stamp ( hv . pts [ best ] [ 0 ] , hv . span ) ) + "</i>" ;
2916+ + "</b><i>" + this . _esc ( this . _stamp ( hv . pts [ best ] [ 0 ] , hv . span ) ) + "</i>"
2917+ + ( hv . pts [ best ] . length >= 4
2918+ ? "<i>hour " + this . _esc ( hv . pts [ best ] [ 2 ] . toFixed ( hv . spec . dec ) + "–"
2919+ + hv . pts [ best ] [ 3 ] . toFixed ( hv . spec . dec ) + " " + hv . spec . unit ) + "</i>"
2920+ : "" ) ;
28602921 el . tip . style . color = hv . col ;
28612922 // Keep the bubble inside the plot at both edges.
28622923 el . tip . style . left = Math . max ( 52 , Math . min ( rect . width - 52 , px ) ) + "px" ;
0 commit comments