@@ -92,7 +92,7 @@ export interface StatCardSlots {
9292 value(props : { value? : string | number , ui: StatCard [' ui' ] }): any
9393 label(props : { current: number , max: number , ui: StatCard [' ui' ] }): any
9494 progress(props : { current: number , max: number , percent: number , ui: StatCard [' ui' ] }): any
95- sparkline(props : { path: string , areaPath: string , viewBox: string , ui: StatCard [' ui' ] }): any
95+ sparkline(props : { path: string , areaPath: string , viewBox: string , zeroLine : string | null , ui: StatCard [' ui' ] }): any
9696 trend(props : { trend? : number , trendDirection? : ' up' | ' down' , ui: StatCard [' ui' ] }): any
9797 default(props : { ui: StatCard [' ui' ] }): any
9898}
@@ -165,7 +165,7 @@ const progressColor = computed(() => props.progressColor || props.color)
165165
166166// Sparkline calculations
167167const sparklineData = computed (() => {
168- if (! props .data || props .data .length === 0 ) return { path: ' ' , areaPath: ' ' , viewBox: ' 0 0 100 40' }
168+ if (! props .data || props .data .length === 0 ) return { path: ' ' , areaPath: ' ' , viewBox: ' 0 0 100 40' , zeroLine: null }
169169
170170 const values = props .data
171171 const min = Math .min (... values )
@@ -176,6 +176,24 @@ const sparklineData = computed(() => {
176176 const height = props .height || 40
177177 const padding = 2
178178
179+ // Check if we need to show zero line (when there are negative values)
180+ const hasNegativeValues = min < 0
181+ let zeroLineY: number | null = null
182+
183+ if (hasNegativeValues && max > 0 && range > 0 ) {
184+ // Calculate where zero should be positioned
185+ // Normalize zero value: (0 - min) / range gives us where zero is in the range [0, 1]
186+ const normalizedZero = (0 - min ) / range
187+ // Convert to Y coordinate: invert because SVG Y increases downward
188+ // We want zero at the bottom when it's the minimum, at the top when it's the maximum
189+ const availableHeight = height - padding * 2
190+ zeroLineY = height - padding - (normalizedZero * availableHeight )
191+
192+ // Ensure zeroLineY is within bounds
193+ if (zeroLineY < padding ) zeroLineY = padding
194+ if (zeroLineY > height - padding ) zeroLineY = height - padding
195+ }
196+
179197 const points: string [] = []
180198
181199 values .forEach ((value , index ) => {
@@ -188,24 +206,24 @@ const sparklineData = computed(() => {
188206 points .push (` ${x },${y } ` )
189207 })
190208
191- if (points .length === 0 ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` }
209+ if (points .length === 0 ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` , zeroLine: null }
192210
193211 const path = ` M ${points .join (' L ' )} `
194212 const firstPoint = points [0 ]
195213 const lastPoint = points [points .length - 1 ]
196214
197- if (! firstPoint || ! lastPoint ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` }
215+ if (! firstPoint || ! lastPoint ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` , zeroLine: null }
198216
199217 const firstXStr = firstPoint .split (' ,' )[0 ]
200218 const lastXStr = lastPoint .split (' ,' )[0 ]
201219
202- if (! firstXStr || ! lastXStr ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` }
220+ if (! firstXStr || ! lastXStr ) return { path: ' ' , areaPath: ' ' , viewBox: ` 0 0 ${width } ${height } ` , zeroLine: null }
203221
204222 const firstX = Number .parseFloat (firstXStr )
205223 const areaPath = ` M ${firstPoint } L ${points .join (' L ' )} L ${lastXStr },${height } L ${firstX },${height } Z `
206224 const viewBox = ` 0 0 ${width } ${height } `
207225
208- return { path , areaPath , viewBox }
226+ return { path , areaPath , viewBox , zeroLine: zeroLineY !== null ? ` M ${ padding },${ zeroLineY } L ${ width - padding },${ zeroLineY } ` : null }
209227})
210228 </script >
211229
@@ -260,14 +278,30 @@ const sparklineData = computed(() => {
260278 </div >
261279
262280 <div v-if =" !!slots.sparkline || (data && data.length > 0)" data-slot =" sparkline" :class =" (ui as any).sparkline({ class: (props.ui as any)?.sparkline })" >
263- <slot name =" sparkline" :path =" sparklineData.path" :area-path =" sparklineData.areaPath" :view-box =" sparklineData.viewBox" :ui =" ui" >
281+ <slot
282+ name =" sparkline"
283+ :path =" sparklineData.path"
284+ :area-path =" sparklineData.areaPath"
285+ :view-box =" sparklineData.viewBox"
286+ :zero-line =" sparklineData.zeroLine"
287+ :ui =" ui"
288+ >
264289 <svg
265290 data-slot =" sparklineSvg"
266291 :class =" (ui as any).sparklineSvg({ class: (props.ui as any)?.sparklineSvg })"
267292 :viewBox =" sparklineData.viewBox"
268293 preserveAspectRatio =" none"
269294 :style =" { height: `${props.height || 40}px` }"
270295 >
296+ <path
297+ v-if =" sparklineData.zeroLine"
298+ data-slot =" sparklineZeroLine"
299+ :d =" sparklineData.zeroLine"
300+ :class =" (ui as any).sparklineZeroLine?.({ class: (props.ui as any)?.sparklineZeroLine }) || 'stroke-default dark:stroke-default'"
301+ fill =" none"
302+ stroke-width =" 1"
303+ stroke-dasharray =" 3 3"
304+ />
271305 <path
272306 v-if =" showArea"
273307 data-slot =" sparklineArea"
0 commit comments