الكثافة السكانية: خارطة حرارية للمملكة بإستخدام D3.js و R


جيوغرافي تفاعلي


مقدمة

في هذا المقال سنتحدث عن جانب مهم من جوانب تحليل البيانات وهوتصوير البيانات (Data Visualization). وكالعادة سوف نتجنب السطحية في الطرح لذلك راح نركز على قسم محدد من تصوير البيانات كي نتمكن مع إرفاق مثال تطبيقي للموضوع. مقالنا سوف يستعرض “تصوير البيانات الجغرافية المكانية” او ما يعرف بالـ (Geovisualization). هذا النطاق من تصوير البيانات ازدادت شعبيته بشكل كبير في الآونة الأخيرة. حيث بدأت الكثير من الشركات والمؤسسات والحكومات تعتمده في صنع القرار. يا ترى لماذا؟

اهم سبب يعود إلى نقطة بسيطة، وهو رغم التقدم التكنلوجي إلا أن الإداة المألوفة جدا بقيت نفسها وهي الخارطة. منذ كنا صغار كان لدينا خارطة الكنز ثم كبرنا قليل و بدأنا نتابع احوال الطقس بالنظر للخارطة على التلفزيون  ثم وصل بنا الأمر أنه بإمكان اي منا إرسال موقعه الحالي عبر برامج التواصل (كمثال WhatsApp). العامل المشترك بين كل تلك المراحل هو أن الخارطة هي بطلة الموقف. قبل أن نبدأ في الموضوع أحب ان اشكر الأخ أسامة على حواره معي حول كيفية الحصول على ملفات الخرائط البيانية وتصويرها. أيضا دافع آخر لهذا المقال هو أنه خلال زيارتي  للرياض  في شهر نوفمبر حضرت مؤتمر البيانات الضخمة. هناك قام الدكتور آنس الفارس بعرض أحد ابحاث مركز الملك عبد العزيز للتقنية والذي كان يتناول موضوع تحليل وتصوير للبيانات بجميع اصعدتها لمدينة الرياض.

الهدف

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

الأدوات المستخدمة و المهارات المطلوبة

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

  1. R
  2. JavaScript
  3. HTML
  4. D3.js
  5. TopoJson.js
  6. Data : Shapefiles, excel

المصادر:

لرسم هذه الخريطة استعنت بالمصادر التالية

خطة العمل

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

جمع البيانات

بإمكاننا الحصول على بيانات عدد السكان  لكل منطقة بتفاصيلها من موقع الهيئة العامة للإحصاء كذلك يمكننا الحصول على مساحة كل منطقة في المملكة من موقع هيئة المساحة الجيولوجية السعودية. كل ما تبقى لنا هنا هو الحصول على ملف الخارطة الرقمي بصيغة shapefiles وذلك متوفر على موقع Global Administration Areas المجاني. هذا كل ما يتوجب علينا فعله من ناحية جمع البيانات

تنقيح ودمج البيانات

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

بما أننا سوف نتعامل مع d3.js يجب علينا معرفة اي الصيغ التي تقبلها مكتبة d3. في هذه الحالة الصيغة هي GeoJSON وهي ايضا صيغة بدأت تنمو شعبيتها بشكل كبير. لكن المشكلة هنا هي أن هذه الصيغة ايضا ثقيلة الوزن. طبعا سبب اهمية حجم الملف بالنسبة لنا هو إنعكاس الأداء على تجربة الزائرين للموقع (تخيل تنتظر خمس دقايق علشان تشاهد الخريطة؟؟). بس ولا يهمك،  راح نستخدم إستراتيجية ابداعية نتفادى هذه المعضلة كلها.   الإستراتيجية: ندمج الملفات كلها ونطلعها على صيغة TopoJSON وهي صيغة خفيفة جدا (تختصر لنا ٨٠٪ من حجم الملف الأصلي). بعد رفع الملف الصغير على الموقع نقوم بتحويلها إلى GeoJSON بإستخدام جافا سكريبت والتي تستخدم القوة الحاسوبية للمستخدم نفسه (client-side) … قلت لك ابداع 🙂 اختصار ملفات الأكسل في ملف CSV لتحضيرها للدمج

تحويل ملف shape files إلى TopoJSON  لتحضيرها للدمج. لفعل ذلك نقوم على  Terminal  بتنصيب بعض الحزم المساعدة

sudo apt-get npm sudo npm install topojson@1.6.27 -g

تحويل الملفات

topojson -o SAU0.json -p -- SAU_adm1.shp

دمج الملفات بإستخدام رمز المنطقة

topojson -o SAU_adm1.json -e PopulationData.csv --id-property=Region_ID,ID_1 -p -- SAU0.json

الآن الملف جاهز لرفعه على الويب، إذا ما زبطت معاك الخطوات السابقة بإمكانك تحميل الملف من هنا

كتابة اكواد D3.js

من هنا نبدأ بكتابة اكواد الجافا سكريبت وإستخدام مكتبة d3 الرائعة خلينا في البداية نبدأ بوضع كل المتغيرات التي سوف نستخدمها لصنع نافذة  SVG  عن طريق تحديد الطول والعرض. أيضا سنحدد نوع الإسقاط لخارطتنا وهو في هذه الحالة إسقاط ميركاتور. ايضا سنضع متغير بدون قيمة ليحوي معلومات الملف لاحقا.

var height = 600;
var width = 750;
var projection = d3.geoMercator(); //defining the projection type function
var saudi = void 0 ;

الجدير بالذكر، أنه عند إستخدام مكتبة d3، يمكنك تعيين متغيرات لتحوي التحديد ذاته (CSS Selectors) حتى ولو كان عنصر الويب المحدد غير موجود في الصفحة في حال التحديد. الخطوة التالية هي صنع النافذة (سوف نطلق عليها لوحة الرسم) التي قمنا بتحديد امتداداتها.

var svg = d3.select("#viz")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "SVG");

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

var path = d3.geoPath(projection);
// End of Parameters

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

  1. تحميل الملف
  2. تحويلها إلى صيغة GeoJSON عن طريق إستخدام مكتبة Topojson.js 
  3. تحديد مقياس الرسم ووضع المركز في نقطة الأصل (في الوقت الحالي)
  4. حساب مقياس ومركز الخارطة المناسبان لحجم الخريطة
  5. جمع كل محتوى الخريطة في مجموعة سنطلق عليها map
  6. رسم كل محتوى الخريطة وإفاق دالتين خاصتين بعملية اللمس او المرور بالماوس والخروج بالماوس على مناطق المملكة
  7. حساب الكثافات السكانية تلوين المناطق وفقا لذلك
  8. رسم المقياس للكثافة السكانية للخريطة
  9. تلوين مقياس الخريطة
//Main:
//1) Importing files 
    // We no longer import the data here, refer to the last section of this post to know why. 
  //2) Converting Files
  var states = topojson.feature(data,data.objects.sa);
  //3) Setting up scale and origin
  projection.scale(1).translate([0,0]);             
  //4) Algorithm for Calculating the Scale and Placement ("Translation")
  var b = path.bounds(states);
  var s = 0.95/Math.max((b[1][0]-b[0][0])/width,(b[1][1] - b[0][1]) / height);
  var t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s *(b[1][1] + b[0][1])) / 2];
  // Re-positioning the Map based on the newly calculated scale and center
  projection.scale(s).translate(t);

  //5) Setting up our map   
  var map = svg.append('g').attr('class', 'boundary');
  saudi = map.selectAll('path').data(states.features);

  //6) drawing the map
   saudi = saudi.enter()
      .append('path')
      .attr('d', path)
      .attr('id', geoID)
      .on('mouseover', hover)
      .on('mouseout',onout);

  // Update
  //7 calculating few important variables and color code the regions       
  var popMax  = d3.max(states.features,function(d)
                       {
                        return (d.properties.Population/d.properties.Area);
                       });
  var  popMin = d3.min(states.features,function(d)
                       {
                        return (d.properties.Population/d.properties.Area);
                       });
  var popMedian = d3.median(states.features,function(d)
                       {
                        return (d.properties.Population/d.properties.Area);
                       });

   var ScaleColor = d3.scaleLinear().domain([popMin,popMedian,popMax]).range(["yellow","orange","red"]);

  saudi.style('fill', function(d,i)
      {
          return ScaleColor((d.properties.Population/d.properties.Area))
        }
          );
  saudi
      .transition()
      .duration(500)
      .attr('fill', function(d)
      {
          return ScaleColor((d.properties.Population/d.properties.Area));
      });
           
  //9 Other cosmatic stuff       
  var svgDefs = svg.append('defs');
  var mainGradient = svgDefs.append('linearGradient').attr('id', 'mainGradient');
                  
          mainGradient.append('stop')
              .style('stop-color', ScaleColor(0))
              .attr('offset', '0');
          mainGradient.append('stop')
              .style('stop-color', ScaleColor(popMax))
              .attr('offset', '1');
  // 10 drawing the scale
          var gh = height/100;
          var gw = width/5;
          var gp = 3;
          var ruler = svg.append("g").attr("id", "ruler");
          ruler.attr("transform","translate(" + (width*0.75) + "," + (height*0.10)+")");
          ruler.append('rect')
              .style('fill', "url(#mainGradient)")
              .attr('width', gw)
              .attr('height', gh);
             var xScale = d3.scaleLinear().domain([0,popMax]).range([0,(width/5)]);
             var xAxis = d3.axisBottom(xScale)
                          .tickValues([0,(popMax/4),(popMax/2),(3*popMax/4),popMax])
                          .tickFormat(d3.format("f"));
                           
             ruler.attr("class", "axis").append("text").attr("y", -10).text("عدد السكان لكل كيلو متر مربع")
             .attr("transform","translate("+gw*.5+",0)");
            ruler.attr("class", "axis").call(xAxis);

في دالة المرور بالماوس سوف نقوم بالتالي

  1. تغير مستوى شفافية الألوان لكل لمنطقة ما عدا الممنطقة المفعلّة
  2. إستخراج جزء من البيانات وإعادة ترتيبه
  3. إدراج مجموعة من عناصر الويب للإضافة الرسم البياني الخاص بتصانيف السكان واجناسهم
  4. رسم الأعمدة البيانية وإضافة البيانات.

var hover = function(data) {
  //M 1
      saudi.attr('fill-opacity', 0.2);// Another Updates
      d3.select('#'+geoID(data)).attr('fill-opacity',1);
      d3.select(".axis text").text(data.properties.Province_Name).attr('fill', 'black')
     
  //M 2     
      var englishLables = ["All_Males",
                  "All_Females",
                  "Expat_Males",
                  "Expat_Females",
                  "Saudi_Males",
                  "Saudi_Females"];
      var arabicLabels =  ["الذكور",
                  "الإناث",
                  "الذكور الأجانب",
                  "الإناث الأجانب",
                  "الذكور السعوديين",
                  "الإناث السعوديين"];
                   
                   
      stat = englishLables.map(function (i){ return parseInt(data.properties[i]); });
  //M 3               
      svg.append("g").attr("id", "barChart").attr("transform","translate(" +width*.75+",98)");
      xscale = d3.scaleLinear().domain([0,(stat[0]+stat[1])]).range([0,200]);
       
      var drwaring = d3.select('#barChart').selectAll("rect").data(stat);
      var labels = d3.select('#barChart').selectAll("text").data(stat); 

  //M 4   
             drwaring = drwaring
              .enter()
              .append("rect")
              .attr("height",15)
              .attr("width", 0)
              .attr("x", function(d){return 0})
              .attr("y",function(d,i){ return ( i * 20)})
              .style("fill", function(d,i){
                  if(i%2 === 0){ return  "lightblue"}
                                      else {return  "pink"}              
              })
              drwaring
              .transition().duration(1000).ease(d3.easeQuad)
              .attr("width",  function(d){ return xscale(d) })
   
   
          var formatPercent = d3.format(",.2r");         
               
          labels
              .enter()
              .append("text")
              .style("font-size", 10).style('color', 'black')
              .attr("y",function(d,i){ return i*20+10}) 
              .transition().duration(1000)
              .attr("x", function(d){return xscale(d)+5})
              .tween("text",function(d,a) {
                      var i = d3.interpolate(0,d);
                      var node= d3.select(this);
                      return function(t) {
                      node.text(formatPercent(i(t)/1000000) + " مليون" ) };})

         labels
              .enter()
              .append("text")
              .style("font-size", 10).style('color', 'black')
              .attr("text-anchor", "end") 
              .attr("x", -10)
              .attr("y",function(d,i){ return i*20+10})
              .text(function(d,i){return arabicLabels[i]})
          };

في دالة خروج الماوس سوف نقوم بالتالي

  1. إعادة مستوى شفافية جميع المناطق إلى حالها السابق
  2. وإعادة إعدادات مقياس الخريطة
  3. و إزالة آعمدة الرسم البياني
var onout = function(d){
    //MO 1
    saudi.attr('fill-opacity', 1);
    //MO 2
    d3.select(".axis text").text("عدد السكان لكل كيلو متر مربع");
    //MO 3
    d3.select('#barChart').remove();}
   
## Reading layer `SAU_adm1' from data source `/Users/Hussain/Documents/Arabian Analyst /Arabian_Analyst_Blog/static/data/SAU_adm1.topojson' using driver `TopoJSON'
## Simple feature collection with 13 features and 25 fields
## geometry type:  MULTIPOLYGON
## dimension:      XY
## bbox:           xmin: 34.4943 ymin: 16.09431 xmax: 55.66659 ymax: 32.27078
## epsg (SRID):    NA
## proj4string:    NA

تحديث لهذا المقال

هذا المقال تم كتابته ٢٠١٦ والبيانات الموجودة في ملف json قديمة. لذلك اسوف نقوم بتحديث البيانات عن طريق الآر . هذه الأكواد تكون بعد عملية تحويل الملفات إلى topojson

هنا نقرأ الملف ونحدد نوع الأعمدة تحضيرا للتحديث

filename <- "data.xlsx"
download.file("https://www.stats.gov.sa/sites/default/files/twzy_lskn_fy_lmnzq_ldry.xlsx", destfile = filename)
sa <-topojson_read(here("static", "data", "SAU_adm1.topojson"))
saudi_data <-read_excel(path =filename,range = "A6:K18", col_names = c(
  "adm_area", 
  "occupied_units", 
  "Saudi_Males",
  "Saudi_Females",
  "All_Saudis",
  "Expat_Males",
  "Expat_Females",
  "All_Expats",
  "All_Males",
  "All_Females",
  "Population")) %>%  arrange(match(adm_area,sa$NAME_1))

هنا نحدث البيانات كلها’

for(col_name in colnames(saudi_data)){
  if(col_name %in% colnames(sa)) {
    sa[col_name] = saudi_data[col_name]
  }else{
    next
  }
}

topo_sa <- topojson_json(sa,object_name = "sa")

هنا نجعل البيانات متوفرة في بيئة جافا سكريبت

  cat(
    paste(
    '<script>
      var data = ',topo_sa,';
    </script>'
    , sep="")
  ) 

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


جرب بنفسك

كامل الكود تجده هنا

comments powered by Disqus