chart.js 코드 정리

MIT 라이선스인 chart.js를 예제를 통해 간단하게 정리해 봅니다. 구현하고자 하는 모습은 아래의 영상과 같습니다.

꺽은선 차트이며, 2개의 데이터 축을 가지고 1초마다 데이터가 변경됩니다.

먼저 DOM 구조입니다.

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<div>
    <canvas id="chart"></canvas>
</div>

CSS는 다음과 같구요.

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@100;300;400;500;700;900&display=swap');

body {
    margin: 0;
    padding: 0;
}

div {
    width: 600px;
    height: 400px;
    padding: 20px;
}

이제 JS 코드에 대한 내용인데, 먼저 차트를 위한 기본 골격은 다음과 같습니다.

Chart.defaults.font.family = 'Noto Sans KR';

const ctx = document.getElementById('chart');

const config = {
    // CORE PART !!
};

const chart = new Chart(ctx, config);

위의 config에 속성값을 지정함으로써 차트를 정의하는 세세한 내용을 결정할 수 있습니다. config에 대한 속성값은 다음과 같이 정의했습니다.

const config = {
    type: 'line',
    data: {
        labels: ['빨강', 'Blue', 'Yellow', '초록', 'Purple', '오렌지'],
        datasets: [
            {
                label: '변곡점1',
                data: [12, 19, 3, 5, 2, 3],
                backgroundColor: 'yellow',
                borderColor: 'black',
                borderWidth: 1
            },
            {
                label: '변곡점2',
                data: [10, 16, 7, 6, 4, 2],
                backgroundColor: 'white',
                borderColor: 'gray',
                borderWidth: 1
            }
        ]
    },
    options: {
        maintainAspectRatio: false,
        plugins: {
            title: {
                display: true,
                text: '차트, 그것이 알고 싶다.'
            }
        },
        scales: {
            x: {
                title: {
                    display: true,
                    text: '색상'
                }
            },
            y: {
                title: {
                    display: true,
                    text: '변곡량'
                }
            }
        },

    }
}

앞서 실행 화면을 보면 1초마다 차트의 값이 변경되는 것을 볼 수 있습니다. 이에 대한 코드는 다음과 같습니다.

setInterval(() => {
    const datasets = config.data.datasets;

    for (let iDataset = 0; iDataset < datasets.length; iDataset++) {
        const data = datasets[iDataset].data;
        for (let iValue = 0; iValue < data.length; iValue++) {
            data[iValue] += Math.random() * 2.0 - 1.0;
        }
    }

    chart.update();
}, 1000);

Javascript의 Array, Set, Object, Map에 대한 데이터 순회하기

자바스크립트에서 데이터 컨테이너로써의 관점에서 데이터를 순회하는 코드를 정리합니다.

먼저 Array입니다.

const arr = new Array(1, 2, 3, 4, 5)

console.log('Array Iteration 1')
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

console.log('Array Iteration 2')
arr.forEach(v => {
    console.log(v)
})

console.log('Array Iteration 3')
for (let v of arr) {
    console.log(v)
}

console.log('Array Iteration 4')
for (let i in arr) {
    console.log(arr[i])
}

다음은 Set입니다.

const set = new Set([1, 2, 3, 4, 'Hello'])

console.log('Set Iteration 1')
for (let v of set) {
    console.log(v)
}

console.log('Set Iteration 2')
for (let v of set.values()) {
    console.log(v)
}

console.log('Set Iteration 3')
set.forEach(v => {
    console.log(v)
});

다음은 Object입니다.

const obj = { a: 1, b: 2, c: 3, 9: 4, e: 'Hello' }

console.log('Object Iteration 1')
const keys = Object.keys(obj) // [ 'a', 'b', 'c', '9', 'e' ]

for (let i = 0; i < keys.length; i++) {
    const k = keys[i]
    const v = obj[k]
    console.log(k, v)
}

console.log('Object Iteration 2')
const values = Object.values(obj) // [ 1, 2, 3, 4, 'Hello' ]
for (let i = 0; i < values.length; i++) {
    const v = values[i]
    console.log(v)
}

console.log('Object Iteration 3')
const entries = Object.entries(obj) // [ ['a', 1], ['b', 2], ['c', 3], ['9', 4], ['e', 'Hello'] ]
for (let i = 0; i < entries.length; i++) {
    const k = entries[i][0]
    const v = entries[i][1]

    console.log(k, v)
}

console.log('Object Iteration 4')
for (let k in obj) {
    const v = obj[k]
    console.log(k, v)
}

끝으로 Map입니다.

const map = new Map( [ ['a', 1], ['b', 2], ['c', 3], ['9', 4], ['e', 'Hello'] ])

console.log('Map Iteration 1')
for (let [k, v] of map) {
    console.log(k, v)
}

console.log('Map Iteration 2')
for (let k of map.keys()) {
    console.log(k, map.get(k))
}

console.log('Map Iteration 3')
for (let v of map.values()) {
    console.log(v)
}

console.log('Map Iteration 4')
for (let [k, v] of map.entries()) {
    console.log(k, v)
}

console.log('Map Iteration 5')
map.forEach(function(v, k) {
    console.log(k, v)
})

사실 Map의 출현으로 더 이상 Object를 데이터 컨테이너로써 사용하는 것은 옳지 않습니다. Object는 기본적으로 생성될 때 기본 키값을 갖지만 Map은 개발자가 추가하지 않은 데이터는 갖지 않습니다. 또한 Map의 Key는 문자열 객체 이외에도 타입도 가능합니다. 또한 저장한 순서대로 값을 얻을 수 있으며 데이터의 개수도 size 함수를 통해 바로 얻을 수 있습니다. 게다가 Map은 데이터의 추가와 삭제 시 Object보다 성능이 뛰어납니다.

three.js의 scene 구성 요소들 확인하기

3차원에서는 다양한 3차원 객체가 Scene Graph 형태로 구성되어 있습니다. 렌더링하는 결과에 따라 그 형태가 매우 복잡하게 구성될 수 있는데.. Scene Graph의 구성 요소를 확인하기 위한 간단한 함수입니다.

dumpVec3(v3, precision = 3) {
    return `${v3.x.toFixed(precision)}, ${v3.y.toFixed(precision)}, ${v3.z.toFixed(precision)}`;
}

dumpObject(obj, lines = [], isLast = true, prefix = '') {
    const localPrefix = isLast ? '└─' : '├─';
    lines.push(`${prefix}${prefix ? localPrefix : ''}${obj.name || '*no-name*'} [${obj.type}]`);
    const dataPrefix = obj.children.length
        ? (isLast ? '  │ ' : '│ │ ')
        : (isLast ? '    ' : '│   ');
    lines.push(`${prefix}${dataPrefix} pos: ${this.dumpVec3(obj.position)}`);
    lines.push(`${prefix}${dataPrefix} rot: ${this.dumpVec3(obj.rotation)}`);
    lines.push(`${prefix}${dataPrefix} scl: ${this.dumpVec3(obj.scale)}`);
    const newPrefix = prefix + (isLast ? '  ' : '│ ');
    const lastNdx = obj.children.length - 1;
    obj.children.forEach((child, ndx) => {
        const isLast = ndx === lastNdx;
        this.dumpObject(child, lines, isLast, newPrefix);
    });
    return lines;
}

사용은 다음과 같습니다.

console.log(this.dumpObject(root).join('\n'));

결과 예는 다음과 같습니다.

*no-name* [Scene]
  │  pos: 0.000, 0.000, 0.000
  │  rot: 0.000, 0.000, 0.000
  │  scl: 1.000, 1.000, 1.000
  ├─*no-name* [DirectionalLight]
  │    pos: -250.000, 800.000, -850.000
  │    rot: 0.000, 0.000, 0.000
  │    scl: 1.000, 1.000, 1.000
  ├─*no-name* [Object3D]
  │    pos: -550.000, 40.000, -450.000
  │    rot: 0.000, 0.000, 0.000
  │    scl: 1.000, 1.000, 1.000

... 

  ├─*no-name* [Object3D]
  │ │  pos: 571.897, -76.040, -1163.608
  │ │  rot: 0.000, 0.000, 0.000
  │ │  scl: 1.000, 1.000, 1.000
  │ └─CAR_03_3 [Object3D]
  │   │  pos: 0.000, 33.000, 0.000
  │   │  rot: 0.000, 3.142, 0.000
  │   │  scl: 1.500, 1.500, 1.500
  │   └─CAR_03_3_World_ap_0 [Mesh]
  │        pos: 0.000, 0.000, 0.000
  │        rot: 0.000, 0.000, 0.000
  │        scl: 1.000, 1.000, 1.000
  └─*no-name* [Line]
       pos: 0.000, -621.000, 0.000
       rot: 0.000, 0.000, 0.000
       scl: 100.000, 100.000, 100.000

matter.js를 이용한 강체 시뮬레이션

matter.js 라이브러리를 이용하여 구현하고자 하는 결과는 아래와 같습니다.

먼저 HTML 코드입니다.

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style_matter.js.css">
    <script src="decomp.js"></script>
    <script src="pathseg.js"></script>
    <script src="matter.0.16.1.js"></script>
    <script src="app_matter.js.js" defer></script>
</head>
<body>
    <div></div>
</body>
</html> 

지형은 SVG 데이터를 통해 좌표를 뽑아오는데 이를 위해 matter.js 이외에도 decomp.js와 pathseg.js 라이브러리가 필요합니다. div 요소에 강체 시뮬레이션의 결과가 표시됩니다. 다음은 CSS입니다.

* {
    outline: none;
    padding: 0;
    margin: 0;
}
    
body {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100vh;
}

div 요소를 화면 중심에 놓기 위한 것이 전부입니다. 이제 js 코드를 살펴보겠습니다.

먼저 아래의 코드로 기본 코드를 작성합니다.

const engine = Matter.Engine.create();
const world = engine.world;

const render = Matter.Render.create({
    element: document.querySelector("div"),
    engine: engine,
    options: {
        width: 800,
        height: 600,
        wireframes: false,
        background: 'black'
    }
});

Matter.Render.run(render);

const runner = Matter.Runner.create();
Matter.Runner.run(runner, engine);

다음으로 지형과 지형 속에 사각형 물체를 구성하는 코드입니다.

fetch("./terrain.svg")
    .then((response) => { return response.text(); })
    .then((raw) => { return (new window.DOMParser()).parseFromString(raw, "image/svg+xml"); })
    .then(function(root) {
        const paths = Array.prototype.slice.call(root.querySelectorAll('path'));
        const vertices = paths.map((path) => { return Matter.Svg.pathToVertices(path, 30); });
        const terrain = Matter.Bodies.fromVertices(400, 350, vertices, {
            isStatic: true,
            render: {
                fillStyle: '#2c3e50',
                strokeStyle: '#2c3e50',
                lineWidth: 1,
            }
        }, true);

        Matter.World.add(world, terrain);

        const bodyOptions = {
            frictionAir: 0.1, 
            friction: 0.5,
            restitution: 0.1
        };
        
        Matter.World.add(world, Matter.Composites.stack(100, 200, 40, 10, 15, 15, (x, y) => {
            if (Matter.Query.point([terrain], { x: x, y: y }).length === 0) {
                return Matter.Bodies.polygon(x, y, 4, 10, bodyOptions);
            }
        }));
    }
);

끝으로 마우스를 통해 사각형 객체를 드레그하여 옮길 수 있도록 합니다.

const mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, {
    mouse: mouse,
    constraint: {
        stiffness: 0.2,
        render: {
            visible: false
        }
    }
});

Matter.World.add(world, mouseConstraint);

matter.js는 매우 정교한 강체 시뮬레이션에는 적합하지 않으나 시각적인 면에서 다양한 물리 효과를 2차원에서 폭넓게 응용할 수 있다는 점이 매우 큰 장점입니다.

아래는 필요한 코드와 데이터 전체를 다운로드 받을 수 있는 링크입니다.

Javascript 객체(object)의 키(key)와 값(value)을 배열로 얻기

만약 다음과 같은 자바스크립트 객체가 있다고 할 때..

const obj = {
  a: 'Dip2K',
  b: 30,
  c: true,
  d: {}
};

obj 객체를 구성하는 전체 키를 배열로 얻는 코드는 다음과 같습니다.

console.log(Object.keys(obj)); 
// Array ["a", "b", "c", "d"]

다시 obj 객체를 구성하는 전체 값을 배열로 얻는 코드는 다음과 같구요.

console.log(Object.values(obj)); 
// Array ["Dip2K", 30, true, Object {  }]

배열에 대해서 각 구성 항목을 참조하는 코드는 다음과 같습니다.

for(i of Object.values(obj)) {
    console.log(i);
}

/* 
"Dip2K"
30
true
Object {  }
*/