نحو مخططات D3.js قابلة للتحديث

نشرت: 2022-03-11

مقدمة

D3.js هي مكتبة مفتوحة المصدر لتصورات البيانات التي طورها مايك بوستوك. D3 تعني المستندات المستندة إلى البيانات ، وكما يوحي اسمها ، تتيح المكتبة للمطورين إنشاء عناصر DOM ومعالجتها بسهولة بناءً على البيانات. على الرغم من عدم تقييده بقدرات المكتبة ، إلا أن D3.js يُستخدم عادةً مع عناصر SVG ويوفر أدوات قوية لتطوير تصورات بيانات المتجه من البداية.

يسمح نمط الرسم البياني القابل للتحديث بجعل مخططات D3.js سهلة
سقسقة

لنبدأ بمثال بسيط. لنفترض أنك تتدرب على سباق 5 كيلومترات ، وتريد عمل مخطط شريطي أفقي لعدد الأميال التي ركضتها كل يوم من الأسبوع الماضي:

 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 ، فقد مر نمط التطوير الخاص بي بتطور ، دائمًا بهدف نهائي يتمثل في إنشاء أكثر تجارب المستخدمين والمستخدمين شمولية. كما سأناقش بمزيد من التفصيل لاحقًا ، قدم نمط مايك بوستوك للمخططات القابلة لإعادة الاستخدام طريقة مجربة وحقيقية لتنفيذ نفس المخطط في أي عدد من التحديدات. ومع ذلك ، تتحقق حدوده بمجرد تهيئة المخطط. إذا كنت أرغب في استخدام انتقالات 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.

لاحظ كيف يتم قياس ارتفاعات وعرض الأشرطة بناءً على حجم البيانات وقيمها. يتم تغيير متغير واحد ، ويتم الاهتمام بالباقي.

الخطوة الثانية: سهولة التكرار من خلال الوظائف

يجب ألا يكون التشفير أبدًا تمرينًا في النسخ واللصق

يجب ألا يكون التشفير أبدًا تمرينًا في النسخ واللصق
سقسقة

من خلال تلخيص بعض منطق الأعمال ، يمكننا إنشاء كود أكثر تنوعًا جاهزًا للتعامل مع قالب معمم من البيانات. والخطوة التالية هي لف هذا الرمز في وظيفة توليد ، مما يقلل التهيئة إلى سطر واحد فقط. تأخذ الوظيفة ثلاث وسيطات: البيانات وهدف 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') . إذا كانت rect موجودة في مكان آخر بالصفحة ، فإن كل المستقيمات الموجودة على الصفحة تصبح جزءًا من التحديد. بدلاً من ذلك ، باستخدام مرجع DOM الذي تم تمريره ، قم بإنشاء كائن svg واحد يمكنك الرجوع إليه عند إلحاق وتحديث العناصر. يمكن لهذه التقنية أيضًا تحسين وقت تشغيل إنشاء المخطط ، حيث إن استخدام مرجع مثل الأشرطة يمنع أيضًا الاضطرار إلى تحديد D3.js مرة أخرى.

الخطوة 3: طريقة التسلسل والاختيارات

في حين أن الهيكل العظمي السابق الذي يستخدم كائنات التكوين شائع جدًا عبر مكتبات JavaScript ، يوصي Mike Bostock ، مبتكر 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 السياق إلى وظيفة المولد. تغلف وظيفة المولد المتغيرات الافتراضية في الإغلاق ، وتسمح للمتصل بتغييرها من خلال تسلسل الأسلوب مع وظائف التكوين التي تعيد كائن المخطط. من خلال القيام بذلك ، يمكن للمتصل عرض نفس الرسم البياني لتحديدات متعددة في وقت واحد ، أو استخدام مخطط واحد لعرض نفس الرسم البياني لتحديدات مختلفة ببيانات مختلفة ، كل ذلك مع تجنب المرور حول كائن خيارات ضخمة.

الخطوة 4: نمط جديد للمخططات القابلة للتحديث

يمنحنا النمط السابق الذي اقترحه مايك بوستوك ، كمطوري الرسم البياني ، الكثير من الطاقة داخل وظيفة المولد. نظرًا لمجموعة واحدة من البيانات وأي تكوينات متسلسلة تم تمريرها ، فإننا نتحكم في كل شيء من هناك. إذا كانت البيانات بحاجة إلى تغيير من الداخل ، فيمكننا استخدام الانتقالات المناسبة بدلاً من مجرد إعادة الرسم من البداية. حتى أشياء مثل تغيير حجم النافذة يمكن التعامل معها بأناقة ، وإنشاء ميزات سريعة الاستجابة مثل استخدام نص مختصر أو تغيير تسميات المحاور.

ولكن ماذا لو تم تعديل البيانات من خارج نطاق وظيفة المولد؟ أو ماذا لو احتاج المخطط إلى تغيير حجمه برمجيًا؟ يمكننا فقط استدعاء وظيفة الرسم البياني مرة أخرى ، مع البيانات الجديدة وتكوين الحجم الجديد. كل شيء سيعاد رسمه ، وفويلا. تم حل المشكلة.

لسوء الحظ ، هناك عدد من المشاكل مع هذا الحل.

أولاً ، نقوم بشكل حتمي تقريبًا بإجراء حسابات تهيئة غير ضرورية. لماذا تتم معالجة البيانات المعقدة عندما يكون كل ما يتعين علينا القيام به هو قياس العرض؟ قد تكون هذه الحسابات ضرورية في المرة الأولى التي يتم فيها تهيئة الرسم البياني ، ولكن بالتأكيد ليس في كل تحديث نحتاج إلى إجرائه. يتطلب كل طلب برمجي بعض التعديل ، وبصفتنا مطورين ، نعرف بالضبط ما هي هذه التغييرات. لا أكثر ولا أقل. علاوة على ذلك ، ضمن نطاق المخطط ، لدينا بالفعل إمكانية الوصول إلى الكثير من الأشياء التي نحتاجها (كائنات 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 لإنشاء تجارب مستخدم وعميل أكثر سلاسة.