كيفية إنشاء مخطط مثل Gantt باستخدام D3 لتصور مجموعة بيانات

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

في هذه المقالة سوف نناقش الجوانب المختلفة لعملية البناء هذه. لتوضيح هذه الجوانب ، سنبني تصوراً مشابهاً لمخطط جانت.

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

الهدف هو إنشاء مخطط يشبه Gantt يشبه المخطط أدناه:

التصور الذي نريد بناءه

كما ترون ، ليس مخطط جانت لأن المهام تبدأ وتنتهي في نفس اليوم.

إنشاء مجموعة البيانات

أنا استخراج البيانات من دقيقة. لكل ملف نصي ، تلقيت معلومات حول المشاريع وحالاتها من الاجتماعات. في البداية ، نظمت بياناتي مثل هذا:

{
    "الاجتماعات": [{
            "التصنيف": "الاجتماع الأول" ،
            "التاريخ": "09/03/2017" ،
            "المشروعات الممثلة": [] ،
            "projects_approved": ["002/2017"]،
            "projects_voting_round_1": ["005/2017"] ،
            "projects_voting_round_2": ["003/2017"، "004/2017"]
        }،
        {
            "التصنيف": "الاجتماع الثاني" ،
            "date_start": "10/03/2017" ،
            "المشروعات_الممثلة": ["006/2017"] ،
            "projects_approved": ["003/2017"، "004/2017"]،
            "projects_voting_round_1": [] ،
            "projects_voting_round_2": ["005/2017"]
        }
    ]
}

دعونا نلقي نظرة فاحصة على البيانات.

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

ما هو الحد الأدنى من المعلومات التي أردت عرضها؟ ما هو الحد الأدنى للعقدة؟ بالنظر إلى الصورة ، إنها معلومات المشروع. لذلك قمت بتغيير بنية البيانات إلى ما يلي:

{
  "مشاريع": [
                  {
                    "الاجتماع": "الاجتماع الأول" ،
                    "النوع": "المشروع" ،
                    "التاريخ": "09/03/2017" ،
                    "التسمية": "المشروع 002/2017" ،
                    "الحالة": "تمت الموافقة"
                  }،
                  {
                    "الاجتماع": "الاجتماع الأول" ،
                    "النوع": "المشروع" ،
                    "التاريخ": "09/03/2017" ،
                    "التسمية": "المشروع 005/2017" ،
                    "الحالة": "vote_round_1"
                  }،
                  {
                    "الاجتماع": "الاجتماع الأول" ،
                    "النوع": "المشروع" ،
                    "التاريخ": "09/03/2017" ،
                    "التسمية": "المشروع 003/2017" ،
                    "الحالة": "vote_round_2"
                  }،
                  {
                    "الاجتماع": "الاجتماع الأول" ،
                    "النوع": "المشروع" ،
                    "التاريخ": "09/03/2017" ،
                    "التسمية": "المشروع 004/2017" ،
                    "الحالة": "vote_round_2"
                  }
               ]
}

وكل شيء يعمل بشكل أفضل بعد ذلك. إنه لأمر مضحك كيف اختفى الإحباط بعد هذا التغيير البسيط.

خلق التصور

الآن وقد أصبح لدينا مجموعة البيانات ، فلنبدأ في إنشاء التصور.

إنشاء المحور السيني

يجب أن يتم عرض كل تاريخ في المحور س. للقيام بذلك ، قم بتعريف d3.timeScale ():

فار timeScale = d3.scaleTime ()
                .domain (d3.extent (مجموعة البيانات ، d => dateFormat (d.date)))
                .range ([0 ، 500]) ؛

يتم إعطاء الحد الأدنى والحد الأقصى للقيم في arrayd3.extent ().

الآن بعد أن أصبح لديك مقياس زمني ، يمكنك الاتصال بالمحور.

var xAxis = d3.axisBottom ()
                .scale (الجدول الزمني)
                .ticks (d3.timeMonth)
                .الحجم (250 ، 0 ، 0)
                .tickSizeOuter (0)؛

يجب أن يكون طول القراد 250 بكسل. لا تريد القراد الخارجي. الكود لعرض المحور هو:

d3.json ("projects.json" ، دالة (خطأ ، بيانات) {
            الرسم البياني (data.projects)؛
})؛
مخطط الوظائف (البيانات) {
    var dateFormat = d3.timeParse ("٪ d /٪ m /٪ Y") ؛
    فار timeScale = d3.scaleTime ()
                   .domain (d3.extent (data، d => dateFormat (d.date)))
                   .range ([0 ، 500]) ؛
    var xAxis = d3.axisBottom ()
                  .scale (الجدول الزمني)
                  .الحجم (250 ، 0 ، 0)
                  .tickSizeOuter (0)؛
    var grid = d3.select ("svg"). إلحاق ('g'). call (xAxis)؛
}

إذا كنت تخطط لهذا ، يمكنك أن ترى أن هناك العديد من القراد. في الواقع ، هناك علامات التجزئة لكل يوم من أيام الشهر. نريد أن نعرض فقط الأيام التي عقدت فيها اجتماعات. للقيام بذلك ، سنقوم بتعيين قيم التجزئة بشكل صريح:

دع dataByDates = d3.nest (). key (d => d.date) .entries (data)؛
اسمحوا tickValues ​​= dataByDates.map (d => dateFormat (d.key)) ؛
var xAxis = d3.axisBottom ()
                .scale (الجدول الزمني)
                .tickValues ​​(tickValues)
                .الحجم (250 ، 0 ، 0)
                .tickSizeOuter (0)؛

باستخدام d3.nest () يمكنك تجميع جميع المشاريع حسب التاريخ (انظر مدى سهولة تكوين البيانات حسب المشاريع؟) ، ثم الحصول على جميع التواريخ ونقلها إلى المحور.

وضع المشاريع

نحتاج إلى وضع المشاريع على طول المحور ص ، لذلك دعونا نحدد مقياسًا جديدًا:

yScale = d3.scaleLinear (). domain ([0، data.length]). range ([0، 250])؛

المجال هو عدد المشاريع. النطاق هو حجم كل علامة. الآن يمكننا وضع المستطيلات:

var projects = d3.select ("svg")
                   .append ( 'ز')
                   .selectAll ( "this_is_empty")
                   .data (البيانات)
                   .أدخل()؛
var innerRects = projects.append ("rect")
              .attr ("rx" ، 3)
              .attr ("ry" ، 3)
              .attr ("x" ، (d ، i) => timeScale (dateFormat (d.date)))
              .attr ("y" ، (d ، i) => yScale (i))
              .attr ("العرض" ، 200)
              .attr ("الارتفاع" ، 30)
              .attr ("السكتة الدماغية" ، "لا شيء")
              .attr ("fill" ، "lightblue") ؛

selectAll () ، والبيانات () ، وأدخل () والتذييل () دائمًا ما يكون خادعًا. لاستخدام طريقة الإدخال () (من أجل إنشاء عقدة جديدة من نقطة بيانات) ، نحتاج إلى تحديد. لهذا السبب نحتاج إلى تحديد الكل ("this_is_empty)" ، حتى لو لم يكن لدينا أي معلومات صحيحة بعد. لقد استخدمت هذا الاسم لتوضيح أننا نحتاج فقط إلى التحديد الفارغ. بمعنى آخر ، نستخدم selectAll ("this_is_empty)" للحصول على تحديد فارغ يمكننا العمل عليه.

تحتوي المشروعات المتغيرة على اختيارات فارغة مرتبطة بالبيانات ، حتى نتمكن من استخدامها لرسم المشاريع في innerRects.

الآن يمكنك أيضًا إضافة ملصق لكل مشروع:

var rectText = projects.append ("text")
                .text (d => d.label)
                .attr ("x"، d => timeScale (dateFormat (d.date)) + 100)
                .attr ("y" ، (d ، i) => yScale (i) + 20)
                .attr ("حجم الخط" ، 11)
                .attr ("مرساة النص" ، "الأوسط")
                .attr ("ارتفاع النص" ، 30)
                .attr ("fill"، "#fff")؛

تلوين كل مشروع

نريد أن يعكس لون كل مستطيل حالة كل مشروع. للقيام بذلك ، لنقم بإنشاء مقياس آخر:

دع dataByCategories = d3.nest (). key (d => d.status) .entries (data)؛
اسمحوا الفئات = dataByCategories.map (d => d.key) .sort ()؛
دع colorScale = d3.scaleLinear ()
             .domain ([0 ، category.length])
             .range (["# 00B9FA" ، "# F95002"])
             .interpolate (d3.interpolateHcl)؛

ومن ثم يمكننا ملء المستطيلات بألوان من هذا المقياس. بتجميع كل ما رأيناه حتى الآن ، إليك الكود:

d3.json ("projects.json" ، دالة (خطأ ، بيانات) {
            الرسم البياني (data.projetos)؛
        })؛
مخطط الوظائف (البيانات) {
    var dateFormat = d3.timeParse ("٪ d /٪ m /٪ Y") ؛
    فار timeScale = d3.scaleTime ()
                   .domain (d3.extent (data، d => dateFormat (d.date)))
                   .range ([0 ، 500]) ؛
  
    دع dataByDates = d3.nest (). key (d => d.date) .entries (data)؛
    اسمحوا tickValues ​​= dataByDates.map (d => dateFormat (d.key)) ؛
  
    دع dataByCategories = d3.nest (). key (d => d.status) .entries (data)؛
    اسمحوا الفئات = dataByCategories.map (d => d.key) .sort ()؛
    دع colorScale = d3.scaleLinear ()
                 .domain ([0 ، category.length])
                 .range (["# 00B9FA" ، "# F95002"])
                 .interpolate (d3.interpolateHcl)؛
  
    var xAxis = d3.axisBottom ()
                .scale (الجدول الزمني)
                .tickValues ​​(tickValues)
                .الحجم (250 ، 0 ، 0)
                .tickSizeOuter (0)؛
    var grid = d3.select ("svg"). إلحاق ('g'). call (xAxis)؛
  
    yScale = d3.scaleLinear (). domain ([0، data.length]). range ([0، 250])؛
  
    var projects = d3.select ("svg")
                   .append ( 'ز')
                   .selectAll ( "this_is_empty")
                   .data (البيانات)
                   .أدخل()؛
  
    var barWidth = 200؛
  
    var innerRects = projects.append ("rect")
                  .attr ("rx" ، 3)
                  .attr ("ry" ، 3)
                  .attr ("x" ، (d ، i) => timeScale (dateFormat (d.date)) - barWidth / 2)
                  .attr ("y" ، (d ، i) => yScale (i))
                  .attr ("العرض" ، عرض النطاق)
                  .attr ("الارتفاع" ، 30)
                  .attr ("السكتة الدماغية" ، "لا شيء")
                  .attr ("fill"، d => d3.rgb (colorScale (classes.indexOf (d.status))))؛
  
    var rectText = projects.append ("text")
                  .text (d => d.label)
                  .attr ("x"، d => timeScale (dateFormat (d.date)))
                  .attr ("y" ، (d ، i) => yScale (i) + 20)
                  .attr ("حجم الخط" ، 11)
                  .attr ("مرساة النص" ، "الأوسط")
                  .attr ("ارتفاع النص" ، 30)
                  .attr ("fill"، "#fff")؛
}

ومع ذلك لدينا بنية الخام لتصورنا.

أحسنت.

إنشاء مخطط قابلة لإعادة الاستخدام

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

هيكل إنشاء مخطط قابل لإعادة الاستخدام هو نفسه دائما. أنا خلقت أداة لتوليد واحدة. في هذا الرسم البياني ، أريد تعيين:

  • البيانات (بالطبع)
  • قيم العرض والارتفاع والهوامش
  • مقياس زمني لقيمة x للمستطيلات
  • مقياس لقيمة y للمستطيلات
  • مقياس للون
  • قيم xScale و yScale و colorScale
  • قيم بداية ونهاية كل مهمة وارتفاع كل شريط

ثم نقل هذا إلى الوظيفة التي قمت بإنشائها:

الرسم البياني: ganttAlikeChart
العرض: 800
الارتفاع: 600
الهامش: {أعلى: 20 ، يمين: 100 ، أسفل: 20 ، يسار: 100}
xScale: d3.scaleTime ()
yScale: d3.scaleLinear ()
colorScale: d3.scaleLinear ()
xValue: d => d.date
colorValue: d => d.status
barHeight: 30
barWidth: 100
dateFormat: d3.timeParse ("٪ d /٪ m /٪ Y")

مما يعطيني هذا:

function ganttAlikeChart () {
العرض = 800 ؛
الارتفاع = 600 ؛
الهامش = {أعلى: 20 ، اليمين: 100 ، أسفل: 20 ، اليسار: 100} ؛
xScale = d3.scaleTime ()؛
yScale = d3.scaleLinear ()؛
colorScale = d3.scaleLinear ()؛
xValue = d => d.date؛
colorValue = d => d.status؛
barHeight = 30 ؛
barWidth = 100 ؛
dateFormat = d3.timeParse ("٪ d /٪ m /٪ Y") ؛
مخطط الوظائف (اختيار) {
 select.each (وظيفة (بيانات) {
  var svg = d3.select (this) .selectAll ("svg"). data ([data]). enter (). append ("svg")؛
  svg.attr ("width"، width + margin.left + margin.right) .attr ("height"، height + margin.top + margin.bottom)؛
  var gEnter = svg.append ("g") ؛
  var mainGroup = svg.select ("g"). attr ("convert"، "translate (" + margin.left + "،" + margin.top + ")")؛
})}
[...]
مخطط العودة ؛
}

الآن نحتاج فقط إلى ملء هذا القالب بالكود الذي أنشأناه من قبل. لقد أجريت أيضًا بعض التغييرات على CSS وأضفت تلميح أدوات.

وهذا كل شيء.

يمكنك التحقق من الكود بأكمله هنا.

شكرا للقراءة!

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