CSSのradial-gradient、conic-gradientで描いた円グラフをJavaScriptで可変

今回も[**こちら**](https://qiita.com/akebi_mh/items/744efe3bb4e53a5a19f7)の続きのような感じですが、単項目の進捗度ではなく複数アイテムの円グラフを描いてみました。

####JavaScript####

```js:circle_graph.js
'use strict';
function drawGraph(obj) {
    const baseElement = document.getElementById(obj.targetId);
    if(baseElement === null) return;

    const
        bgColor  = obj.backgroundColor ?? 'transparent',
        size     = obj.size ?? 200,
        weight   = obj.weight ?? 0.5,
        sort     = obj.sort ?? 1,
        items    = (obj.items ?? []).slice();

    // ソート除外アイテム退避
    const fixed = [];
    for(let i = 0; i < items.length; i++) {
        if(items[i].fixed !== undefined && 'fr'.indexOf(items[i].fixed[0]) >= 0) {
            fixed.push(items.splice(i--, 1)[0]);
        }
    }

    // ソート
    if(sort !== 0) items.sort(function(a, b) { return (b.v - a.v) * sort;});

    // ソート除外アイテムを戻し
    if(fixed.length) {
        fixed.forEach(i => {
            if(i.fixed === 'f') items.unshift(i);
            else if(i.fixed === 'r') items.push(i);
        });
    }

    // トータル算出
    let total = 0;
    items.forEach(i => { total += +i.v;});
    // 百分率追加
    items.forEach(i => { i.p = total > 0 ? 100 * i.v / total : 0;});

    // conic-gradientへ渡すパラメータ生成
    const degree = [];
    let t = 0;
    items.forEach(i => {
        if(i.p > 0) degree.push(`${i.c} ${t}deg ${t = t + i.p * 3.6}deg`);
    });
    if(total === 0) degree.push('#ddd 0 360deg');

    // グラフ描画
    baseElement.innerHTML = "<div class='circle'><div class='graph'></div></div>";

    const
        circle = baseElement.querySelector('.circle'),
        graph  = circle.querySelector('.graph'),
        outerRadius = size / 2,
        innerRadius = outerRadius * (1 - weight);

    circle.style.position = 'relative';
    graph.style.position = 'absolute';
    graph.style.mixBlendMode = 'multiply';

    baseElement.style.width =
    baseElement.style.height =
    circle.style.width =
    circle.style.height =
    graph.style.width =
    graph.style.height = `${size}px`;
    circle.style.background =
        `radial-gradient(${bgColor} ${innerRadius - 1}px, #fff ${innerRadius}px, #fff ${outerRadius - 1}px, ${bgColor} ${outerRadius}px)`;
    graph.style.background =
        `radial-gradient(#fff ${innerRadius - 1}px, #0000 ${innerRadius}px, #0000  ${outerRadius - 1}px, #fff ${outerRadius}px), conic-gradient(${degree.join(', ')})`;

    return {
        total: total,
        items: items,
    };
}
```

####サンプルHTML####

```html
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<script src='./circle_graph.js'></script>
</head>
<body style='background-color: #ccc'>

    <div id='id1'></div>

    <script>
        'use strict';
        const obj = {
            // 描画先要素ID
            targetId: 'id1',
            // 背景色
            backgroundColor: 'transparent',
            // グラフ直径
            size: 250,
            // 太さ 0～1
            weight: 0.5,
            // ソート 1:降順 -1:昇順 0:配置順
            sort: 1,
            // アイテム配列
            items: [
                // c:色, v:数, n:名前 [,fixed:ソート除外('f'前方固定|'r'後方固定)]
                {c:'red',     v:239, n:'name1'},
                {c:'orange',  v:110, n:'name2'},
                {c:'blue',    v:75,  n:'name3'},
                {c:'cyan',    v:33,  n:'name4'},
                {c:'yellow',  v:428, n:'name5'},
                {c:'magenta', v:183, n:'name6'},
                {c:'#fff',    v:31,  n:'other1', fixed:'f'},
                {c:'#444',    v:230, n:'other2', fixed:'r'},
            ],
        };

        window.addEventListener('DOMContentLoaded', function(){
            const res = drawGraph(obj);
            console.log(res);
        });
    </script>
</body>
</html>
```

こんな感じになります。
![01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/b49e8d4d-abe5-b7b3-b4c6-b1e9afe35e88.jpeg)
[**動作デモ**](https://akebi.jp/temp/circle_graph/index.html)

####スライダーでの入力反映サンプル####

```html
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<script src='./circle_graph.js'></script>
<style>
.panel {
    background-color: #fff8;
    position: fixed;
    bottom: 0px;
    left: 0px;
    padding: 8px;
    width: 100%;
}
.colorcode, .name {
    width: 80px;
}
.picker {
    width: 28px;
    height: 20px;
    padding: 0px;
}
.slider {
    width: 50%;
}
.view {
    display: flex;
    padding: 10px;
}
div[id=id1] {
    display: inline-table;
}
div[id=id2] {
    margin: 8px;
}
#icon {
    position: absolute;
    top: 0px;
    right: 20px;
    width: 20px;
    height: 20px;
    cursor: pointer;
}
</style>
</head>
<body style='background-color: #ccc'>

    <div class='view'>
        <div id='id1'></div>
        <div id='id2'></div>
    </div>

    <div class='panel'>
        <span id='icon'>▼</span>
        <input type='range' min='10' max='500' id='size' oninput='update()'>直径&emsp;
        <input type='range' min='0' max='1' step='0.02' id='weight' oninput='update()'>太さ&emsp;
        <select id='sort' onchange='update()'>
            <option value='0'>配置順</option>
            <option value='1'>降順</option>
            <option value='-1'>昇順</option>
        </select>

        <div class='items'>
            <br>
            <span class='p'>
                <input type='checkbox' class='check' checked onclick='chgActive()'>
                <input type='text' class='colorcode' value='#000000' oninput='update()'>
                <input type='color' class='picker' oninput='update(1)'>
                <input type='text' class='name' value='name1' oninput='update()'>
                <input type='range' class='slider' min='0' max='1000' value='0' oninput='update()'>
                <select class='fixed' onchange='update()'>
                    <option value=''>---</option>
                    <option value='f'>前方固定</option>
                    <option value='r'>後方固定</option>
                </select>
            </span>
        </div>
    </div>

    <script>
    'use strict';
    const obj = {
        targetId: 'id1',
        size: 250,
        weight: 1,
        sort: -1,
        items: [
            {c:'#ff0000', v:50,  n:'name1'},
            {c:'#00ff00', v:100, n:'name2'},
            {c:'#ffff00', v:150, n:'name3'},
            {c:'#0000ff', v:200, n:'name4'},
            {c:'#ff00ff', v:250, n:'name5'},
            {c:'#00ffff', v:300, n:'name6'},
            {c:'#ffffff', v:150, n:'other1'},
            {c:'#000000', v:150, n:'other2'},
        ],
    };

    window.addEventListener('DOMContentLoaded', function() {
        const panelItems = document.querySelector('.panel .items');
        let s1 = panelItems.innerHTML,
            s2 = s1.repeat(obj.items.length);
        panelItems.innerHTML = s2;

        const
            colorcodes = document.querySelectorAll('input[class=colorcode]'),
            sliders    = document.querySelectorAll('input[class=slider]'),
            pickers    = document.querySelectorAll('input[class=picker]'),
            names      = document.querySelectorAll('input[class=name]'),
            fixeds     = document.querySelectorAll('select[class=fixed]');

        for(let i = 0; i < obj.items.length; i++) {
            pickers[i].value = colorcodes[i].value = obj.items[i].c;
            sliders[i].value = obj.items[i].v;
            names[i].value = obj.items[i].n;
            if(obj.items[i].fixed !== undefined && 'fr'.indexOf(obj.items[i].fixed) >= 0) {
                fixeds[i].value = obj.items[i].fixed;
            }
        }
        document.getElementById('size').value = obj.size;
        document.getElementById('weight').value = obj.weight;
        document.getElementById('sort').value = obj.sort;

        putList(drawGraph(obj));

        document.getElementById('icon').addEventListener('click', function(){
            const p = document.querySelector('.panel');
            if(p.style.height !== '22px') {
                this.textContent = '▲';
                p.style.height = '22px';
            }
            else {
                this.textContent = '▼';
                p.style.height = '';
            }
        });
    });

    function update(s) {
        const
            checkboxs  = document.querySelectorAll('input[type=checkbox]'),
            colorcodes = document.querySelectorAll('input[class=colorcode]'),
            sliders    = document.querySelectorAll('input[class=slider]'),
            pickers    = document.querySelectorAll('input[class=picker]'),
            names      = document.querySelectorAll('input[class=name]'),
            fixeds     = document.querySelectorAll('select[class=fixed]');

        const p = [];
        obj.items = [];
        for(let i = 0; i < sliders.length; i++) {
            if(!checkboxs[i].checked) continue;
            if(s) colorcodes[i].value = pickers[i].value;
            else pickers[i].value = cvtColorCode(colorcodes[i].value);

            const
                colorcode = colorcodes[i].value,
                slider    = sliders[i].value,
                name      = names[i].value,
                fixed     = fixeds[i].value;

            p.push({c:colorcode,v:slider,n:name,fixed:fixed});
            obj.items.push({c:colorcode,v:+slider,n:name,fixed:fixed});
        }

        obj.size = document.getElementById('size').value;
        obj.weight = document.getElementById('weight').value;
        obj.sort = document.getElementById('sort').value;

        putList(drawGraph(obj));
    }

    function putList(obj) {
        let str = '';
        obj.items.forEach(i => {
            if(i.v > 0) {
                str += `<span style='color:${i.c}'>■</span> ${i.n.replace(/^\s*$/, '----').replace(/</g, '&lt;')} : ${i.v} (${i.p.toFixed(1)}%)<br>`;
            }
        });
        str += `<br>&emsp;TOTAL : ${obj.total}`;
        document.getElementById('id2').innerHTML = str;
    }

    function chgActive() {
        const
            ps = document.querySelectorAll('span.p'),
            checkboxs = document.querySelectorAll('input[type="checkbox"]');

        for(let i = 0; i < checkboxs.length; i++) {
            ps[i].style.opacity = checkboxs[i].checked ? '1' : '0.5';
        }
        update();
    }

    function cvtColorCode(s) {
        const dummy = document.createElement('p');
        dummy.style.background = s;
        const c = dummy.style.background.split(',');
        for(let i = 0; i < c.length; i++) {
            c[i] = parseFloat(c[i].replace(/^\D+/,''));
            if(!isNaN(c[i])) c[i] = c[i].toString(16).padStart(2, '0');
        }
        return /^[\da-f]{2}$/i.test(c[0]) ? '#' + c.slice(0, 3).join('') : '#dddddd';
    }
    </script>
</body>
</html>
```

![02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618708/cbd2c0d0-4bcc-dd87-1ced-c622911705a4.jpeg)
[**動作デモ**](https://akebi.jp/temp/circle_graph/index2.html)
スライダーが多いのでスマホでは被って見辛いかもしれません。

----