카테고리 없음

D3.js를 이용한 과제 자료

Isaac Kenastan 2025. 5. 9. 12:39

D3.js (Data-Driven Documents) | devkuma

 

D3.js (Data-Driven Documents)

개발 지식 공유

www.devkuma.com

 

//----------------------------------------------
// ✨ Kirby 캐릭터 완성하기 (D3.js) ✨
//----------------------------------------------

// 1) 먼저 HTML 문서의 <body>를 선택해요.
//    → 여기다가 SVG 그림판을 붙일 거예요.
let body = d3.select("body");

// 2) SVG 캔버스 추가 및 크기·테두리 설정
//    • width, height: 500px × 500px 크기로 만들어요.
//    • 스타일: 테두리를 넣어서 어디까지가 캔버스인지 한눈에 보여줘요.
let svg = body
  .append("svg")                      
  .attr("width", 500)                  
  .attr("height", 500)                
  .style("border", "1px solid black");

//----------------------------------------------
// 3) Kirby의 팔(arm) 그리기 🎨
//----------------------------------------------
// ————————————————————————————————
// 왼쪽 팔
// ————————————————————————————————
svg
  .append("ellipse")               // 타원 모양(ellipse) 생성
  .attr("cx", 160)                 // 타원의 중심 x 좌표
  .attr("cy", 320)                 // 타원의 중심 y 좌표
  .attr("rx", 80)                  // 가로 반지름
  .attr("ry", 50)                  // 세로 반지름
  .attr("transform", "rotate(70, 250, 250)")
    // → (250,250)을 중심으로 70도 회전!
  .style("fill", "#ffb4dc")        // 연한 분홍색으로 채우기
  .style("stroke", "black")        // 테두리는 검정색
  .style("stroke-width", 8);       // 테두리 두께 8px

// ————————————————————————————————
// 오른쪽 팔
// ————————————————————————————————
svg
  .append("ellipse")
  .attr("cx", 320)                
  .attr("cy", 180)                
  .attr("rx", 80)
  .attr("ry", 50)
  .attr("transform", "rotate(50, 250, 250)")
    // → 중심(250,250) 기준으로 50도 회전!
  .style("fill", "#ffb4dc")
  .style("stroke", "black")
  .style("stroke-width", 8);

//----------------------------------------------
// 4) Kirby의 발(leg) 그리기 👟
//----------------------------------------------
// ————————————————————————————————
// 왼쪽 발
// ————————————————————————————————
svg
  .append("ellipse")
  .attr("cx", 160)
  .attr("cy", 320)
  .attr("rx", 80)
  .attr("ry", 40)
  .attr("transform", "rotate(-30, 250, 250)")
    // → 왼쪽으로 -30도 빙글빙글~
  .style("fill", "#cf4f99")       // 짙은 분홍색
  .style("stroke", "black")
  .style("stroke-width", 8);

// ————————————————————————————————
// 오른쪽 발
// ————————————————————————————————
svg
  .append("ellipse")
  .attr("cx", 340)
  .attr("cy", 320)
  .attr("rx", 80)
  .attr("ry", 40)
  .attr("transform", "rotate(30, 250, 250)")
    // → 오른쪽으로 30도 회전!
  .style("fill", "#cf4f99")
  .style("stroke", "black")
  .style("stroke-width", 8);

//----------------------------------------------
// 5) Kirby의 몸통(body) 그리기 🟣
//----------------------------------------------
svg
  .append("circle")                // 원(circle) 생성
  .attr("cx", 250)                 // 중심 x = 250
  .attr("cy", 250)                 // 중심 y = 250
  .attr("r", 120)                  // 반지름 120px
  .style("fill", "#ffb4dc")        // 몸통은 연분홍
  .style("stroke", "black")        // 테두리 검정
  .style("stroke-width", 8);       // 테두리 두께 8px

//----------------------------------------------
// 6) Kirby의 눈(eyes) 그리기 👀
//----------------------------------------------
// 왼쪽 눈동자 그리기
svg
  .append("ellipse")
  .attr("cx", 225)
  .attr("cy", 240)
  .attr("rx", 6)
  .attr("ry", 20)
  .style("fill", "black");         // 눈동자는 검정색

// 왼쪽 눈 하이라이트(반짝임)
svg
  .append("ellipse")
  .attr("cx", 225)
  .attr("cy", 235)
  .attr("rx", 2)
  .attr("ry", 10)
  .style("fill", "white");         // 작게 흰색 점을 찍어줘요

// 오른쪽 눈동자
svg
  .append("ellipse")
  .attr("cx", 275)
  .attr("cy", 240)
  .attr("rx", 6)
  .attr("ry", 20)
  .style("fill", "black");

// 오른쪽 눈 하이라이트
svg
  .append("ellipse")
  .attr("cx", 275)
  .attr("cy", 235)
  .attr("rx", 2)
  .attr("ry", 10)
  .style("fill", "white");

//----------------------------------------------
// 7) Kirby의 입(mouth) 그리기 👄
//----------------------------------------------
svg
  .append("polygon")               // 다각형(polygon)으로 그려요
  .attr("points", "240,270 260,270 250,290")
    // → 좌표 3개로 작은 삼각형을 만듭니다
  .style("fill", "red")            // 채우기 빨강
  .style("stroke", "black")        // 테두리 검정
  .style("stroke-width", 4);       // 테두리 두께 4px

//----------------------------------------------
// 8) Star Rod 불러와 장식하기 🌟
//----------------------------------------------
// d3.xml로 외부 SVG 파일(starrod.svg) 로드
d3.xml("starrod.svg").then((starrod) => {
  // • 새로운 그룹<g>을 추가하고
  // • (250,250)을 중심으로 60도 회전시켜요
  let g = svg.append("g")
    .attr("transform", "rotate(60, 250, 250)");

  // • 로드된 starrod.svg를 그룹에 붙이고
  //   위치(x,y)와 크기(width,height)를 조절해 배치합니다
  g.append(() => starrod.documentElement)
    .attr("width", 200)
    .attr("height", 200)
    .attr("x", -20)
    .attr("y", 150);
});

 

console.log("hello World!") // We encourage you to utilize console.log frequently!



// sample data
const data = [
    { date: "2022-01-10", temperature: 28.4 },
    { date: "2022-01-04", temperature: 23.1 },
    { date: "2022-01-05", temperature: null },
    { date: "2022-01-08", temperature: 22.8 },
    { date: "2022-01-02", temperature: 24.2 },
    { date: null, temperature: 32.0 },
    { date: "2022-01-12", temperature: 23.9 },
    { date: "2022-01-14", temperature: 21.7 },
    { date: null, temperature: 25.1 },
    { date: "2022-01-18", temperature: 30.8 },
    { date: "2022-01-20", temperature: 29.6 },
    { date: "2022-01-01", temperature: 21.5 },
    { date: null, temperature: 26.7 },
    { date: "2022-01-09", temperature: null },
    { date: "2022-01-13", temperature: null },
    { date: "2022-01-07", temperature: 30.5 },
    { date: "2022-01-16", temperature: 27.3 },
    { date: "2022-01-03", temperature: null },
    { date: "2022-01-11", temperature: 32.0 },
    { date: "2022-01-17", temperature: null },
];


parsedData = data.map(d => {
    return { date: d3.timeParse("%Y-%m-%d")(d.date), temperature: d.temperature }
})

/*
-------------------------------------------------

Your Code Starts here!!
 
-------------------------------------------------
*/

// 1) null 값(유효하지 않은 날짜 혹은 온도) 제거하기
// filteredData 변수에는 'parsedData' 배열에서
// .filter() 메서드가 true를 반환한 요소들만 남깁니다.
var filteredData = parsedData.filter(d => {
    // d.date   !== null?  → d.date가 null이 아니어야 통과
    // d.temperature !== null? → d.temperature가 null이 아니어야 통과
    // date나 temperature가 하나라도 null이면 false가 되어 해당 항목을 걸러냅니다.
    return d.date !== null && d.temperature !== null;
  });
  // ---------------------------------------------
  // filter 설명:
  //  - parsedData는 {date: Date객체, temperature: 숫자 또는 null} 형태의 객체 배열.
  //  - .filter(callback) 내부의 callback(d)는 각 요소를 검사.
  //  - return 값이 true인 요소만 새로운 배열(filteredData)에 포함됩니다.
  //  - 여기서는 'null인 항목 제거'가 목적입니다.
  // ---------------------------------------------
 
 
  // 2) 날짜(Date) 오름차순 정렬하기
  // filteredData 배열 자체를 정렬(sorting)하여
  // 날짜가 빠른 것부터 느린 것 순서로 재배치합니다.
  filteredData.sort((a, b) => {
    // d3.ascending(a.date, b.date)는
    //  - a.date < b.date  → 음수 반환 → a가 b보다 앞에 옴
    //  - a.date > b.date  → 양수 반환 → a가 b보다 뒤로 옮겨짐
    //  - 같으면 0 반환
    return d3.ascending(a.date, b.date);
  });
  // ---------------------------------------------
  // sort 설명:
  //  - .sort(compareFunction)는 배열을 제자리에서 정렬함(in-place).
  //  - compareFunction(a, b)에서 음수/양수/0에 따라 순서를 결정.
  //  - d3.ascending은 두 값을 비교해 위 규칙에 맞는 숫자를 반환.
  //  - 이 과정을 거치면 시간 순서대로 차트가 그려지게 됩니다.


// 2) null date 또는 null temperature 항목 제거
var filteredData = parsedData.filter(d =>
    d.date   !== null &&
    d.temperature !== null
);

// 3) 날짜 오름차순 정렬
filteredData.sort((a, b) => d3.ascending(a.date, b.date));

/*
-------------------------------------------------

Your Code Ends here!!

-------------------------------------------------
*/

var margin = { top: 20, right: 30, bottom: 30, left: 60 },
    width = 800 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;


// generate SVG
var svg = d3.select("body")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");


// Set x,y axis
var x = d3.scaleTime().domain(d3.extent(filteredData, function (d) { return d.date })).range([0, width]);
var y = d3.scaleLinear().domain([20, d3.max(filteredData, function (d) { return d.temperature })]).range([height, 0]);

// Add the X Axis
svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x))
    .append('text')
    .attr('text-anchor', 'middle')
    .text('Date');

// Add the Y Axis
svg.append("g")
    .call(d3.axisLeft(y));

// Draw lines
svg.append("path")
    .datum(filteredData)
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-width", 1.5)
    .attr("d", d3.line()
        .x(function (d) { return x(d.date) })
        .y(function (d) { return y(d.temperature) })
    )

 

console.log("hello world!") // You can see this in the browser console if you run the server correctly
// Don't edit skeleton code!!


d3.csv('data/owid-covid-data.csv')
    .then(data => {

        /*
        -------------------------------------------
        YOUR CODE STARTS HERE

        TASK 1 - Data Processing

        TO-DO-LIST
        1. Exclude data which contain missing values on columns you need
        2. Exclude data all data except the data where the continent is Asia
        3. Calculate the rate of fully vaccinated people, partially vaccinated people, and total rate of vaccinated people
        4. Exclude data where total rate of vaccinated people is over 100%
        5. Exclude all data except the latest data for each country
        6. Sort the data with descending order by total reat of vaccinated people
        7. Extract Top 15 countries
        -------------------------------------------
        */

        // 1) 필요한 칼럼에 결측값이 있는 항목 제거
        //    → continent, location, date, population, people_vaccinated, people_fully_vaccinated 모두 값이 있어야 합니다.
        let processedData = data.filter(d => {
            // d.continent가 truthy인지 확인 (빈 문자열 ''이나 undefined, null 제거)
            // d.location: 국가 이름
            // d.date: 날짜 문자열
            // d.population: 인구 수
            // d.people_vaccinated: 최소 1회 접종 인원
            // d.people_fully_vaccinated: 완전 접종 인원
            return d.continent &&
                   d.location &&
                   d.date &&
                   d.population &&
                   d.people_vaccinated &&
                   d.people_fully_vaccinated;
        });

        // 2) 아시아(Asia) 대륙 데이터만 남기기
        processedData = processedData.filter(d => d.continent === "Asia");
        //    → d.continent가 "Asia"인 항목만 통과

        // 3) 날짜 문자열 → Date 객체, 숫자 문자열 → 숫자로 변환
        processedData = processedData.map(d => {
            return {
                country: d.location,                                 // 국가 이름
                date:    d3.timeParse("%Y-%m-%d")(d.date),           // "2021-01-01" → Date 객체
                pop:     +d.population,                              // 문자열 → 숫자
                vacc1:   +d.people_vaccinated,                       // 최소 1회 접종 인원 숫자
                vaccFull:+d.people_fully_vaccinated                  // 완전 접종 인원 숫자
            };
        });

        // 4) 접종률 계산: totalRate, fullRate, partialRate 추가
        processedData = processedData.map(d => {
            // • totalRate: 1회 이상 접종 비율
            const totalRate   = (d.vacc1 / d.pop) * 100;
            // • fullRate: 완전 접종 비율
            const fullRate    = (d.vaccFull / d.pop) * 100;
            // • partialRate: 1회 접종자 중 미완전 접종(1회 접종만) 비율
            const partialRate = ((d.vacc1 - d.vaccFull) / d.pop) * 100;
            return {
                ...d,
                total_rate:   totalRate,
                full_rate:    fullRate,
                partial_rate: partialRate
            };
        });

        // 5) total_rate가 100% 초과하는 항목 제거
        processedData = processedData.filter(d => d.total_rate <= 100);
        //    → 이상치 제거

        // 6) 나라별로 최신(latest) 데이터만 남기기
        //    → d3.group 으로 country별 배열로 묶은 뒤, reduce로 가장 최근 date 선택
        const latestByCountry = Array.from(
            d3.group(processedData, d => d.country),
            ([country, records]) => {
                return records.reduce((prev, curr) =>
                    prev.date > curr.date ? prev : curr
                );
            }
        );

        // 7) total_rate 기준 내림차순 정렬 후, 상위 15개 추출
        latestByCountry.sort((a, b) => b.total_rate - a.total_rate);
        processedData = latestByCountry.slice(0, 15);
        //    → processedData에 Top15 국가별 최신 데이터가 저장됩니다


        /*
        -------------------------------------------
        YOUR CODE ENDS HERE
        -------------------------------------------
        */

        drawBarChart(processedData);

    })
    .catch(error => {
        console.error(error);
    });

function drawBarChart(data) {

    // Define the screen
    const margin = { top: 5, right: 30, bottom: 50, left: 120 },
        width = 800 - margin.left - margin.right,
        height = 600 - margin.top - margin.bottom;

    // Define the position of the chart
    const svg = d3.select("#chart")
        .append("svg")
        .attr('width', width + margin.left + margin.right)
        .attr('height', height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`);


    /*
    -------------------------------------------
    YOUR CODE STARTS HERE

    TASK 2 - Data processing

    TO-DO-LIST
    1. Create a scale named xScale for x-axis
    2. Create a scale named yScale for x-axis
    3. Define a scale named cScale for color
    4. Process the data for a stacked bar chart
    5. Draw Stacked bars
    6. Draw the labels for bars
    -------------------------------------------
    */

    // 1. Create a scale for x-axis
    // const xScale

    // 2. Create a scale for y-axis
    // const yScale

    // 3. Define a scale for color
    // const cScale

    // 4. Process the data for a stacked bar chart
    // * Hint - Try to utilze d3.stack()
    // const stackedData

    // 5.  Draw Stacked bars

    // 6. Draw the labels for bars

    // 1) X축 스케일: 0%부터 최대 total_rate까지 선형 스케일
    const xScale = d3.scaleLinear()
        .domain([0, d3.max(data, d => d.total_rate)]) // 최소 0, 최대 전체 접종률
        .range([0, width]);                            // 화면 좌우 영역

    // 2) Y축 스케일: 국가 이름을 세로 밴드로 매핑
    const yScale = d3.scaleBand()
        .domain(data.map(d => d.country))             // Top15 국가명 배열
        .range([0, height])                           // 화면 위아래 영역
        .padding(0.2);                                // 바 사이 여백

    // 3) 색상 스케일: 'full_rate'와 'partial_rate'을 색상에 매핑
    const cScale = d3.scaleOrdinal()
        .domain(['full_rate', 'partial_rate'])
        .range(['#7bccc4', '#2b8cbe']);
        // → 완전 접종: 연한 청록, 부분 접종: 진한 파랑

    // 4) 스택 데이터 생성: d3.stack으로 두 속성(full_rate, partial_rate) 스택킹
    const stackGenerator = d3.stack()
        .keys(['full_rate', 'partial_rate']);
    const stackedData = stackGenerator(data);
    // stackedData는 [
    //   [ [x0,x1,data0], [x0,x1,data1], ... ],  // full_rate 시리즈
    //   [ [x0,x1,data0], [x0,x1,data1], ... ]   // partial_rate 시리즈
    // ]

    // 5) 스택형 막대(겹쳐진 바) 그리기
    svg.selectAll(".serie")                // 시리즈 그룹 선택
        .data(stackedData)                 // full_rate, partial_rate 두 개의 배열 바인딩
        .enter()
        .append("g")                       // 각 시리즈마다 <g> 생성
          .attr("class", "serie")
          .attr("fill", d => cScale(d.key))// 시리즈 키에 따른 색 지정
        .selectAll("rect")                // 각 그룹 내부에 rect 그리기
        .data(d => d)                     // 시리즈 하나(d)에 속한 배열 요소 바인딩
        .enter()
        .append("rect")
          .attr("y", d => yScale(d.data.country))        // 해당 국가 위치
          .attr("x", d => xScale(d[0]))                  // 스택 시작 위치
          .attr("height", yScale.bandwidth())            // 밴드 두께만큼 높이
          .attr("width", d => xScale(d[1]) - xScale(d[0]));// 스택 폭

    // 6) 막대 위에 퍼센트 레이블 그리기
    // 6-1) 완전 접종 레이블: 바 안쪽 오른쪽 끝에 흰색 글씨
    svg.selectAll(".label-full")
        .data(data)
        .enter()
        .append("text")
          .attr("class", "label-full")
          .attr("x", d => xScale(d.full_rate) - 4)       // 바 안쪽 끝에서 4px 안쪽
          .attr("y", d => yScale(d.country) + yScale.bandwidth() / 2)
          .attr("dy", "0.35em")                          // 글씨 수직 중앙 정렬
          .attr("text-anchor", "end")                    // 오른쪽 맞춤
          .style("fill", "white")
          .style("font-size", "12px")
          .text(d => d.full_rate.toFixed(1) + "%");

    // 6-2) 부분 접종 레이블: 바 바깥쪽 왼쪽 끝에 검정 글씨
    svg.selectAll(".label-partial")
        .data(data)
        .enter()
        .append("text")
          .attr("class", "label-partial")
          .attr("x", d => xScale(d.total_rate) + 4)      // 전체 스택 끝에서 4px 바깥
          .attr("y", d => yScale(d.country) + yScale.bandwidth() / 2)
          .attr("dy", "0.35em")
          .attr("text-anchor", "start")                  // 왼쪽 맞춤
          .style("fill", "black")
          .style("font-size", "12px")
          .text(d => d.partial_rate.toFixed(1) + "%");

    /*
    -------------------------------------------
    YOUR CODE ENDS HERE
    -------------------------------------------
    */

    // Define the position of each axis
    const xAxis = d3.axisBottom(xScale);
    const yAxis = d3.axisLeft(yScale);

    // Draw axes
    svg.append("g")
        .attr('class', 'x-axis')
        .attr('transform', `translate(0, ${height})`)
        .call(xAxis);

    svg.append("g")
        .attr('class', 'y-axis')
        .call(yAxis)

    // Indicate the x-axis label
    svg.append("text")
        .attr("text-anchor", "end")
        .attr("x", width)
        .attr("y", height + 40)
        .attr("font-size", 17)
        .text("Share of people (%)");

    // Draw Legend
    const legend = d3.select("#legend")
        .append("svg")
        .attr('width', width)
        .attr('height', 70)
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`);

    legend.append("rect").attr('x', 0).attr('y', 18).attr('width', 12).attr('height', 12).style("fill", "#7bccc4")
    legend.append("rect").attr('x', 0).attr('y', 36).attr('width', 12).attr('height', 12).style("fill", "#2b8cbe")
    legend.append("text").attr("x", 18).attr("y", 18).text("The rate of fully vaccinated people").style("font-size", "15px").attr('text-anchor', 'start').attr('alignment-baseline', 'hanging');
    legend.append("text").attr("x", 18).attr("y", 36).text("The rate of partially vaccinated people").style("font-size", "15px").attr('text-anchor', 'start').attr('alignment-baseline', 'hanging');

}