업데이트 가능한 D3.js 차트를 향하여

게시 됨: 2022-03-11

소개

D3.js는 Mike Bostock이 개발한 데이터 시각화를 위한 오픈 소스 라이브러리입니다. D3는 데이터 기반 문서를 의미하며 이름에서 알 수 있듯이 라이브러리를 통해 개발자는 데이터를 기반으로 DOM 요소를 쉽게 생성하고 조작할 수 있습니다. 라이브러리의 기능에 제한되지는 않지만 D3.js는 일반적으로 SVG 요소와 함께 사용되며 처음부터 벡터 데이터 시각화를 개발하기 위한 강력한 도구를 제공합니다.

업데이트 가능한 차트 패턴으로 D3.js 차트를 쉽게 만들 수 있습니다.
트위터

간단한 예부터 시작하겠습니다. 5k 경주를 위해 훈련 중이고 지난 주에 매일 달린 마일 수에 대한 가로 막대 차트를 만들고 싶다고 가정합니다.

 var milesRun = [2, 5, 4, 1, 2, 6, 5]; d3.select('body').append('svg') .attr('height', 300) .attr('width', 800) .selectAll('rect') .data(milesRun) .enter() .append('rect') .attr('y', function (d, i) { return i * 40 }) .attr('height', 35) .attr('x', 0) .attr('width', function (d) { return d*100}) .style('fill', 'steelblue');

작동하는 모습을 보려면 bl.ocks.org에서 확인하십시오.

이 코드가 친숙해 보인다면 대단한 것입니다. 그렇지 않다면 Scott Murray의 튜토리얼이 D3.js를 시작하기 위한 훌륭한 리소스라는 것을 알게 되었습니다.

D3.js 개발에 수백 시간을 일한 프리랜서로서 제 개발 패턴은 항상 가장 포괄적인 클라이언트 및 사용자 경험을 만드는 것을 최종 목표로 하는 진화를 거쳤습니다. 나중에 더 자세히 논의하겠지만 재사용 가능한 차트에 대한 Mike Bostock의 패턴은 여러 선택에서 동일한 차트를 구현하기 위한 시도되고 진정한 방법을 제공했습니다. 그러나 차트가 초기화되면 한계가 나타납니다. 이 방법으로 D3의 전환 및 업데이트 패턴을 사용하려면 차트가 생성된 동일한 범위 내에서 데이터 변경을 완전히 처리해야 했습니다. 실제로 이것은 동일한 기능 범위 내에서 필터, 드롭다운 선택, 슬라이더 및 크기 조정 옵션을 구현하는 것을 의미했습니다.

이러한 제한 사항을 반복적으로 경험한 후 D3.js의 모든 기능을 활용할 수 있는 방법을 만들고 싶었습니다. 예를 들어 완전히 분리된 구성 요소의 드롭다운에서 변경 사항을 수신하고 이전 데이터에서 새 데이터로 원활하게 차트 업데이트를 트리거합니다. 모든 기능을 갖춘 차트 컨트롤을 넘겨주고 논리적이고 모듈화된 방식으로 전달하고 싶었습니다. 그 결과 업데이트 가능한 차트 패턴이 생성되며 이 패턴을 만드는 과정을 완전히 살펴보겠습니다.

D3.js 차트 패턴 진행

1단계: 구성 변수

시각화를 개발하기 위해 D3.js를 사용하기 시작하면서 구성 변수를 사용하여 차트의 사양을 빠르게 정의하고 변경하는 것이 매우 편리해졌습니다. 이를 통해 내 차트는 모든 다른 길이와 데이터 값을 처리할 수 있었습니다. 마일 실행을 표시한 동일한 코드가 이제 딸꾹질 없이 더 긴 온도 목록을 표시할 수 있습니다.

 var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68]; var height = 300; var width = 800; var barPadding = 1; var barSpacing = height / highTemperatures.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(highTemperatures); var widthScale = width / maxValue; d3.select('body').append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(highTemperatures) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', 'steelblue');

작동하는 모습을 보려면 bl.ocks.org에서 확인하십시오.

막대의 높이와 너비가 데이터의 크기와 값에 따라 어떻게 조정되는지 확인하십시오. 하나의 변수가 변경되고 나머지는 처리됩니다.

2단계: 함수를 통한 쉬운 반복

코딩은 복사 붙여넣기 연습이 되어서는 안 됩니다.

코딩은 복사 붙여넣기 연습이 되어서는 안 됩니다.
트위터

일부 비즈니스 로직을 추상화하여 일반화된 데이터 템플릿을 처리할 준비가 된 보다 다양한 코드를 만들 수 있습니다. 다음 단계는 이 코드를 생성 함수로 래핑하여 초기화를 한 줄로 줄이는 것입니다. 이 함수는 데이터, DOM 대상 및 기본 구성 변수를 덮어쓰는 데 사용할 수 있는 옵션 개체의 세 가지 인수를 받습니다. 이 작업을 수행하는 방법을 살펴보십시오.

 var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; function drawChart(dom, data, options) { var width = options.width || 800; var height = options.height || 200; var barPadding = options.barPadding || 1; var fillColor = options.fillColor || 'steelblue'; var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(dom).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); } var weatherOptions = {fillColor: 'coral'}; drawChart('#weatherHistory', highTemperatures, weatherOptions); var runningOptions = {barPadding: 2}; drawChart('#runningHistory', milesRun, runningOptions);

작동하는 모습을 보려면 bl.ocks.org에서 확인하십시오.

이 컨텍스트에서 D3.js 선택에 대해 메모하는 것도 중요합니다. d3.selectAll('rect') 과 같은 일반적인 선택은 항상 피해야 합니다. SVG가 페이지의 다른 곳에 있으면 페이지의 모든 rect 가 선택 항목의 일부가 됩니다. 대신 전달된 DOM 참조를 사용하여 요소를 추가하고 업데이트할 때 참조할 수 있는 하나의 svg 개체를 만듭니다. 이 기술은 또한 막대와 같은 참조를 사용하면 D3.js를 다시 선택하지 않아도 되므로 차트 생성 런타임을 개선할 수 있습니다.

3단계: 메서드 체인 및 선택

구성 개체를 사용하는 이전 스켈레톤은 JavaScript 라이브러리에서 매우 일반적이지만 D3.js의 작성자인 Mike Bostock은 재사용 가능한 차트를 만들기 위한 또 다른 패턴을 권장합니다. 간단히 말해서 Mike Bostock은 getter-setter 메서드를 사용하여 차트를 클로저로 구현할 것을 권장합니다. 차트 구현에 약간의 복잡성을 추가하는 동안 구성 옵션을 설정하는 것은 단순히 메서드 체인을 사용하여 호출자에게 매우 간단합니다.

 // Using Mike Bostock's Towards Reusable Charts Pattern function barChart() { // All options that should be accessible to caller var width = 900; var height = 200; var barPadding = 1; var fillColor = 'steelblue'; function chart(selection){ selection.each(function (data) { var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(this).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); }); } chart.width = function(value) { if (!arguments.length) return margin; width = value; return chart; }; chart.height = function(value) { if (!arguments.length) return height; height = value; return chart; }; chart.barPadding = function(value) { if (!arguments.length) return barPadding; barPadding = value; return chart; }; chart.fillColor = function(value) { if (!arguments.length) return fillColor; fillColor = value; return chart; }; return chart; } var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; var runningChart = barChart().barPadding(2); d3.select('#runningHistory') .datum(milesRun) .call(runningChart); var weatherChart = barChart().fillColor('coral'); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

작동하는 모습을 보려면 bl.ocks.org에서 확인하십시오.

차트 초기화는 D3.js 선택을 사용하여 관련 데이터를 바인딩하고 DOM 선택을 this 컨텍스트로 생성기 함수에 전달합니다. 생성기 함수는 클로저에 기본 변수를 래핑하고 호출자가 차트 개체를 반환하는 구성 함수를 사용하여 메서드 연결을 통해 기본 변수를 변경할 수 있도록 합니다. 이렇게 하면 호출자가 동일한 차트를 한 번에 여러 선택 항목에 렌더링하거나 하나의 차트를 사용하여 동일한 그래프를 다른 데이터를 사용하여 다른 선택 항목에 렌더링할 수 있습니다.

4단계: 업데이트 가능한 차트의 새로운 패턴

Mike Bostock이 제안한 이전 패턴은 차트 개발자로서 생성기 기능 내에서 많은 권한을 제공합니다. 한 세트의 데이터와 전달된 연결 구성이 주어지면 거기에서 모든 것을 제어합니다. 데이터를 내부에서 변경해야 하는 경우 처음부터 다시 그리는 대신 적절한 전환을 사용할 수 있습니다. 창 크기 조정과 같은 작업도 우아하게 처리하여 축약된 텍스트를 사용하거나 축 레이블을 변경하는 것과 같은 반응형 기능을 생성할 수 있습니다.

그러나 데이터가 생성기 함수의 범위 외부에서 수정되면 어떻게 될까요? 또는 차트의 크기를 프로그래밍 방식으로 조정해야 하는 경우 어떻게 해야 합니까? 새 데이터와 새 크기 구성으로 차트 함수를 다시 호출할 수 있습니다. 모든 것이 다시 그려질 것이고 짜잔. 문제 해결됨.

불행히도 이 솔루션에는 여러 가지 문제가 있습니다.

먼저 불필요한 초기화 계산을 거의 불가피하게 수행하고 있습니다. 너비를 조정하기만 하면 되는데 왜 복잡한 데이터 조작을 합니까? 이러한 계산은 차트를 처음 초기화할 때 필요할 수 있지만 반드시 업데이트해야 할 때마다 필요한 것은 아닙니다. 각 프로그래밍 방식의 요청에는 약간의 수정이 필요하며 개발자로서 이러한 변경 사항이 무엇인지 정확히 알고 있습니다. 그 이상도 이하도 아닌. 또한 차트 범위 내에서 이미 필요한 많은 항목(SVG 개체, 현재 데이터 상태 등)에 액세스할 수 있으므로 변경 사항을 간단하게 구현할 수 있습니다.

위의 막대 차트 예를 예로 들어 보겠습니다. 너비를 업데이트하고 전체 차트를 다시 그려 그렇게 했다면 최대 데이터 값 찾기, 막대 높이 계산, 이러한 모든 SVG 요소 렌더링과 같은 불필요한 계산이 많이 발생하게 됩니다. 실제로 width 가 새 값에 할당되면 변경해야 할 사항은 다음과 같습니다.

 width = newWidth; widthScale = width / maxValue; bars.attr('width', function(d) { return d*widthScale}); svg.attr('width', width);

하지만 더 좋아집니다. 이제 차트의 일부 기록이 있으므로 D3의 내장 전환을 사용하여 차트를 업데이트하고 쉽게 애니메이션할 수 있습니다. 위의 예를 계속하면 width 에 전환을 추가하는 것은 변경하는 것만 큼 간단합니다.

 bars.attr('width', function(d) { return d*widthScale});

에게

 bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});

더 좋은 점은 사용자가 새 데이터 세트를 전달하도록 허용하면 D3의 업데이트 선택(입력, 업데이트 및 종료)을 사용하여 새 데이터에도 전환을 적용할 수 있다는 것입니다. 그러나 어떻게 새로운 데이터를 허용합니까? 기억하시겠지만, 이전 구현에서는 다음과 같은 새 차트를 만들었습니다.

 d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

데이터를 D3.js 선택 항목에 바인딩하고 재사용 가능한 차트를 호출했습니다. 데이터에 대한 모든 변경은 새 데이터를 동일한 선택 항목에 바인딩하여 수행해야 합니다. 이론적으로 우리는 이전 패턴을 사용하고 기존 데이터에 대한 선택을 조사한 다음 새 데이터로 결과를 업데이트할 수 있습니다. 이것은 구현하기가 복잡하고 복잡할 뿐만 아니라 기존 차트가 동일한 유형 및 형식이라는 가정이 필요합니다.

대신 JavaScript 생성기 함수의 구조를 일부 변경하여 호출자가 메서드 체인을 통해 외부적으로 변경 사항을 쉽게 알릴 수 있는 차트를 만들 수 있습니다. 구성 및 데이터가 설정되고 그대로 유지되기 전에는 차트가 초기화된 후에도 호출자가 다음과 같은 작업을 수행할 수 있습니다.

 weatherChart.width(420);

그 결과 기존 차트에서 새 너비로 원활하게 전환됩니다. 불필요한 계산이 필요 없고 매끄러운 전환으로 고객이 만족할 수 있습니다.

불필요한 계산 없음 + 매끄러운 전환 = 행복한 고객

불필요한 계산 없음 + 매끄러운 전환 = 행복한 고객
트위터

이 추가 기능에는 개발자의 노력이 약간 증가합니다. 그러나 역사적으로 그 시간을 할애할 가치가 있다는 것을 알게 된 노력입니다. 업데이트 가능한 차트의 골격은 다음과 같습니다.

 function barChart() { // All options that should be accessible to caller var data = []; var width = 800; //... the rest var updateData; var updateWidth; //... the rest function chart(selection){ selection.each(function () { // //draw the chart here using data, width // updateWidth = function() { // use width to make any changes }; updateData = function() { // use D3 update pattern with data } }); } chart.data = function(value) { if (!arguments.length) return data; data = value; if (typeof updateData === 'function') updateData(); return chart; }; chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; }; //... the rest return chart; }

완전히 구현된 것을 보려면 bl.ocks.org에서 확인하십시오.

새로운 구조를 검토해 보겠습니다. 이전 클로저 구현에서 가장 큰 변화는 업데이트 기능의 추가입니다. 앞서 논의한 바와 같이 이러한 기능은 D3.js 전환 및 업데이트 패턴을 활용하여 새로운 데이터 또는 차트 구성을 기반으로 필요한 변경을 원활하게 수행합니다. 호출자가 이러한 항목에 액세스할 수 있도록 함수가 차트에 속성으로 추가됩니다. 그리고 더 쉽게 하기 위해 초기 구성과 업데이트 모두 동일한 기능을 통해 처리됩니다.

 chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; };

updateWidth 는 차트가 초기화될 때까지 정의되지 않습니다. undefined 경우 구성 변수는 전역적으로 설정되고 차트 클로저에서 사용됩니다. 차트 함수가 호출되면 모든 전환이 updateWidth 함수로 전달되며, 이 함수는 변경된 width 변수를 사용하여 필요한 변경을 수행합니다. 이 같은:

 updateWidth = function() { widthScale = width / maxValue; bars.transition().duration(1000).attr('width', function(d) { return d*widthScale}); svg.transition().duration(1000).attr('width', width); };

이 새로운 구조를 사용하면 차트의 데이터가 D3.js 선택 항목에 바인딩되는 대신 다른 구성 변수와 마찬가지로 메서드 체인을 통해 전달됩니다. 차이점:

 var weatherChart = barChart(); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

다음이 됩니다.

 var weatherChart = barChart().data(highTemperatures); d3.select('#weatherHistory') .call(weatherChart);

그래서 우리는 약간의 변경을 가하고 약간의 개발자 노력을 추가했습니다. 이점을 살펴보겠습니다.

새로운 기능 요청이 있다고 가정해 보겠습니다. “사용자가 고온과 저온 사이를 변경할 수 있도록 드롭다운을 추가합니다. 그리고 거기에 있는 동안에도 색상을 변경하십시오.” 현재 차트를 지우고 새 데이터를 바인딩하고 처음부터 다시 그리는 대신 이제 낮은 온도가 선택되었을 때 간단한 호출을 할 수 있습니다.

 weatherChart.data(lowTemperatures).fillColor('blue');

그리고 마법을 즐기세요. 계산을 저장할 뿐만 아니라 시각화가 업데이트됨에 따라 이전에는 불가능했던 새로운 수준의 이해력을 시각화에 추가합니다.

여기에서 전환에 대한 중요한 주의가 필요합니다. 동일한 요소에서 여러 전환을 예약할 때는 주의하십시오. 새 전환을 시작하면 이전에 실행 중인 전환이 암시적으로 취소됩니다. 물론 하나의 D3.js 시작 전환에서 여러 속성이나 스타일을 요소에서 변경할 수 있지만 여러 전환이 동시에 트리거되는 경우가 있습니다. 이러한 경우 업데이트 기능을 생성할 때 부모 및 자식 요소에 동시 전환을 사용하는 것이 좋습니다.

철학의 변화

Mike Bostock은 차트 생성을 캡슐화하는 방법으로 클로저를 도입했습니다. 그의 패턴은 여러 위치에서 다른 데이터로 동일한 차트를 생성하는 데 최적화되어 있습니다. 그러나 D3.js를 사용하면서 몇 년 동안 우선 순위에서 약간의 차이를 발견했습니다. 차트의 한 인스턴스를 사용하여 다른 데이터로 동일한 시각화를 만드는 대신, 내가 도입한 새로운 패턴을 사용하면 호출자가 차트의 여러 인스턴스를 쉽게 만들 수 있으며 각 인스턴스는 초기화 후에도 완전히 수정할 수 있습니다. 또한 이러한 각 업데이트는 차트의 현재 상태에 대한 전체 액세스 권한으로 처리되므로 개발자는 불필요한 계산을 제거하고 D3.js의 기능을 활용하여 보다 원활한 사용자 및 클라이언트 경험을 만들 수 있습니다.