1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-10

今回もこちらの続きのような感じですが、単項目の進捗度ではなく複数アイテムの円グラフを描いてみました。

####JavaScript####

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####

<!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
動作デモ

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

<!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
動作デモ
スライダーが多いのでスマホでは被って見辛いかもしれません。


1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?