D3.js е библиотека с отворен код за визуализации на данни, разработена от Майк Босток. D3 означава документи, управлявани от данни и както подсказва името му, библиотеката позволява на разработчиците лесно да генерират и манипулират DOM елементи въз основа на данни. Въпреки че не е ограничен от възможностите на библиотеката, D3.js обикновено се използва с SVG елементи и предлага мощни инструменти за разработване на визуализации на векторни данни от нулата.
Моделът на диаграмата, който може да се актуализира, улеснява диаграмите D3.js TweetНека започнем с прост пример. Да предположим, че тренирате за 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 .
Ако този код изглежда познат, това е страхотно. Ако не, намерих Уроци на Скот Мъри да бъде отличен ресурс за започване на работа с D3.js.
Като свободна професия, която е работила стотици часове, разработвайки се с D3.js, моят модел на развитие е преминал през еволюция, винаги с крайна цел да създаде най-изчерпателното изживяване на клиенти и потребители. Както ще обсъдя по-подробно по-късно, Моделът на Майк Босток за диаграми за многократна употреба предложи изпитан и верен метод за внедряване на една и съща диаграма при произволен брой селекции. Ограниченията му обаче се осъзнават, след като диаграмата бъде инициализирана. Ако исках да използвам преходите на D3 и да актуализирам шаблони с този метод, промените в данните трябваше да се обработват изцяло в същия обхват, в който е генерирана диаграмата. На практика това означаваше прилагане на филтри, падащи селектори, плъзгачи и опции за преоразмеряване, всички в един и същ обхват на функцията.
След като многократно изпитвах тези ограничения от първа ръка, исках да създам начин да използвам пълната мощ на D3.js. Например, прослушване за промени в падащо меню на напълно отделен компонент и безпроблемно задействане на актуализации на диаграми от стари данни към нови. Исках да мога да предам контролите на диаграмата с пълна функционалност и да го направя по начин, който е логичен и модулен. Резултатът е актуализируем шаблон на диаграма и ще премина през пълния си напредък към създаването на този модел.
Когато започнах да използвам 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 .
Забележете как се мащабират височините и ширините на лентите въз основа на размера и стойностите на данните. Една променлива се променя, а за останалата част се грижи.
Абстрахирайки част от бизнес логиката, ние сме в състояние да създадем по-гъвкав код, който е готов да обработва обобщен шаблон от данни. Следващата стъпка е да увиете този код във функция за генериране, която намалява инициализацията до само един ред. Функцията приема три аргумента: данните, 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.
Докато предишният скелет, използващ конфигурационни обекти, е много разпространен в JavaScript библиотеките, Майк Босток, създателят на D3.js, препоръчва друг модел за създаване на диаграми за многократна употреба. Накратко, Майк Босток препоръчва внедряване на диаграми като затваряне с методи за получаване на настройки . Въпреки че добавя известна сложност към изпълнението на диаграмата, настройването на опциите за конфигуриране става много лесно за повикващия, като просто се използва верига от методи:
// 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
контекст на функцията генератор. Функцията генератор обгръща променливи по подразбиране в затваряне и позволява на повикващия да ги промени чрез обвързване на метода с конфигурационни функции, които връщат обекта на диаграмата. Правейки това, повикващият може да изобрази една и съща диаграма към множество селекции едновременно или да използва една диаграма, за да изобрази една и съща графика към различни селекции с различни данни, като същевременно избягва преминаването около обемисти обекти с опции.
Предишният модел, предложен от Майк Босток, ни дава като разработчици на диаграми , много мощност в рамките на генераторната функция. Като се има предвид един набор от данни и всички предадени верижни конфигурации, ние контролираме всичко от там. Ако данните трябва да бъдат променени отвътре, можем да използваме подходящи преходи, вместо просто да пречертаем от нулата. Дори неща като преоразмеряване на прозорци могат да се обработват елегантно, създавайки отзивчиви функции като използване на съкратен текст или промяна на етикетите на оста.
Но какво, ако данните се модифицират извън обхвата на функцията генератор? Или какво, ако диаграмата трябва да бъде променена програмно? Можем просто да извикаме отново функцията на диаграмата, с новите данни и новата конфигурация на размера. Всичко ще бъде преначертано и voilà. Проблема решен.
За съжаление има редица проблеми с това решение.
Първо, ние почти неизбежно извършваме ненужно изчисление за инициализация. Защо манипулиране на сложни данни, когато всичко, което трябва да направим, е да мащабираме ширината? Тези изчисления може да са необходими при първото инициализиране на диаграма, но със сигурност не при всяка актуализация, която трябва да направим. Всяка програмна заявка изисква известна модификация и като разработчици ние знаем точно какви са тези промени. Нито повече, нито по - малко. Освен това в рамките на обхвата на диаграмата вече имаме достъп до много неща, от които се нуждаем (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 преход, но съм срещал някои случаи, при които множество преходи се задействат едновременно. В тези случаи обмислете използването на едновременни преходи на родителски и дъщерни елементи, когато създавате функциите си за актуализиране.
Майк Босток представя затваряния като начин за капсулиране на генерирането на диаграми. Неговият модел е оптимизиран за създаване на една и съща диаграма с различни данни на много места. В годините си на работа с D3.js обаче открих малка разлика в приоритетите. Вместо да използвам един екземпляр на диаграма за създаване на една и съща визуализация с различни данни, новият модел, който въведох, позволява на повикващия лесно да създава множество екземпляри на диаграма, всеки от които може да бъде напълно модифициран дори след инициализация. Освен това всяка от тези актуализации се обработва с пълен достъп до текущото състояние на диаграмата, което позволява на разработчика да премахне ненужните изчисления и да използва силата на D3.js, за да създаде по-безпроблемно изживяване на потребителите и клиентите.