Skip to content

Commit 38bf177

Browse files
committed
fix: use svg instead of mask
1 parent f50686c commit 38bf177

File tree

3 files changed

+102
-34
lines changed

3 files changed

+102
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ playwright-artifacts
2222
.env.test.local
2323
.env.production.local
2424
.vscode
25+
.cursor
2526

2627
npm-debug.log*
2728
yarn-debug.log*

src/components/DoughnutMetrics/DoughnutMetrics.scss

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,50 @@
11
.ydb-doughnut-metrics {
2-
--doughnut-border: 16px;
3-
--doughnut-width: 100px;
4-
--doughnut-wrapper-indent: calc(var(--doughnut-border) + 5px);
52
--doughnut-color: var(--g-color-base-positive-heavy);
63
--doughnut-backdrop-color: var(--g-color-base-generic);
74
--doughnut-overlap-color: var(--g-color-base-positive-heavy-hover);
85
--doughnut-text-color: var(--g-color-text-positive-heavy);
96

7+
position: relative;
8+
109
&__doughnut {
1110
position: relative;
1211

13-
width: var(--doughnut-width);
14-
aspect-ratio: 1;
12+
display: block;
13+
14+
// Enable smooth rendering for SVG
15+
shape-rendering: geometricPrecision;
16+
-webkit-font-smoothing: antialiased;
17+
-moz-osx-font-smoothing: grayscale;
1518

16-
border-radius: 50%;
17-
mask: radial-gradient(circle at center, transparent 46%, #000 46.5%);
19+
// Ensure SVG renders smoothly
20+
image-rendering: smooth;
21+
will-change: transform;
1822

19-
transform: rotate(180deg);
23+
// Preserve rotation origin
24+
transform-origin: center;
2025
}
2126

22-
// Size modifiers - using visually centered values
27+
// Size modifiers
2328
&__doughnut_size_small {
24-
--doughnut-border: 12px;
25-
--doughnut-width: 65px;
26-
--doughnut-wrapper-indent: 15px;
29+
width: 65px;
30+
height: 65px;
2731
}
2832

2933
&__doughnut_size_medium {
30-
--doughnut-border: 16px;
31-
--doughnut-width: 100px;
32-
--doughnut-wrapper-indent: calc(var(--doughnut-border) + 5px);
34+
width: 100px;
35+
height: 100px;
3336
}
3437

3538
&__doughnut_size_large {
36-
--doughnut-border: 20px;
37-
--doughnut-width: 130px;
38-
--doughnut-wrapper-indent: 25px;
39+
width: 130px;
40+
height: 130px;
41+
}
42+
43+
// Progress circle animation
44+
&__progress-circle,
45+
&__overlap-circle {
46+
transition: stroke-dasharray 0.3s ease;
47+
transform-origin: center;
3948
}
4049

4150
&_status_warning {
@@ -51,19 +60,20 @@
5160
&__text-wrapper {
5261
position: absolute;
5362
z-index: 1;
54-
top: var(--doughnut-wrapper-indent);
55-
left: var(--doughnut-wrapper-indent);
63+
top: 50%;
64+
left: 50%;
5665

5766
display: flex;
5867
flex-direction: column;
5968
justify-content: center;
6069
align-items: center;
6170

62-
width: calc(100% - calc(var(--doughnut-wrapper-indent) * 2));
71+
width: 100%;
72+
height: 100%;
6373

6474
text-align: center;
6575

66-
aspect-ratio: 1;
76+
transform: translate(-50%, -50%);
6777
}
6878

6979
&__value {

src/components/DoughnutMetrics/DoughnutMetrics.tsx

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,81 @@ export function DoughnutMetrics({
7777
className,
7878
size = 'medium',
7979
}: DoughnutProps) {
80-
let filledDegrees = fillWidth * 3.6;
81-
let doughnutFillVar = 'var(--doughnut-color)';
82-
let doughnutBackdropVar = 'var(--doughnut-backdrop-color)';
83-
84-
if (filledDegrees > 360) {
85-
filledDegrees -= 360;
86-
doughnutBackdropVar = 'var(--doughnut-color)';
87-
doughnutFillVar = 'var(--doughnut-overlap-color)';
80+
// Size configurations
81+
const sizeConfig = {
82+
small: {width: 65, strokeWidth: 12},
83+
medium: {width: 100, strokeWidth: 16},
84+
large: {width: 130, strokeWidth: 20},
85+
};
86+
87+
const config = sizeConfig[size];
88+
const radius = (config.width - config.strokeWidth) / 2;
89+
const circumference = 2 * Math.PI * radius;
90+
91+
// Calculate stroke dash for filled portion
92+
let strokeDasharray: string;
93+
// Start from bottom (270 degrees = 0.75 of circumference)
94+
const strokeDashoffset = circumference * 0.75;
95+
96+
if (fillWidth <= 100) {
97+
const filledLength = (fillWidth / 100) * circumference;
98+
// Use negative dash to go counter-clockwise
99+
strokeDasharray = `0 ${circumference - filledLength} ${filledLength} 0`;
100+
} else {
101+
// For values over 100%, we need to show overlap
102+
strokeDasharray = `0 0 ${circumference} 0`;
103+
// We'll use a second circle for the overlap
88104
}
89105

90-
const doughnutStyle: React.CSSProperties = {
91-
background: `conic-gradient(${doughnutFillVar} 0deg ${filledDegrees}deg, ${doughnutBackdropVar} ${filledDegrees}deg 360deg)`,
92-
};
106+
const needsOverlapCircle = fillWidth > 100;
107+
const overlapDasharray = needsOverlapCircle
108+
? `0 ${circumference - ((fillWidth - 100) / 100) * circumference} ${((fillWidth - 100) / 100) * circumference} 0`
109+
: '0 0';
93110

94111
return (
95112
<SizeContext.Provider value={size}>
96113
<div className={b({status}, className)} style={{position: 'relative'}}>
97-
<div style={doughnutStyle} className={b('doughnut', {size})}></div>
114+
<svg width={config.width} height={config.width} className={b('doughnut', {size})}>
115+
{/* Background circle */}
116+
<circle
117+
cx={config.width / 2}
118+
cy={config.width / 2}
119+
r={radius}
120+
fill="none"
121+
stroke="var(--doughnut-backdrop-color)"
122+
strokeWidth={config.strokeWidth}
123+
/>
124+
125+
{/* Progress circle */}
126+
<circle
127+
cx={config.width / 2}
128+
cy={config.width / 2}
129+
r={radius}
130+
fill="none"
131+
stroke="var(--doughnut-color)"
132+
strokeWidth={config.strokeWidth}
133+
strokeDasharray={strokeDasharray}
134+
strokeDashoffset={strokeDashoffset}
135+
strokeLinecap="butt"
136+
className={b('progress-circle')}
137+
/>
138+
139+
{/* Overlap circle for values > 100% */}
140+
{needsOverlapCircle && (
141+
<circle
142+
cx={config.width / 2}
143+
cy={config.width / 2}
144+
r={radius}
145+
fill="none"
146+
stroke="var(--doughnut-overlap-color)"
147+
strokeWidth={config.strokeWidth}
148+
strokeDasharray={overlapDasharray}
149+
strokeDashoffset={strokeDashoffset}
150+
strokeLinecap="butt"
151+
className={b('overlap-circle')}
152+
/>
153+
)}
154+
</svg>
98155
<div className={b('text-wrapper')}>{children}</div>
99156
</div>
100157
</SizeContext.Provider>

0 commit comments

Comments
 (0)