@@ -235,23 +235,67 @@ class AirQualityCard extends HTMLElement {
235235 return value . toFixed ( 1 ) ;
236236 }
237237
238- _updateMinMaxDisplay ( graphId , data , unit ) {
238+ // Anchor min/max value labels to the actual data points on the line.
239+ // The position percentages are computed against the SVG's 300×50 viewBox;
240+ // because preserveAspectRatio="none" stretches the SVG to fill the wrapper,
241+ // the same percentage maps cleanly to the wrapper's dimensions.
242+ _updateMinMaxDisplay ( graphId , data , colorFn ) {
239243 const minMax = this . _getMinMax ( data ) ;
240- if ( ! minMax ) return ;
241- const container = this . shadowRoot . getElementById ( `${ graphId } -graph-container` ) ;
242- if ( ! container ) return ;
243- let minmaxEl = this . shadowRoot . getElementById ( `${ graphId } -minmax` ) ;
244- if ( ! minmaxEl ) {
245- const header = container . querySelector ( '.graph-header' ) ;
246- if ( ! header ) return ;
247- minmaxEl = document . createElement ( 'div' ) ;
248- minmaxEl . className = 'graph-minmax' ;
249- minmaxEl . id = `${ graphId } -minmax` ;
250- header . insertAdjacentElement ( 'afterend' , minmaxEl ) ;
251- }
252- const minStr = this . _formatGraphValue ( minMax . min , unit ) ;
253- const maxStr = this . _formatGraphValue ( minMax . max , unit ) ;
254- minmaxEl . innerHTML = `<span class="arrow">↓</span>${ minStr } · <span class="arrow">↑</span>${ maxStr } ${ unit } ` ;
244+ if ( ! minMax || minMax . min === minMax . max ) {
245+ this . _clearMinMaxMarkers ( graphId ) ;
246+ return ;
247+ }
248+ let minIdx = 0 , maxIdx = 0 ;
249+ for ( let i = 1 ; i < data . length ; i ++ ) {
250+ if ( data [ i ] . value < data [ minIdx ] . value ) minIdx = i ;
251+ if ( data [ i ] . value > data [ maxIdx ] . value ) maxIdx = i ;
252+ }
253+ const points = this . _graphData [ graphId ] && this . _graphData [ graphId ] . points ;
254+ if ( ! points || ! points . length ) return ;
255+ const wrapper = this . shadowRoot . getElementById ( `${ graphId } -graph` ) ;
256+ if ( ! wrapper ) return ;
257+
258+ this . _renderMinMaxMarker ( graphId , 'max' , points [ maxIdx ] , colorFn ( minMax . max ) , this . _formatGraphValue ( minMax . max , this . _graphData [ graphId ] . unit ) ) ;
259+ this . _renderMinMaxMarker ( graphId , 'min' , points [ minIdx ] , colorFn ( minMax . min ) , this . _formatGraphValue ( minMax . min , this . _graphData [ graphId ] . unit ) ) ;
260+ }
261+
262+ _renderMinMaxMarker ( graphId , kind , point , color , valueStr ) {
263+ if ( ! point ) return ;
264+ const wrapper = this . shadowRoot . getElementById ( `${ graphId } -graph` ) ;
265+ if ( ! wrapper ) return ;
266+ const id = `${ graphId } -minmax-${ kind } ` ;
267+ let marker = this . shadowRoot . getElementById ( id ) ;
268+ if ( ! marker ) {
269+ marker = document . createElement ( 'div' ) ;
270+ marker . id = id ;
271+ marker . className = `minmax-marker minmax-marker--${ kind } ` ;
272+ wrapper . appendChild ( marker ) ;
273+ }
274+ // Y position: point.y is in the 0..50 SVG coordinate system; convert to %
275+ const leftPct = ( point . x / 300 ) * 100 ;
276+ const topPct = ( point . y / 50 ) * 100 ;
277+ // Flip the label across the chart vertical midline so it can't get
278+ // clipped by the chart's top/bottom edge: anchor the label on the
279+ // opposite side of the data point from where it sits.
280+ const placeBelow = point . y < 25 ;
281+ // Same idea for horizontal: when very close to an edge, anchor the
282+ // label to that edge instead of centering on the point.
283+ let anchor = 'center' ;
284+ if ( leftPct < 12 ) anchor = 'left' ;
285+ else if ( leftPct > 88 ) anchor = 'right' ;
286+ marker . style . left = `${ leftPct } %` ;
287+ marker . style . top = `${ topPct } %` ;
288+ marker . style . color = color ;
289+ marker . dataset . place = placeBelow ? 'below' : 'above' ;
290+ marker . dataset . anchor = anchor ;
291+ marker . textContent = valueStr ;
292+ }
293+
294+ _clearMinMaxMarkers ( graphId ) {
295+ [ 'min' , 'max' ] . forEach ( kind => {
296+ const el = this . shadowRoot . getElementById ( `${ graphId } -minmax-${ kind } ` ) ;
297+ if ( el ) el . remove ( ) ;
298+ } ) ;
255299 }
256300
257301 _isCompact ( ) {
@@ -1068,19 +1112,45 @@ class AirQualityCard extends HTMLElement {
10681112 border-radius: 3px;
10691113 }
10701114
1071- .graph-minmax {
1072- font-size: 0.7em;
1073- color: var(--secondary-text-color);
1074- opacity: 0.7;
1075- text-align: right;
1076- margin-top: -4px;
1077- margin-bottom: 4px;
1078- letter-spacing: 0.3px;
1115+ /* Min/max value labels overlaid on the graph at the actual data points
1116+ where the extremes occurred. The text-shadow halo lets them remain
1117+ legible regardless of where they land on the gradient fill. */
1118+ .minmax-marker {
1119+ position: absolute;
1120+ pointer-events: none;
1121+ font-size: 10px;
1122+ font-weight: 600;
1123+ letter-spacing: 0.2px;
1124+ font-variant-numeric: tabular-nums;
1125+ line-height: 1;
1126+ white-space: nowrap;
1127+ transform: translate(-50%, -100%);
1128+ margin-top: -6px;
1129+ text-shadow:
1130+ 0 0 3px var(--ha-card-background, var(--card-background-color, #fff)),
1131+ 0 0 6px var(--ha-card-background, var(--card-background-color, #fff)),
1132+ 0 1px 2px var(--ha-card-background, var(--card-background-color, #fff));
1133+ opacity: 0.95;
10791134 }
10801135
1081- .graph-minmax .arrow {
1082- font-weight: 600;
1083- margin: 0 1px;
1136+ .minmax-marker[data-place="below"] {
1137+ transform: translate(-50%, 0);
1138+ margin-top: 6px;
1139+ }
1140+
1141+ /* When near a chart edge, anchor the label's side to the point rather
1142+ than centering on it — keeps text inside the chart. */
1143+ .minmax-marker[data-anchor="left"] {
1144+ transform: translate(0, -100%);
1145+ }
1146+ .minmax-marker[data-anchor="left"][data-place="below"] {
1147+ transform: translate(0, 0);
1148+ }
1149+ .minmax-marker[data-anchor="right"] {
1150+ transform: translate(-100%, -100%);
1151+ }
1152+ .minmax-marker[data-anchor="right"][data-place="below"] {
1153+ transform: translate(-100%, 0);
10841154 }
10851155
10861156 .graph-wrapper {
@@ -1897,10 +1967,6 @@ class AirQualityCard extends HTMLElement {
18971967 const timeAxis = this . shadowRoot . getElementById ( `${ graphId } -time-axis` ) ;
18981968 if ( ! svg || ! data . length ) return ;
18991969
1900- if ( this . _config . show_min_max ) {
1901- this . _updateMinMaxDisplay ( graphId , data , unit ) ;
1902- }
1903-
19041970 const width = 300 ;
19051971 const height = 50 ;
19061972 const padding = 2 ;
@@ -1932,6 +1998,12 @@ class AirQualityCard extends HTMLElement {
19321998
19331999 this . _graphData [ graphId ] = { points, outdoorPoints, unit, colorFn, outdoorLabel : outdoorLabel || 'Outdoor' } ;
19342000
2001+ if ( this . _config . show_min_max ) {
2002+ this . _updateMinMaxDisplay ( graphId , data , colorFn ) ;
2003+ } else {
2004+ this . _clearMinMaxMarkers ( graphId ) ;
2005+ }
2006+
19352007 if ( points . length < 2 ) return ;
19362008
19372009 const ts = Date . now ( ) ;
@@ -2371,7 +2443,21 @@ if (LitElement && !customElements.get('air-quality-card-editor')) {
23712443 { name : 'air_quality_entity' , selector : { entity : { domain : 'sensor' } } } ,
23722444 { name : 'hours_to_show' , selector : { number : { min : 1 , max : 168 , mode : 'box' , unit_of_measurement : 'hours' } } } ,
23732445 { name : 'show_min_max' , selector : { boolean : { } } } ,
2374- { name : 'order' , selector : { object : { } } } ,
2446+ { name : 'order' , selector : { select : { multiple : true , mode : 'list' , options : [
2447+ { value : 'co' , label : 'CO' } ,
2448+ { value : 'radon' , label : 'Radon' } ,
2449+ { value : 'co2' , label : 'CO₂' } ,
2450+ { value : 'pm25' , label : 'PM2.5' } ,
2451+ { value : 'pm10' , label : 'PM10' } ,
2452+ { value : 'pm1' , label : 'PM1' } ,
2453+ { value : 'pm03' , label : 'PM0.3' } ,
2454+ { value : 'pm4' , label : 'PM4' } ,
2455+ { value : 'hcho' , label : 'HCHO' } ,
2456+ { value : 'tvoc' , label : 'tVOC' } ,
2457+ { value : 'nox' , label : 'NOx' } ,
2458+ { value : 'humidity' , label : 'Humidity' } ,
2459+ { value : 'temperature' , label : 'Temperature' }
2460+ ] } } } ,
23752461 { name : 'display' , selector : { select : { options : [ { value : 'full' , label : 'Full (graphs and details)' } , { value : 'compact' , label : 'Compact (status badge only)' } ] , mode : 'dropdown' } } } ,
23762462 { name : 'tap_action' , selector : { ui_action : { } } } ,
23772463 { name : 'hold_action' , selector : { ui_action : { } } } ,
0 commit comments