3

For the following code: REPL

How could I make each field of the circle be the same size in a simple way? The fields on the top and bottom are currently bigger than the other ones for obvious reasons.

I could do some complicated calculations with position relative and absolute, but I wonder if there's an easy solution for my problem?

This is what it should look like: enter image description here

6
  • It's not clear what you are asking. The heights of every lines in the circle are the same. Commented Sep 1, 2022 at 12:43
  • 2
    Please include all relevant code in the question. Commented Sep 1, 2022 at 12:43
  • @AmauryHanser I added a picture to my original question. Commented Sep 1, 2022 at 12:46
  • @H.B. everything is in the REPL I linked. Commented Sep 1, 2022 at 12:49
  • 1
    That is not relevant: StackOverflow requires questions to be self-contained, any links should be completely optional to the question. Commented Sep 1, 2022 at 12:56

3 Answers 3

5

Here's a variable <svg> solution REPL
Unfortunately not every field is seperately clickable which I just saw you mentioned in a comment. That's a crucial fact which you might add to your question.

Edit: By changing to fill: none; actually every field is seperately clickable, thanks @herrstrietzel

<script> const size = 300 let strokeWidth = 25 let gap = 20 $: circumference = Math.PI * (size - strokeWidth) $: r = size/2 - strokeWidth/2 let pieces = [ {stroke: 'teal'}, {stroke: 'magenta'}, {stroke: 'orange'}, ] let color = '#1411DF' </script> <div style:width="{size}px"> <svg width={size} height={size} style="transform: rotate({-90+(gap/2/r/Math.PI*180)}deg)"> {#each pieces as piece, index} {@const ownLength = circumference / pieces.length - gap} <circle r={r} cx={size/2} cy={size/2} style:stroke-width={strokeWidth} style:stroke={piece.stroke} style:stroke-dasharray="{ownLength} {circumference}" style="fill: none; transform-origin: center;" style:transform="rotate({index * 360 / pieces.length}deg)" on:click="{() => console.log(piece.stroke)}" /> {/each} </svg> <input type="color" bind:value={color}> <button on:click={() => pieces = [...pieces, {stroke: color}]}> add piece </button> <label> stroke-width: <input type="range" bind:value={strokeWidth} min="1" max={size/5}> {strokeWidth} </label> <label> gap: <input type="range" bind:value={gap} min="1" max={size/5}> {gap} </label> </div> <style> circle:hover { stroke: black !important; } div { margin: 0 auto; display: flex; flex-direction: column; align-items: center; } svg { display: block; margin-bottom: 2rem; } label { width: 100%; font-size: .9rem; padding: 1rem 0; } input { padding: 0; } </style> 

And inspired by H.B.'s answer a different solution with path elements REPL

<script> let colors = ['teal', 'DarkOrchid', 'orange'] let color = '#2E15D1' const size = 300 let strokeWidth = 10 let gap = 10 // degree $: r = size/2 - strokeWidth/2 $: deg = (180 - (360 / colors.length) + gap) / 2 $: x = r * Math.cos(rad(deg)) $: y = r * Math.sin(rad(deg)) function rad(angle) { return angle * Math.PI / 180; } </script> <div style:width="{size}px"> <svg width={size} height={size} viewbox="{-size/2} {-size/2} {size} {size}" xmlns="http://www.w3.org/2000/svg"> {#each colors as color, index} <path d="M -{x} -{y} A {r} {r} 0 0 1 {x} -{y}" style="fill: none;" style:stroke={color} style:stroke-width="{strokeWidth}" style:transform="rotate({360 / colors.length * index}deg)" on:click="{() => console.log(color)}" /> {/each} <!-- <circle cx="0" cy="0" {r} fill="none" stroke="black"></circle> --> <!-- <circle cx="0" cy="0" r="2"></circle> --> </svg> <input type="color" bind:value={color}> <button on:click={() => colors = [...colors, color]}> add piece </button> <label> stroke-width: <input type="range" bind:value={strokeWidth} min="1" max={size/5}> {strokeWidth} </label> <label> gap: <input type="range" bind:value={gap} min="1" max={(360/colors.length)-1}> {gap}° </label> </div> <style> div { margin: 0 auto; display: flex; flex-direction: column; align-items: center; } svg { margin: 2rem; /* transform: rotate(180deg); */ } path:hover { stroke: black !important; } input { padding: 0; margin: .4rem; } label { display: grid; align-items: center; grid-template-columns: 1fr max-content 1fr; font-size: .9rem; white-space: nowrap; } </style> 
Sign up to request clarification or add additional context in comments.

9 Comments

Using stroke-dasharray is quite a creative approach. I just implemented an arc-based solution, maybe you will find that interesting.
@Corrl: Change fill to fill: none - now the click should be working as expected. fill transparent will create an invisible area, that's why you can't select the correct pie wedges (the last segment is actually overlapping the previous segments)
@Corrl Thank you for the simple solution! I’ve searched for a long time and the solutions I found were either complicated or non existent.
@herrstrietzel I encountered another small issue. Some of the circle elements overlap buttons which are below my svg. So whenever I click on one of the circles, the text inside of my button gets highlighted as well. Do you have a solution for that problem?
@h-thilo do you mean by highlighted the text of the button is selected? Maybe something user-select: none; can fix..?
|
3

I could do some complicated calculations with position relative and absolute, but I wonder if there's an easy solution for my problem?

I'm afraid there is no solution completely without calculations, but at least this one is not complicated.

You can use conic-gradient and split it by degrees (in my example by 60deg) but the corners tend to be too pixelated. You can also use repeating-conic-gradient.

div { position: relative; width: 200px; height: 200px; border-radius: 100%; background: conic-gradient( /* per 60deg - 5*2deg for white space */ white 5deg, red 5deg 55deg, white 55deg 65deg, orange 65deg 115deg, white 115deg 125deg, blue 125deg 175deg, white 175deg 185deg, pink 185deg 235deg, white 235deg 245deg, gray 245deg 295deg, white 295deg 305deg, yellow 305deg 355deg, white 355deg 360deg ); } div:after { content: ''; position: absolute; left: 20px; top: 20px; width: 160px; height: 160px; background: white; border-radius: 100%; }
<div></div>

3 Comments

That's what I thought of first. The problem is that I need each field to be clickable (Probably should have mentioned that) and I don't think that works with using conic gradients, or does it?
It could be possible with <map> but in that case it is probably better to create your own SVG and paste it as a <svg>.
This answer might be helpful.
2

You could use an SVG. You can create path elements with arcs and stroke them in the respective color.

Arcs are fairly complicated with many parameters:

 A rx ry x-axis-rotation large-arc-flag sweep-flag x y a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy 

I would recommend reading the MDN documentation or the spec.

Example of a segment:

<svg width="320" height="320" xmlns="http://www.w3.org/2000/svg"> <path d="M 50 50 a 50 50 0 0 1 50 0" stroke="black" stroke-width="20" fill="none"/> </svg>


The easiest method is probably calculating the angles (start/end) and converting from polar to cartesian. The paths only need two commands: Absolute move to start location & absolute arc to end location.

Full example with clickable segments and keyboard support for additional accessibility (could still be improved, e.g. by supplying screen reader texts):

<script> let radius = 150; let stroke = 20; let gap = 5; let segments = [ '#ff0000', '#00ff00', '#0000ff', ]; function getCoordinates(i, gap) { const angleDelta = 360 / segments.length; const start = polarToCartesian(radius, i * angleDelta + gap); const end = polarToCartesian(radius, i * angleDelta + angleDelta); return { start, end }; } const polarToCartesian = (r, angle) => { return { x: r * Math.cos(rad(angle)), y: r * Math.sin(rad(angle)), } } const rad = x => x * Math.PI / 180; const onClick = i => alert('Segment ' + i); </script> <div> <svg width="320" height="320" xmlns="http://www.w3.org/2000/svg"> <g transform="translate(160, 160) rotate({-90 - gap/2})"> {#each segments as segment, i (i)} {@const { start, end } = getCoordinates(i, gap)} <path d="M {start.x} {start.y} A {radius} {radius} 0 0 1 {end.x} {end.y}" stroke={segment} stroke-width={stroke} fill="none" tabindex={0} on:keydown={e => { if (e.key == 'Enter') onClick(i); }} on:click={() => onClick(i)} /> {/each} </g> </svg> </div> 

REPL

2 Comments

That's definitely interesting! Building the paths itself seem cleaner than the stroke-dasharray approach - but it's also quite complicated ;-D I was trying this a bit differently, but gave up... One thing both our examples could probably be improved is the overall rotation ~ starting at the right with either the gap above or below always feels a bit off :)
True, nothing some quick maths can't fix though 😄