# ์ค์ต ์์
์ด๋ฒ ์ฑํฐ์์๋ ์์ ์ฝ๋๋ฅผ ์์ฉํด์ ์กฐ๊ธ ๋ ์ค์ ์ ์ธ ์์ ๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค. ๊ตฌํํด ๋ณผ ์์ ๋ npm์ ๋ฐฐํฌ๋ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด npm ํจํค์ง ํ์ด์ง๋ฅผ ๋ฐฉ๋ฌธํ๋ฉด ๋ณด์ด๋ ์ฐจํธ ์์ญ์ ๋๋ค.
์ ์ด๋ฏธ์ง๋ Vue์ npm ํ์ด์ง๋ก ๋๊ทธ๋ผ๋ฏธ ์น ๋ถ๋ถ์ ์ฃผ๋ณ ๋ค์ด๋ก๋ ํ์ ์ถ์ด๋ฅผ ์๊ฐํํ์ฌ ๋ณด์ฌ์ฃผ๊ณ ์์ต๋๋ค.
# ๋ฐ์ดํฐ ์ค๋น
ํจํค์ง ๋ค์ด๋ก๋ ํ์ ์ถ์ด๋ฅผ ์๊ฐํํ ์ฐจํธ ์์ญ์ ์ดํด๋ณด๋ฉด ์ ์ผ๋ถํฐ 364์ผ๊ฐ์ ์ผ๋ณ ๋ค์ด๋ก๋ ํ์๋ฅผ ์์ผ ์๊ด์์ด 7์ผ์ฉ ํฉ์ณ์ ์ด 52์ฃผ์น์ ๋ฐ์ดํฐ์์ ์ ์ ์์ต๋๋ค.
npm ๋ ์ง์คํธ๋ฆฌ ํจํค์ง ๋ค์ด๋ก๋ ํ์ ๋ฌธ์์ API ํธ์ถ ๊ฐ์ด๋์ ๋ฐ๋ผ์ https://api.npmjs.org/downloads/range/2020-10-12:2021-10-10/vue
์ ๊ฐ์ด URL์ ๋ง๋ค์ด์ ๋ค์ด๋ก๋ ํ์ ๋ฐ์ดํฐ๋ฅผ ์น ๋ธ๋ผ์ฐ์ ์์ ์กฐํํด ๋ณผ ์ ์์ต๋๋ค.
TIP
์์ธํ ๋ด์ฉ์ npm ๋ ์ง์คํธ๋ฆฌ ํจํค์ง ๋ค์ด๋ก๋ ํ์ ๋ฌธ์ (opens new window)๋ฅผ ํ์ธํด ์ฃผ์ธ์
์น ๋ธ๋ผ์ฐ์ ์์ ์กฐํ๋๋ ๋ฐ์ดํฐ๋ ์ผ๋ณ ๋ค์ด๋ก๋ ํ์์
๋๋ค. ์ ์ฒด๋ฅผ ๋ณต์ฌํด์ ๋ค์ jsonData
๋ณ์์ ๋์
ํด์ 7์ผ๋ง๋ค์ ๋ค์ด๋ก๋ ํฉ๊ณ๋ฅผ 52๊ฐ ๋ฐํํ๋ ํจ์๋ฅผ ์์ฑํฉ๋๋ค.
// data-vue.js
const jsonData = ... // ์น ๋ธ๋ผ์ฐ์ ์์ ์กฐํํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฒด ๋ณต์ฌํ ๋ค์ ์ฌ๊ธฐ์ ๋ถ์ฌ ๋ฃ์ผ์ธ์
export const fetchData = () => {
return jsonData.downloads.reduce((accumulator, currentValue, index) => {
if (index % 7 === 0) {
const data = {
label: currentValue.day + ' to ',
downloads: 0
}
accumulator.push(data)
} else if (index % 7 === 6) {
accumulator[accumulator.length -1].label += currentValue.day
}
accumulator[accumulator.length -1].downloads += currentValue.downloads
return accumulator
}, [])
}
# ์คํํฌ ๋ผ์ธ ๊ทธ๋ฆฌ๊ธฐ
์ด์ ์ ์งํํ ์ํ ์ฝ๋์์ SVG ์์ญ์ ์ ์ฒด ํฌ๊ธฐ๋ฅผ ์กฐ์ ํ๊ณ ์ ์ ๊ทธ๋ฆฌ๋ path ์์ ์ธ์ ์์ญ์ ๊ทธ๋ฆฌ๋ path ์์๋ฅผ ์ถ๊ฐํ ๋ค์ ์คํํฌ ๋ผ์ธ ๋๋์ ์ด๋ฆฌ๋ ์คํ์ผ๋ง์ ๋ํด์ค๋๋ค.
<template>
<svg
class="sparkline"
:width="width"
:height="height"
stroke-width="3"
stroke="#8956FF"
fill="rgba(137, 86, 255, .2)"
>
<path
class="sparkline--fill"
stroke="none"
:d="area(weeklyDownloads)"
>
</path>
<path
class="sparkline--line"
fill="none"
:d="line(weeklyDownloads)"
>
</path>
</svg>
</template>
<script>
import * as d3 from 'd3'
import { fetchData } from './data-vue'
export default {
data () {
return {
width: 200,
height: 40,
}
},
computed: {
weeklyDownloads () {
return fetchData()
},
xScale () {
return d3.scaleLinear()
.domain([0, this.weeklyDownloads.length])
.range([8, this.width])
},
yScale () {
return d3.scaleLinear()
.domain([0, d3.max(this.weeklyDownloads, d => d.downloads)]).nice()
.range([this.height, 5])
},
line () {
return d3.line()
.x((d, i) => this.xScale(i))
.y(d => this.yScale(d.downloads))
},
area () {
return d3.area()
.x((d, i) => this.xScale(i))
.y0(this.yScale(0))
.y1(d => this.yScale(d.downloads))
}
}
}
</script>
# ์ปค์ ์ด๋ฒคํธ ์ถ๊ฐํ๊ธฐ
npm ํจํค์ง ํ์ด์ง์ ๋งํฌ์
์ ์ฐธ๊ณ ํด์ ์ปค์๋ก ์ฌ์ฉ๋๋ ์์์ธ line
๊ณผ circle
์ ์ถ๊ฐํฉ๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ์ปค์๋ฅผ ๋ณด์ฌ์ฃผ์ง ์๊ธฐ ์ํด ์ขํ ๊ฐ์ผ๋ก -1000์ ๊ฐ์ง๋ ๋ณ์๋ฅผ data
์ ์ ์ํฉ๋๋ค. ๋ง์ฐ์ค๋ฅผ ์ฐจํธ ์์ญ์ ์ฌ๋ ธ์ ๋ ์ปค์๋ฅผ ๋ณด์ฌ์ฃผ๊ธฐ ์ํ ์ขํ๊ฐ์ path
๋ฐ์ดํฐ ๊ฐ์ ๊ตฌํ ๋ ์ฌ์ฉํ xScale
๊ณผ yScale
์ ํ์ฉํด์ xPoint
, yPoint
๋ก ๊ฐ๊ฐ ๊ตฌํฉ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ผ๋ก -1000์ ๊ฐ์ง๋ data
์ ์ขํ ๊ฐ ๋ณ์๋ฅผ ์๋ก์ด ์ขํ ๊ฐ์ผ๋ก ์
๋ฐ์ดํธํ๋ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ methods
์ ๋ง๋ค์ด์ค๋๋ค.
<template>
.
.
</path>
<line class="sparkline--cursor" :x1="lineX" :x2="lineX" y1="0" y2="40" stroke-width="2"></line>
<circle class="sparkline--spot" :cx="cx" :cy="cy" r="2"></circle>
<rect
class="sparkline--interaction-layer"
style="fill: transparent; stroke: transparent"
:width="width"
:height="height"
@mousemove="mousemoveHandler"
@mouseout="mouseoutHandler"
></rect>
</svg>
</template>
<script>
import * as d3 from 'd3'
import { fetchData } from './data-vue'
export default {
data () {
return {
width: 200,
height: 40,
lineX: -1000,
cx: -1000,
cy: -1000,
}
},
computed: {
.
.
xPoint () {
return this.weeklyDownloads.map((d, i) => this.xScale(i))
},
yPoint () {
return this.weeklyDownloads.map(d => this.yScale(d.downloads))
},
},
methods: {
hideCusor () {
this.lineX = -1000
this.cx = -1000
this.cy = -1000
},
mousemoveHandler (event) {
const pointIndex = this.xPoint.findIndex(d => event.layerX <= d)
if (pointIndex < 0) {
this.hideCusor()
} else {
this.lineX = this.xPoint[pointIndex]
this.cx = this.xPoint[pointIndex]
this.cy = this.yPoint[pointIndex]
}
},
mouseoutHandler () {
this.hideCusor()
}
}
}
</script>
# ๋๋ด๊ธฐ
๋ง์ง๋ง์ผ๋ก npm ํ์ด์ง๋ฅผ ์ฐธ๊ณ ํด์ ์คํ์ผ๋ง๊ณผ ๋งํฌ์
์ ๋ํ๊ณ methods
์ ์ ์ํ ์ด๋ฒคํธ ํธ๋ค๋ฌ์ ๊ธฐ๋ฅ์ ๋ณด์ํฉ๋๋ค.
<template>
<div class="downloads">
<h3 class="downloads--title">
{{ downloadTitle }}
</h3>
<div class="downloads--container">
<svg
class="sparkline"
:width="width"
:height="height"
stroke-width="3"
stroke="#8956FF"
fill="rgba(137, 86, 255, .2)"
>
<path
class="sparkline--fill"
stroke="none"
:d="area(weeklyDownloads)"
></path>
<path
class="sparkline--line"
fill="none"
:d="line(weeklyDownloads)"
></path>
<line
class="sparkline--cursor"
:x1="lineX"
:x2="lineX"
y1="0"
y2="40"
stroke-width="2"
></line>
<circle
class="sparkline--spot"
:cx="cx"
:cy="cy"
r="2"
></circle>
<rect
class="sparkline--interaction-layer"
style="fill: transparent; stroke: transparent"
:width="width"
:height="height"
@mousemove="mousemoveHandler"
@mouseout="mouseoutHandler"
></rect>
</svg>
<p class="downloads--count">
{{
downloadValue ||
weeklyDownloads[weeklyDownloads.length - 1].downloads
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
}}
</p>
</div>
</div>
</template>
<script>
import * as d3 from "d3";
import { fetchData } from "./data-vue";
export default {
data() {
return {
width: 200,
height: 40,
lineX: -1000,
cx: -1000,
cy: -1000,
downloadTitle: "Weekly Downloads",
downloadValue: null,
};
},
computed: {
weeklyDownloads() {
return fetchData();
},
xScale() {
return d3
.scaleLinear()
.domain([0, this.weeklyDownloads.length])
.range([8, this.width]);
},
yScale() {
return d3
.scaleLinear()
.domain([0, d3.max(this.weeklyDownloads, (d) => d.downloads)])
.nice()
.range([this.height, 5]);
},
line() {
return d3
.line()
.x((d, i) => this.xScale(i))
.y((d) => this.yScale(d.downloads));
},
area() {
return d3
.area()
.x((d, i) => this.xScale(i))
.y0(this.yScale(0))
.y1((d) => this.yScale(d.downloads));
},
xPoint() {
return this.weeklyDownloads.map((d, i) => this.xScale(i));
},
yPoint() {
return this.weeklyDownloads.map((d) => this.yScale(d.downloads));
},
},
methods: {
resetGraph() {
this.lineX = -1000;
this.cx = -1000;
this.cy = -1000;
this.downloadTitle = "Weekly Downloads";
this.downloadValue = null;
},
updateGraph(pointIndex) {
this.lineX = this.xPoint[pointIndex];
this.cx = this.xPoint[pointIndex];
this.cy = this.yPoint[pointIndex];
this.downloadTitle = this.weeklyDownloads[pointIndex].label;
this.downloadValue = this.weeklyDownloads[pointIndex].downloads
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
mousemoveHandler(event) {
const pointIndex = this.xPoint.findIndex((d) => event.layerX <= d);
if (pointIndex < 0) {
this.resetGraph();
} else {
this.updateGraph(pointIndex);
}
},
mouseoutHandler() {
this.resetGraph();
},
},
};
</script>
<style scoped>
.downloads {
width: 390px;
}
.downloads--title {
margin-top: 0.5rem;
margin-bottom: 0;
font-size: 1rem;
color: #757575;
}
.downloads--container {
display: flex;
align-items: flex-end;
flex-direction: row-reverse;
border-bottom: 2px solid rgba(137, 86, 255, 0.2);
}
.sparkline {
margin-right: -4px;
}
.downloads--count {
flex: 1 1 auto;
padding-bottom: 0.25rem;
padding-right: 0.5rem;
margin: 0;
font-weight: 600;
font-size: 1rem;
}
</style>
# D3 Gallery ์์
๋ค์ํ D3 ์์ ๋ฅผ ๋ชจ์๋์ D3 Gallery (opens new window)์ ์ฝ๋๋ฅผ ๋ทฐ๋ก ์ ํํ ๋ ๋ง์ ์ค์ต ์ฝ๋๋ฅผ ์ฌ๊ธฐ (opens new window)์์ ํ์ธํ ์ ์์ต๋๋ค.
โ D3 with Vue Concept โ