본문으로 바로가기

React + D3.js (3) Axes and Scales

category 📈 js 그래픽/react + d3.js 2021. 1. 17. 17:19

Axis(축)와 Scale(데이터 스케일링)

전에 그려본 curved line graph에 axis를 그려주고, Scale을 통해 데이터 스케일링도 진행하여 더욱 가독성 있게 만들어 봅시다. 여기서 이해하고 넘어가야할 것이 axis, scale관련 메서드와 입니다.

 

axis : github.com/d3/d3/blob/master/API.md#axes-d3-axis

scale : github.com/d3/d3/blob/master/API.md#scales-d3-scale

 

 

* 데이터 스케일링

 

d3 공식 문서에 따르면 scale이란 Encodings that map abstract data to visual representation. 입니다.

시각화를 위해 데이터를 추상적인 데이터 변환하는 과정입니다.

 

고등학교 통계 시간을 생각해본다면, StandardScaler (평균 0, 분산 1인 정규분포화), MinMaxScaler, RobustScaler 등을 해주는 것이 스케일링이라고 보시면됩니다.

 

데이터 분석계에서는 데이터의 값이 너무 큰 경우(오버 플로우)혹은 작은 경우(언더 플로우)에 모델 알고리즘 학습과정에서 0으로 수렴하거나 무한으로 발산하는 경우를 방지하기 위해 스케일링을 해준다고 하네요.

 

 

index.d.ts 살펴보기

d3-axis, d3-scale의 정의부분을 살펴보면,

axis객체로 axisBottom, axisTop, axisLeft, axisRight이 존재하고 각 axis 내부에는 역시나 그래프에 빠지지 않는 tick이 있음을 확인할 수 있다.

 

상식적으로 axioBottom은 하단(x축), axisTop은 상단(x축), axisLeft은 좌측(y축), axisRight는 우측(y축)을 그리는데 필요한 메서드라는 것을 짐작할 수 있을 것이다.

/**
 * Constructs a new bottom-oriented axis generator for the given scale, with empty tick arguments,
 * a tick size of 6 and padding of 3. In this orientation, ticks are drawn below the horizontal domain path.
 *
 * @param scale The scale to be used for axis generation.
 */
export function axisBottom<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;

 

 

그리고 d3-axis의 index.d.ts를 확인해보면 모든 axis 생성 메서드들은 scale을 인자로 받는 것을 확인할 수 있습니다.

export function axisTop<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisRight<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisBottom<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;
export function axisLeft<Domain extends AxisDomain>(scale: AxisScale<Domain>): Axis<Domain>;

 

axis 객체는 scale을 인자로 받는 것을 확인할 수 있습니다. 이 뿐만 아니라 별도로 x, y 축 등을 스케일링할 때 사용하기도 합니다. scale은 d3-scale의 index.d.ts 부분을 살펴보시면 Linear Scale Factory에 의해 여러 가지가 생성되어 있는 것을 확인하실 수 있습니다.

 

공식 문서와 함께 참고하면 사용할 수 있는 Scale 들을 살펴볼 수 있습니다.

 

Continuous Scales
연속적인 값으로 스케일링합니다. 가장 직관적이죠.

scaleLinear create a quantitative linear scale.
scalePow create a quantitative power scale.
scaleSqrt create a quantitative power scale with exponent 0.5.
scaleLog create a quantitative logarithmic scale.

Sequential Scales

scaleSequential create a sequential scale.
scaleSequentialLog create a logarithmic sequential scale.
scaleSequentialPow create a power sequential scale.
scaleSequentialSqrt create a power sequential scale with exponent 0.5.

Diverging Scales

scaleDiverging create a diverging scale.


... etc

 

각자 프로젝트의 성격에 맞는 스케일을 적용하시면 됩니다.

역시 시각화는 코딩이 아니라 수학이죠.

 

 

 

Scale 적용하기

 

기본적인 선형 스케일링인 scaleLinear를 활용하여 스케일링을 진행했습니다.

scale 객체를 만들고 해당 값을 Linedml x, y축에 적용해주었습니다.

import { select, line, curveBasis, scaleLinear } from "d3";

function App() {
  const [data, setData] = useState([24, 30, 45, 70, 26, 65, 93]);
  const svgRef = useRef(null);

  useEffect(() => {
    const svg = select(svgRef.current); // selection 객체

    /* 스케일링 */
    // 0이면 0, 6이면 300에 대응되게 scale 설정. 가독성 좋게 scaling 한다고 보면 된다.
    // 여기선 x축을 스케일링하려고 한다.
    const xScale = scaleLinear()
      .domain([0, data.length - 1])
      .range([0, 300]);

    // 웹에서 y는 반전이기 때문에 0을 src viewBox의 세로축 끝으로 지정하고, 가장 큰 값은 0으로 스케일링
    const yScale = scaleLinear().domain([0, 93]).range([300, 0]);

    // line 객체를 만들자
    const myLine = line()
      .x((value, index) => xScale(index)) // x축을 스케일링
      .y((value) => yScale(value)) // 300 - value 라고 직접 값을 변경하는 대신 Scale 활용
      .curve(curveBasis);

    svg
      .selectAll("path")
      .data([data])
      .join((enter) => enter.append("path"))
      .attr("d", (value) => myLine(value))
      .attr("fill", "none")
      .attr("stroke", "red");
  }, [data]);
   ... 중략

좌측이 스케일링 전, 우측이 스케일링 후

 

 

Axis 그리기

 

이건 수학이라기보다는 일종의 tip에 가깝습니다.

import { select, line, curveBasis, axisBottom, scaleLinear } from "d3";

function App() {
  const [data, setData] = useState([24, 30, 45, 70, 26, 65, 93]);
  const svgRef = useRef(null);

  useEffect(() => {
    const svg = select(svgRef.current); // selection 객체

    const xScale = scaleLinear()
      .domain([0, data.length - 1])
      .range([0, 300]);

    const yScale = scaleLinear().domain([0, 93]).range([300, 0]);

    // xAxis 설정. x축 스케일에 대응하게 해주자
    const xAxis = axisBottom(xScale);

    // axis를 그려주기 위해 설정한 g 태그를 선택하여 xAxis를 붙여주자
    // 그리고 맨 위에 축이 붙기 때문에 style로 아래로 내려주자.
    svg.select(".x-axis").style("transform", "translateY(280px)").call(xAxis);

    // line 객체를 만들자
    const myLine = line()
      .x((value, index) => xScale(index)) // x축을 스케일링
      .y((value) => yScale(value)) // 300 - value 라고 직접 값을 변경하는 대신 Scale 활용
      .curve(curveBasis);

    // path 태그 전체를 selectAll 하기 보다 line 클래스를 가진 path만 지정하여 축의 translateY로 같이 이동하지 않게 하자.
    svg
      .selectAll(".line")
      .data([data])
      .join((enter) => enter.append("path"))
      .attr("class", "line")
      .attr("d", (value) => myLine(value))
      .attr("fill", "none")
      .attr("stroke", "red");
  }, [data]);

  return (
    <>
      <svg
        ref={svgRef}
        xmlns="http://www.w3.org/2000/svg"
        width="300"
        height="300"
        viewBox="0 0 300 300"
        version="1.1"
      >
        <g className="x-axis"></g>
      </svg>
    </>
  );
}

 

코드 전체를 가져와서 헷갈릴 수 있는데 chunk마다 떼어봅시다.

 

우선 axios를 가져온 후 설정한 x축 scale을 인자로 넣어 axis를 만들었고,

svg 내부 에서 x-axis라는 클래스를 가진 dom을 가져와서 스타일링을 먹인 후 xAxis를 붙였습니다.

이로서 x 축이 생겼습니다.

import { axisBottom, scaleLinear } from "d3";


// xAxis 설정. x축 스케일에 대응하게 해주자
const xAxis = axisBottom(xScale);

// 만약 tick을 설정하고 싶다면 아래처럼 하면 된다. 
// const xAxis = axisBottom(xScale).ticks(7);

// tick 설정에 각 tick의 형식까지 변형하고 싶다면 아래처럼하자.
// const xAxis = axisBottom(xScale).ticks(7).tickFormat((index) => index + 1);

// axis를 그려주기 위해 설정한 g 태그를 선택하여 xAxis를 붙여주자
// 그리고 맨 위에 축이 붙기 때문에 style로 아래로 내려주자.
svg.select(".x-axis").style("transform", "translateY(280px)").call(xAxis);

 

이렇게 까지 해주면 이상하게도 그래프까지 translateY가 적용되어 내려간 것을 확인할 수 있습니다.

내 그래프 어디갔어

 

 

tick을 설정하면 설정한 갯수 만큼한 tick이 나오는 것을 확인.

 

 

이를 방지하기 위해서 selectAll을 path가 아니라 특정한 클래스를 가진 것만 특정하여 적용합니다.

svg
  .selectAll("path")
  .data([data])
  .join((enter) => enter.append("path"))
  .attr("d", (value) => myLine(value))
  .attr("fill", "none")
  .attr("stroke", "red");
// path 태그 전체를 selectAll 하기 보다 line 클래스를 가진 path만 지정하여 축의 translateY로 같이 이동하지 않게 하자.
svg
  .selectAll(".line")
  .data([data])
  .join((enter) => enter.append("path"))
  .attr("class", "line")
  .attr("d", (value) => myLine(value))
  .attr("fill", "none")
  .attr("stroke", "red");

 

네, 이제 되었군요.

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체