# ์‹ค์Šต ์˜ˆ์ œ

์ด๋ฒˆ ์ฑ•ํ„ฐ์—์„œ๋Š” ์•ž์˜ ์ฝ”๋“œ๋ฅผ ์‘์šฉํ•ด์„œ ์กฐ๊ธˆ ๋” ์‹ค์ „์ ์ธ ์˜ˆ์ œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„ํ•ด ๋ณผ ์˜ˆ์ œ๋Š” npm์— ๋ฐฐํฌ๋œ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด npm ํŒจํ‚ค์ง€ ํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜๋ฉด ๋ณด์ด๋Š” ์ฐจํŠธ ์˜์—ญ์ž…๋‹ˆ๋‹ค.

tutorial-npm-vue

์œ„ ์ด๋ฏธ์ง€๋Š” 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 ์˜ˆ์ œ๋ฅผ ๋ชจ์•„๋†“์€ D3 Gallery (opens new window)์˜ ์ฝ”๋“œ๋ฅผ ๋ทฐ๋กœ ์ „ํ™˜ํ•œ ๋” ๋งŽ์€ ์‹ค์Šต ์ฝ”๋“œ๋ฅผ ์—ฌ๊ธฐ (opens new window)์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.