D3 | SVG from D3

공부의 기록·2022년 1월 11일
1

Dev 데이터 시각화

목록 보기
5/6
post-thumbnail

D3

본 문서는 2022년 1월 11일 에 작성되었습니다.
일련의 수정과정을 2022년 1월 13일 에 작성되었습니다.

D3.JS 를 사용하기 위하여 다음의 내용을 포함하고 있습니다.

  1. Installation
  2. Error of D3 import
  3. Theory of D3 functions
  4. 실습

Installation

다음의 D3 npm docs 를 참고하였습니다.

D3 를 사용하고 싶은 사용자는 2가지 선택을 고를 수 있습니다.

  1. Node.js 사용자
  2. Vanilla.js 사용자

1번에서 중요한 것은 Node.js, npm 을 이용하여 설치 가능한 자 가아니라,
초기설정을 마친 Node.js 상에서 Javascript 파일을 호출하는 자 를 의미합니다.

따라서 npm install d3 --save 를 해도 사용할 수 없습니다.

Node.js 사용자

단순 연습에 해당하더라도,
본인이 초기 환경설정(nodemon, babel-node 등의 셋팅) 혹은
초기 환경설정을 포함하고 있는 프레임워크(react, nest, next 등)를
사용하고 있다면 이를 사용하는 것을 권고드립니다.

그 이유는,
오픈 소스 라이브러리 특징 상,
별도의 모듈을 따로 다운받지 않고 호출 시마다 웹에서 라이브러리를 호출한다면,
라이브러리의 업데이트 시에 함수가 사라지거나 이름이 바뀌는 등으로 문제가 발생할 것이기 때문입니다.

단,
정말로 누군가에게 보여줄 마음이 없고 빠르게 공부하고 버릴 생각이라면,
외부 스크립트를 index.html 혹은 index.js 에서 호출하는 것도 나쁘지 않다고 생각합니다.

npm install d3

Vanilla JS 사용자

순수한 Vanilla JS 사용자는 required 나 import 를 사용할 방법이 없는 것으로 알고 있습니다. 저는 JQuery 를 배우지 않았고 해당 부분을 배울 생각이 아직 없는 사람이기에 이렇게 알고 있으나, 정확한 지식 아닐 수도 있습니다.

따라서,
외부 스크립트(url) 를 호출하는 다음의 방법을 소개드리고 있습니다.
하기 주소는 2022년 1월 13일 까지 정상 작동하였습니다.

  1. import in HTML File
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="내가 작성할 *.js"></script>

Error

Error of D3 import

Installation 에서 전술한 바와 같이,
저는 react-router-dom@5 >> react-router-dom@6 업데이트 중에
특정 함수가 사라지고 이름이 바뀌는 등의 문제를 겪은 바 있습니다.

그렇기에 모듈로 다운받아서 끌어오고 싶었지만,
node.js(구문법/ES6) 를 사용하지 않고 *.js 모듈 import 를 찾지 못하였습니다.

습관적으로 쓴 다음의 코드는 이어지는 다음의 에러를 일으켰습니다.

import * as d3 from "d3";
Uncaught SyntaxError: Cannot use import statement outside a module

작업환경은 다음과 같았는데, 딱히 작업 환경에 문제가 있다고 보지는 않았습니다.

// file
index.html
index.js
// vsc extensions
Live Server

구글링 결과, required 와 import 과 Node.js 를 통해서 *.js 파일의 모듈을 받아오는 문구라는 것을 알게 되었습니다.

쉣!


Theory

D3 에 대한 기본 이론입니다.

본 문서를 작성하는 과정에서 다음의 블로그 포스트를 참고하였습니다.

평범한 개발자의 이야기 :: d3 중요문법 정리

선택영역

본 도서에 따르면,

선택영역(selections) 는 웹 페이지의 DOM(Documen Object Model; 문서 객체 모델) 중에서
선택된 요소를 의미한다.

말은 어렵게 나와 있는데, Vanilla JS 에 익숙한 사람들의 관점에서 설명하면 아래와 같다.

// D3
cosnt main=d3.select("#main");

// Vanilla JS
const main=docuemnt.querySelector("#main");

단, 명심해야 할 것은 비슷한 것 이지 절대로 동일한 것 이 아니다.
아래 콘솔 창에서의 console.log(객체) 출력 결과를 보면 바로 알 수 있다.

핑프를 위한 한 줄 설명을 덧붙이자면,
위에는 우리가 익히 아는 그런 기능을 하는 객체지만,
아래는 d3 만의 주소값 객체라고 생각하면 편하다.
따라서 parentNode 와 같은 함수가 없다.

select() 외의 다양한 함수는 직접 사용해보거나 1차 API 문서를 확인하자

Append

사실 선택 영역 외에 특별히 이론적으로 새로워진 것은 없다고 생각한다.

그러나,
몇몇 함수는 특색이 있는 부분이 있어서 따로 작성하게 되었다.

append 당신 왜 Vanilla JS 에는 없는 거죠?

Vanilla JS 사용자라면 .js 에서 .html 안으로 HTML Tags 를 생성해서 넣은 경험이 있을 것이라고 생각한다. 이러한 기능은 다음과 같은 코드로 구현이 가능했다.

const parentTag=document.getElementById("HTML 태그 id");

const childTag=document.createElement("HTML 태그 이름(예: p, div, ...)");
childTag.어떤 attributes="넣을 값";
childTag.style.어떤 attirbutes="넣을 값";
parentTag.appendChild(childTag);

그렇다면 d3 에서는 어떻게 다른 것일까?

const parent=d3.select("HTML 태그 id");
const child=parent.append("HTML 태그 이름")
	.attr("속성명","속성값").attr("속성명","속성값");

쉣!
물론, 만능은 아닌 것 같고 사용 결과 다음의 단점도 있었다.

  1. 해당 태그에 대해서 지원되는 변수 및 함수가 나오지 않는다.
    상당히 불편하지만, 간단한 속성만 제어하면 나쁘지 않는다.
    1.1. HTML Tags Attributes 가 나오지 않는다.
    1.2. CSS Tags Attributes 가 나오지 않는다.
  2. 부모->자식으로 가는 것은 편하나, 자식-> 부모로 가는 방법은 모르겠다.
    d3는 러닝키브가 높고 아직 내가 발견하지 못했을 확률이 높다고 생각한다.
  3. 사용자가 그렇게 넓지는 않은 것 같다.
    3.1. 원하는 정보를 찾기가 불편하거나 제대로된 정보가 없다.

그러나 위 단점은 어느 정도 해결책이 있다.

  • CSS 같은 경우는 클래스를 만들어 놓고 attr("class",".className") 과 같이 사용이 가능하다.
  • SVG 나 디자인 등 좁은 부분에만 사용하는 방법도 나쁘지 않을 것 같다.

실습

D3 | SVG from JS 중 # 실습 에서와 같이 이번 예제도 다음을 준수하였습니다.

  1. Data 는 JavaScript 객체 배열로 존재하고 있다.
  2. index.html 에는 오직 <svg id="svg__container"> </svg>만 있다.
  3. Graph 는 JavaScript 로 생성하여 HTML 에 주입해줄 것이다.

위와 같은 룰을 정한 이유는 다음과 같습니다.

  1. SQL, NoSQL 로 받은 데이터 모두 JSON 과 같은 형태로 가공하여 사용, 가장 유사한 객체 배열의 형태를 적용하였다.
  2. 프론트 단에 그래프를 보여줄 영역을 지정하고 그 영역에 그래프를 그리는 연습을 하고 싶다.
  3. Spring 이나 Next.js 등에서 경험한 DI(의존성 주입)과 같이 그래프를 외부에서 주입하고 Javascript 를 통해 숏코딩을 통한 유지보수성을 확보하고 싶었다.

앞선 문서에서,
Vanilla JS 를 이용한 SVG Graph 를 그렸습니다.

따라서,
이번에도 똑같은 가로 막대기 그래프 예제를 D3 로 작성해볼 생각입니다.

# 가로 막대기 그래프

특정 집합군이 퍼센트 정보를 가지고 있는 데이터의 가로 막대기 그래프입니다.
모든 집합군의 퍼센트 총합은 당연히 100% 입니다.

일반적인 가로 막대기 그래프 에서는 다음 절차를 따르면 좋습니다. (Tutorials)

  1. 데이터 제작
  2. SVG Container 작성
  3. 막대기 그리기
  4. 막대기 가로 길이 맞추기(400px)
  5. 막대기 라벨 붙이기
  6. 막대기 축 만들기
  7. 막대기 축 라벨 붙이기
  8. 완성된 그래프를 뒤집기

0단계 | 데이터 제작

const datas=[
    { range: "0-4", value: 9.3 },
    { range: "5-9", value: 8.8 },
    { range: "10-14", value: 8.6 },
    { range: "15-19", value: 8.80 },
    { range: "20-24", value: 8.90 },
    { range: "25-29", value: 8.10 },
    { range: "30-34", value: 7.30 },
    { range: "35-39", value: 7.10 },
    { range: "40-44", value: 6.60 },
    { range: "45-49", value: 6.00 },
    { range: "50-54", value: 5.10 },
    { range: "55-59", value: 4.50 },
    { range: "60-64", value: 3.40 },
    { range: "65-69", value: 2.60 },
    { range: "70-74", value: 2.10 },
    { range: "75-79", value: 1.50 },
    { range: "80 이상", value: 1.60 },
];

1단계 | SVG Container 만들기

D3 를 통해서 SVG 를 주입하기 위하여
SVG Container 를 index.html 에 작성해 둡니다.

<body>
  <svg id="svg__container" />
</body>

2,3단계 | 막대기 그리기 + 가로 길이 맞추기

SVG from JS 와 동일하게 진행하였으나,
2번째 실습인 관계로 동일 성질의 2,3 단계를 한꺼번에 진행하였습니다.

const expandSize=43.1;
const svgTag=d3.select("#svg__container")
	.attr("transform","translate(10,10)");
// document.querySelect("#svg__container") 와 유사하다.

const rectG=svgTag.append("g")
	.attr("transform","translate(80,30)");
// createElement 와 appendChild 가 합쳐진 문장이다.

let count=0;
const rectSize=20;
const rectY=rectSize+5;
// 가로는 20, 확장간격은 20+5 으로 하나의 막대기마다 5만큼의 간격이 벌어진다.

datas.forEach(({range,value})=>{
    rectG.append("rect").attr("x",0).attr("y",rectY*count)
      	.attr("width",value*expandSize).attr("height",rectSize);
  	// 그래프는 x 시작축은 고정 y 시작축이 달라진다.
  	// 또한 width(길이) 는 변동이고 height(높이) 는 고정이다.
  
    count++;
});

4단계 | 막대기 라벨 붙이기

datas 객체 배열의 각 객체인 datas[index].range 를 라벨로 붙여줄 것입니다.

// rectG 변수 밑
const textG=svgTag.append("g")
	.attr("transform","translate(15,51)");

datas.forEahc(({range,value})=>{
  	// rectG.append("rect") 밑
  	textG.append("text").attr("x",0).attr("y",rectY*count)
    	.attr("font-size",16).text(range);
}

5단계 | 막대기 축 그리기

막대기 축을 그리는데,
이 부분은 SVG from JS 와 조금 다를 것이라고 생각한다.
그 이유는 do{ } while(조건) 문과 2.5*widthGap 을 통해서 축을 그리기 보다는,
[0,2.5,5,7.5,10] 의 숫자 배열에 forEach 문을 통해서 축을 그리는 것이 가시적으로 좋다고 생각했기 때문이다.

const percent=[0,2.5,5,7.5,10];
const lineG=svgTag.append("g")
	.attr("transform","translate(80,20)").attr("stroke","black");
// 간혹 line Tags 의 attr(attributes) 를 주어도
// 간혹 각각의 line Tags 에 stroke(선색)을 주거나, 상위 그룹에 stroke 를 주면 된다.

const lineLabelG=svgTag.append("g")
	.attr("transform","translate(75,0)");

6단계 | 막대기 축 라벨 붙이기

// lineG 변수 밑
const lineLabelG=svgTag.append("g")
	.attr("transform","translate(75,0)");
percent.forEach((width)=>{
  	// lineG.append("line") 밑
	lineLabelG.append("text").attr("x",width*expandSize).attr("y",15)
      	.attr("font-size",16).text(`${width===0 ? width : width+"%"}`);
});

7단계 | 완성된 그래프를 뒤집기

SVG from JS 와 동일하게 reverse() 함수를 사용한다.

datas.reverse().forEach(({range,value})=>{
});

최종해설

D3 | SVG from JS 중 최종해설 파트 를 참고하자.
완벽하게 동일하다.

다만 결과물이 조금 다른 점이 있을 수는 있는데,
이 부분은 디자인 적인 부분(면적 차이) 만 있을 뿐이기에 수정하지 않았다.


최종 코드

Github unchpatered // 22-01-03/src/002 svg from d3/datas.js

const datas=[
    { range: "0-4", value: 9.3 },
    { range: "5-9", value: 8.8 },
    { range: "10-14", value: 8.6 },
    { range: "15-19", value: 8.80 },
    { range: "20-24", value: 8.90 },
    { range: "25-29", value: 8.10 },
    { range: "30-34", value: 7.30 },
    { range: "35-39", value: 7.10 },
    { range: "40-44", value: 6.60 },
    { range: "45-49", value: 6.00 },
    { range: "50-54", value: 5.10 },
    { range: "55-59", value: 4.50 },
    { range: "60-64", value: 3.40 },
    { range: "65-69", value: 2.60 },
    { range: "70-74", value: 2.10 },
    { range: "75-79", value: 1.50 },
    { range: "80 이상", value: 1.60 },
];

const expandSize=43.1;
const svgTag=d3.select("#svg__container").attr("transform","translate(10,10)");
const rectG=svgTag.append("g").attr("transform","translate(80,30)");
const textG=svgTag.append("g").attr("transform","translate(15,51)");

let count=0;
const rectSize=20;
const rectY=rectSize+5;
datas.forEach(({range,value})=>{
    rectG.append("rect").attr("x",0).attr("y",rectY*count).attr("width",value*expandSize).attr("height",rectSize);
    textG.append("text").attr("x",0).attr("y",rectY*count).attr("font-size",16).text(range);

    count++;
});

const percent=[0,2.5,5,7.5,10];
const lineG=svgTag.append("g").attr("transform","translate(80,20)").attr("stroke","black");
const lineLabelG=svgTag.append("g").attr("transform","translate(75,0)");
percent.forEach((width)=>{
    lineG.append("line").attr("x1",width*expandSize).attr("y1",5)
                        .attr("x2",width*expandSize).attr("y2",0);
    lineLabelG.append("text").attr("x",width*expandSize).attr("y",15).attr("font-size",16).text(`${width===0 ? width : width+"%"}`);
});


# 세로 막대기 그래프

발상의 전환이 필요한 부분이다.
SVG from JS 가로 막대기 그래프SVG from D3 가로 막대기 그래프 를 하였다면,
같은 데이터를 이용하여 세로 막대기 그래프를 만들어 보자.

충분히 색다른 경험일 것이라고 생각하고
나와는 다른 방법으로 푼 사람도 많을 것이다.



Issue

SVG 는 좌상단에서 다음과 같이 진행하며 객체를 그린다.

  1. 오른쪽 방향
  2. 아래 방향

따라서 baseline(그래프의 끝단) 에 맞춰서 그래프를 그리기 위한 방법이 필요했다.

# 그래프 뒤집기?

CSS 에서 이미지 뒤집기에 대해서 배운 기억이 있다.
그 당시에는 scaleY(-1) 과 같은 코드로 이미지를 상하로 뒤집을 수 있었다.

하지만 d3 에서는 적용되지 않았다.
이것이 d3 인지 svg의 고유 특성인지 알 수는 없었으나.
내가 배운 d3 + svg 교재에서는 원하는 정보를 발견할 수 없었다.

시간은 한정적이었고 나는 꼼수를 찾아야 했다.

# 그래프를 그리지말고 지워서 남겨볼까?

동그라미를 그리는 방법은
빈 도화지에 동그라미를 그리는 방법 뿐만 아니라,
동그라미 부분만 남기고 배경색으로 칠하는 방법도 있다.

데이터 최댓값 만큼 아래로 확장될테고
그 최댓값에 해당하는 길이 + 추가 간격 만큼이 baseline 이 될테니
반복 문을 통해서 최댓값을 찾고 색을 지워나가보자.


최종 코드

계속 반복해서 설명할 필요는 없다고 생각했다.
새로운 함수는 단 하나도 나오지 않았고 위 과정을 따라온 사람이라면,
오히려 주석이 이해를 방해할 것이라고 생각했다.

따라서 최소한의 주석만을 작성하였다.
이 마저도 보기 싫다면 다음의 깃 저장소를 확인하자.

Github unchpatered // 22-01-03/src/003 svg from d3 - 2 /datas.js

// DB 에서 2개의 데이터를 받아왔다고 가정.
const mansOfCustom=[
    { target:"10", gender:"남성", mount:1000 },
    { target:"20", gender:"남성", mount:2000 },
    { target:"30", gender:"남성", mount:2200 },
    { target:"40", gender:"남성", mount:5000 },
    { target:"50", gender:"남성", mount:5500 },
    { target:"60+", gender:"남성", mount:1200 }
];
const womanOfCustom=[
    { target:"10", gender:"여성", mount:300 },
    { target:"20", gender:"여성", mount:4300 },
    { target:"30", gender:"여성", mount:6000 },
    { target:"40", gender:"여성", mount:3000 },
    { target:"50", gender:"여성", mount:2000 },
    { target:"60+", gender:"여성", mount:800 },
];

// 성별 통합 데이터를 제공하고 싶어서 아래의 코드 작성
let allOfCustom=[];
for (let index = 0; index<mansOfCustom.length; index++) {
    // 객체 구조분해할당,
    // const womanMount=womanOfCustom[index].mount; 와 동일
    const { target, mount:manMount }=mansOfCustom[index];
    const { mount:womanMount }=womanOfCustom[index];

    allOfCustom.push({
        target,
        gender:"전체",
        mount:manMount+womanMount
    });
}

// css 기획
// 세로 막대기 그래프로...
const rectSize=20;
const rectX=rectSize+5;
const backColor="white";
const rectColor="#6c5ce7";
// 세 개의 그래프를 가로로 정렬하는데, 밑단을 맞추기 위하여 최댓값 계산
const maxOfGrpah=findMaxMount(allOfCustom);

const svgTag=d3.select("#svg__container");

draw(allOfCustom,{title:"전체 성별 그래프"});
draw(mansOfCustom,{title:"남자 성별 그래프"},200);
draw(womanOfCustom,{title:"여자 성별 그래프"},400);

function draw(array, meta, xExpan=0, yExpan=0) {
    // 막대 그래프 및 라벨 그룹 태그
    const rectG=svgTag.append("g")
    	.attr("transform",`translate(${80+xExpan},${30+yExpan})`);
    const rectLabelG=svgTag.append("g")
    	.attr("transform",`translate(${80+xExpan},${maxOfGrpah/20+yExpan + 45})`);
  
    // 제목
    const rectMeta=svgTag.append("g")
    	.attr("transform",`translate(${80+xExpan + 5},${maxOfGrpah/20+yExpan + 70})`);
    rectMeta.append("text").attr("font-size",20).text(meta.title);

    // 가로선 (2000 mount 마다 선긋기)
    const rectLineG=svgTag.append("g")
    	.attr("transform",`translate(${80+xExpan},${maxOfGrpah/20 + 30})`);

  
  	// 배열의 최댓값을 계산
    const arrayMax=findMaxMount(array);

    // 아래 array.forEach 에서 보면 알겠지만, mount 를 20을 나누어서 사용 중이기에
    // 최대반복수는 arrayMax/2000 이며, 가로선도 -2000/20 간격으로 그어야 한다.
    for (let index=0; index<arrayMax/2000; index++) {
        const yModify=(-2000*index)/20;
        rectLineG.append("line")
            .attr("y1",yModify)
            .attr("y2",yModify)
            .attr("x1",-5)
            .attr("x2",rectX*array.length)
            .attr("stroke","grey");
        
        rectLineG.append("text")
            .attr("y",yModify+7)
            .attr("x",-40)
            .text(2000*index);
    }

    array.forEach(({target, gender, mount},key)=>{
      	// 길이가 세 그래프 최대값 중 제일 큰 값의 길이 만큼 그어져있다.
        // 그 이유는 세 그래프의 밑선을 맞추기 위함이다.
        rectG.append("rect")
            .attr("x",rectX*key).attr("y",0)
            .attr("width",rectSize).attr("height",maxOfGrpah/20)
            .attr("fill",rectColor);
    	
        // 각 막대기의 mount 만큼 뺀값을 다시 최댓값에서 빼고 그 부분에 배경색으로 긋는다.
        rectG.append("rect")
            .attr("x",rectX*key).attr("y",0)
            .attr("width",rectSize).attr("height",(maxOfGrpah - mount)/20)
            .attr("fill",backColor);
    
        rectLabelG.append("text")
            .attr("x",rectX*key).attr("y",0)
            .text(target);
    });
}

// 한 배열의 최댓값을 구하는 간단한 함수
function findMaxMount(array) {
    let maxMount=0;
    array.forEach(({mount})=>{
        if (maxMount<=mount){
            maxMount=mount;
        }
    });
    return maxMount;
}


# 가로 선 그래프


Issue

선이란 무엇인가?
선은 두 점 사이를 잇는 직선이다.

즉, P1(x1,y1)과 P2(x2,y2) 를 연결하는 직선 P1P2 를 구해야 한다. 여기서 x와 y의 값은 다음의 특징을 가진다.

  1. x 는 등차수열로 거리가 멀어진다.
  2. y 는 (datas[index].value, datas[index+1].value) 이다.

최종 코드

Github unchaptered // 22-01-03/src/004 svg from d3 -3/datas.js

const datas=[
    { range: "0-4", value: 9.3 },
    { range: "5-9", value: 8.8 },
    { range: "10-14", value: 8.6 },
    { range: "15-19", value: 8.80 },
    { range: "20-24", value: 8.90 },
    { range: "25-29", value: 8.10 },
    { range: "30-34", value: 7.30 },
    { range: "35-39", value: 7.10 },
    { range: "40-44", value: 6.60 },
    { range: "45-49", value: 6.00 },
    { range: "50-54", value: 5.10 },
    { range: "55-59", value: 4.50 },
    { range: "60-64", value: 3.40 },
    { range: "65-69", value: 2.60 },
    { range: "70-74", value: 2.10 },
    { range: "75-79", value: 1.50 },
    { range: "80 이상", value: 1.60 },
];


/* 선이란 무엇인가?
    선은 두 점 사이를 짓는 직선으로 정의하고자 한다.
    즉, P1(x1, y1) 과 P2(x2, y2) 를 연결하는 직선 P1P2 를 구해야 한다.
    
    우리가 그리고자 하는 가로로 확장하는 선 그래프에서는 x 값은 등차수열이다.
    또한 (y1, y2) 의 값은 (datas[1].value, datas[2].value) 와 같다.

    이 예제에서는 점이 17개 이므로 선은 16개 그려야 한다.
 */
const widthGap=50;
const heightExpan=50;
const svgTag=d3.select("#svg__container");
const maxOfArray=findMaxMount(datas);

const lineG=svgTag.append("g")
    .attr("x",0).attr("y",0)
    .attr("stroke","black")
    .attr("transform",`translate(80,${11*heightExpan})`);
const lineLabelG=svgTag.append("g")
    .attr("x",0).attr("y",0)
    .attr("stroke","grey")
    .attr("transform",`translate(80,${11*heightExpan})`);
const lineZeroG=svgTag.append("g") // 그래프 원점 좌표
    .attr("x",0).attr("y",0)
    .attr("stroke","grey")
    .attr("transform",`translate(80,${11*heightExpan})`);
const linePercentLabelG=svgTag.append("g")
    .attr("x",0).attr("y",0)
    .attr("stroke","grey")
    .attr("transform",`translate(80,${11*heightExpan})`);
for (let index = 0; index<datas.length; index++) {
    if (datas[index+1]!==undefined) {
        const line=lineG.append("line")
            .attr("x1",widthGap*index).attr("x2",widthGap*(index+1))
            .attr("y1",10-datas[index].value*heightExpan)
            .attr("y2",10-datas[index+1].value*heightExpan);
    }
    const lineLabel=lineLabelG.append("text")
        .attr("x",widthGap*index-10)
        .attr("y",40)
        .attr("font-size",13)
        .text(datas[index].range);
    const lineVerticalLabel=lineLabelG.append("line")
        .attr("x1",widthGap*index).attr("x2",widthGap*index)
        .attr("y1",20)
        .attr("y2",10-datas[index].value*heightExpan)
        .attr("font-size",13)
        .attr("stroke-dasharray",3);
}
lineZeroG.append("line")
    .attr("x1",0).attr("x2",widthGap*16)
    .attr("y1",20).attr("y2",20);
lineZeroG.append("line")
    .attr("y1",20).attr("y2",-10*heightExpan)
    .attr("x1",0).attr("x2",0);

const linePercent=[2,4,6,8,10];
linePercent.forEach((percent,key)=>{
    linePercentLabelG.append("text")
        .attr("x",-40)
        .attr("y",-2*(key+1)*heightExpan+8)
        .text(percent+"%");
})
    
function findMaxMount(array) {
    let maxValue=0;
    array.forEach(({value})=>{
        if (maxValue<=value){
            maxValue=value;
        }
    });
    return maxValue;
}


후기

  1. SVG 는 찐짜 어렵다.
  2. D3 는 진짜 진짜 어렵다.
  3. 그리고 디자인이 제일 어렵다.
profile
블로그 이전 : https://inblog.ai/unchaptered

0개의 댓글