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

3 Answers3

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>
Corrl
  • 6,206
  • 1
  • 10
  • 36
  • Using `stroke-dasharray` is quite a creative approach. I just implemented an arc-based solution, maybe you will find that interesting. – H.B. Sep 01 '22 at 16:42
  • @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) – herrstrietzel Sep 01 '22 at 16:53
  • @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. –  Sep 01 '22 at 19:50
  • @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? –  Sep 03 '22 at 19:55
  • @h-thilo do you mean by highlighted the text of the button is selected? Maybe something `user-select: none;` can fix..? – Corrl Sep 03 '22 at 20:10
  • @Corrl Yeah, that's what I mean. The problem with user-select none is that I don't allow selecting the text anymore, which is not the greatest solution. I thought that there would maybe be a solution by manipulating the circle elements directly. –  Sep 03 '22 at 20:17
  • @Corrl user-select: none; unfortunately doesn't fix the problem. I am using the second solution. –  Sep 07 '22 at 11:30
  • @h-thilo do you have a Repl showing the problem? Probably would be best to start a new question with this... – Corrl Sep 07 '22 at 11:48
  • @Corrl I don't, but I can try to reproduce it. Unfortunately it's part of a larger application –  Sep 07 '22 at 11:56
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>
Jax-p
  • 7,225
  • 4
  • 28
  • 58
  • 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? –  Sep 01 '22 at 13:13
  • 1
    It could be possible with [](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map) but in that case it is probably better to create your own SVG and paste it as a [](https://developer.mozilla.org/en-US/docs/Web/SVG). – Jax-p Sep 01 '22 at 13:17
  • [This answer](https://stackoverflow.com/questions/21205652/how-to-draw-a-circle-sector-in-css#answer-31583489) might be helpful. – Jax-p Sep 01 '22 at 14:01
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

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 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 :) – Corrl Sep 01 '22 at 18:24
  • True, nothing some *quick maths* can't fix though – H.B. Sep 01 '22 at 18:53