@@ -105,55 +105,79 @@ export function ActivityRing({
105105 } , [ positiveValue , negativeValue ] ) ;
106106
107107 // Calculate arc angles based on ratio
108- const { positiveArc, negativeArc } = useMemo ( ( ) => {
109- const total = positiveValue + negativeValue ;
108+ const { positiveArc, negativeArc, showOnlyPositive, showOnlyNegative } =
109+ useMemo ( ( ) => {
110+ const total = positiveValue + negativeValue ;
110111
111- // Available sweep angle (360 - 2 gaps)
112- const availableSweep = 360 - GAP_ANGLE * 2 ;
112+ // Available sweep angle (360 - 2 gaps)
113+ const availableSweep = 360 - GAP_ANGLE * 2 ;
113114
114- if ( total === 0 ) {
115- // Neutral: equal arcs at 50% each
116- const halfSweep = availableSweep / 2 ;
117- return {
118- // Right side: from top going clockwise to bottom
119- positiveArc : { start : GAP_ANGLE / 2 , end : GAP_ANGLE / 2 + halfSweep } ,
120- // Left side: from bottom going clockwise to top
121- negativeArc : {
122- start : 180 + GAP_ANGLE / 2 ,
123- end : 180 + GAP_ANGLE / 2 + halfSweep ,
124- } ,
125- } ;
126- }
115+ if ( total === 0 ) {
116+ // Neutral: equal arcs at 50% each
117+ const halfSweep = availableSweep / 2 ;
118+ return {
119+ // Right side: from top going clockwise to bottom
120+ positiveArc : { start : GAP_ANGLE / 2 , end : GAP_ANGLE / 2 + halfSweep } ,
121+ // Left side: from bottom going clockwise to top
122+ negativeArc : {
123+ start : 180 + GAP_ANGLE / 2 ,
124+ end : 180 + GAP_ANGLE / 2 + halfSweep ,
125+ } ,
126+ showOnlyPositive : false ,
127+ showOnlyNegative : false ,
128+ } ;
129+ }
127130
128- const positiveRatio = positiveValue / total ;
131+ // When one value is 0, show full circle of the other color (no gaps)
132+ if ( positiveValue === 0 ) {
133+ return {
134+ positiveArc : { start : 0 , end : 0 } ,
135+ negativeArc : { start : 0 , end : 360 } ,
136+ showOnlyPositive : false ,
137+ showOnlyNegative : true ,
138+ } ;
139+ }
129140
130- // Calculate sweeps with minimum visibility
131- let positiveSweep = positiveRatio * availableSweep ;
132- let negativeSweep = ( 1 - positiveRatio ) * availableSweep ;
141+ if ( negativeValue === 0 ) {
142+ return {
143+ positiveArc : { start : 0 , end : 360 } ,
144+ negativeArc : { start : 0 , end : 0 } ,
145+ showOnlyPositive : true ,
146+ showOnlyNegative : false ,
147+ } ;
148+ }
133149
134- // Ensure minimum visibility for non-zero values
135- if ( positiveValue > 0 && positiveSweep < MIN_ARC_DEGREES ) {
136- positiveSweep = MIN_ARC_DEGREES ;
137- negativeSweep = availableSweep - MIN_ARC_DEGREES ;
138- }
139- if ( negativeValue > 0 && negativeSweep < MIN_ARC_DEGREES ) {
140- negativeSweep = MIN_ARC_DEGREES ;
141- positiveSweep = availableSweep - MIN_ARC_DEGREES ;
142- }
150+ const positiveRatio = positiveValue / total ;
143151
144- return {
145- // Right side: from top going clockwise
146- positiveArc : {
147- start : GAP_ANGLE / 2 ,
148- end : GAP_ANGLE / 2 + positiveSweep ,
149- } ,
150- // Left side: from where positive ends + gap, going clockwise
151- negativeArc : {
152- start : GAP_ANGLE / 2 + positiveSweep + GAP_ANGLE ,
153- end : GAP_ANGLE / 2 + positiveSweep + GAP_ANGLE + negativeSweep ,
154- } ,
155- } ;
156- } , [ positiveValue , negativeValue ] ) ;
152+ // Calculate sweeps with minimum visibility
153+ let positiveSweep = positiveRatio * availableSweep ;
154+ let negativeSweep = ( 1 - positiveRatio ) * availableSweep ;
155+
156+ // Ensure minimum visibility for non-zero values
157+ if ( positiveValue > 0 && positiveSweep < MIN_ARC_DEGREES ) {
158+ positiveSweep = MIN_ARC_DEGREES ;
159+ negativeSweep = availableSweep - MIN_ARC_DEGREES ;
160+ }
161+ if ( negativeValue > 0 && negativeSweep < MIN_ARC_DEGREES ) {
162+ negativeSweep = MIN_ARC_DEGREES ;
163+ positiveSweep = availableSweep - MIN_ARC_DEGREES ;
164+ }
165+
166+ return {
167+ // Right side: from top going clockwise
168+ positiveArc : {
169+ start : GAP_ANGLE / 2 ,
170+ end : GAP_ANGLE / 2 + positiveSweep ,
171+ } ,
172+ // Left side: from where positive ends + gap, going clockwise
173+ negativeArc : {
174+ start : GAP_ANGLE / 2 + positiveSweep + GAP_ANGLE ,
175+ end : GAP_ANGLE / 2 + positiveSweep + GAP_ANGLE + negativeSweep ,
176+ } ,
177+ showOnlyPositive : false ,
178+ showOnlyNegative : false ,
179+ } ;
180+ } , [ positiveValue , negativeValue ] ) ;
157181
158182 const center = size / 2 ;
159183 const strokeWidth = 3 ;
@@ -193,8 +217,34 @@ export function ActivityRing({
193217 style = { { width : size , height : size } }
194218 >
195219 < svg width = { size } height = { size } viewBox = { `0 0 ${ size } ${ size } ` } >
196- { /* Filled arc for positive (behind stroke) */ }
197- { positiveValue > 0 && (
220+ { /* Full circle fill for 100% positive */ }
221+ { showOnlyPositive && (
222+ < circle
223+ cx = { center }
224+ cy = { center }
225+ r = { ( fillOuterRadius + fillInnerRadius ) / 2 }
226+ fill = "none"
227+ stroke = { COLORS . positive }
228+ strokeWidth = { fillOuterRadius - fillInnerRadius }
229+ opacity = { 0.1 }
230+ />
231+ ) }
232+
233+ { /* Full circle fill for 100% negative */ }
234+ { showOnlyNegative && (
235+ < circle
236+ cx = { center }
237+ cy = { center }
238+ r = { ( fillOuterRadius + fillInnerRadius ) / 2 }
239+ fill = "none"
240+ stroke = { COLORS . negative }
241+ strokeWidth = { fillOuterRadius - fillInnerRadius }
242+ opacity = { 0.1 }
243+ />
244+ ) }
245+
246+ { /* Filled arc for positive (behind stroke) - only when not full circle */ }
247+ { positiveValue > 0 && ! showOnlyPositive && (
198248 < path
199249 d = { describeFilledArc (
200250 center ,
@@ -209,8 +259,8 @@ export function ActivityRing({
209259 />
210260 ) }
211261
212- { /* Filled arc for negative (behind stroke) */ }
213- { negativeValue > 0 && (
262+ { /* Filled arc for negative (behind stroke) - only when not full circle */ }
263+ { negativeValue > 0 && ! showOnlyNegative && (
214264 < path
215265 d = { describeFilledArc (
216266 center ,
@@ -225,35 +275,63 @@ export function ActivityRing({
225275 />
226276 ) }
227277
228- { /* Positive arc stroke (right side) */ }
229- < path
230- d = { describeArc (
231- center ,
232- center ,
233- outerRadius ,
234- positiveArc . start ,
235- positiveArc . end ,
236- ) }
237- stroke = { positiveColor }
238- strokeWidth = { strokeWidth }
239- fill = "none"
240- strokeLinecap = "round"
241- />
242-
243- { /* Negative arc stroke (left side) */ }
244- < path
245- d = { describeArc (
246- center ,
247- center ,
248- outerRadius ,
249- negativeArc . start ,
250- negativeArc . end ,
251- ) }
252- stroke = { negativeColor }
253- strokeWidth = { strokeWidth }
254- fill = "none"
255- strokeLinecap = "round"
256- />
278+ { /* Full circle stroke for 100% positive */ }
279+ { showOnlyPositive && (
280+ < circle
281+ cx = { center }
282+ cy = { center }
283+ r = { outerRadius }
284+ fill = "none"
285+ stroke = { positiveColor }
286+ strokeWidth = { strokeWidth }
287+ />
288+ ) }
289+
290+ { /* Full circle stroke for 100% negative */ }
291+ { showOnlyNegative && (
292+ < circle
293+ cx = { center }
294+ cy = { center }
295+ r = { outerRadius }
296+ fill = "none"
297+ stroke = { negativeColor }
298+ strokeWidth = { strokeWidth }
299+ />
300+ ) }
301+
302+ { /* Positive arc stroke (right side) - only when not full circle */ }
303+ { ! showOnlyPositive && ! showOnlyNegative && (
304+ < path
305+ d = { describeArc (
306+ center ,
307+ center ,
308+ outerRadius ,
309+ positiveArc . start ,
310+ positiveArc . end ,
311+ ) }
312+ stroke = { positiveColor }
313+ strokeWidth = { strokeWidth }
314+ fill = "none"
315+ strokeLinecap = "round"
316+ />
317+ ) }
318+
319+ { /* Negative arc stroke (left side) - only when not full circle */ }
320+ { ! showOnlyPositive && ! showOnlyNegative && (
321+ < path
322+ d = { describeArc (
323+ center ,
324+ center ,
325+ outerRadius ,
326+ negativeArc . start ,
327+ negativeArc . end ,
328+ ) }
329+ stroke = { negativeColor }
330+ strokeWidth = { strokeWidth }
331+ fill = "none"
332+ strokeLinecap = "round"
333+ />
334+ ) }
257335 </ svg >
258336
259337 { /* Centered icon */ }
0 commit comments