الكثافة السكانية: خارطة حرارية للمملكة بإستخدام D3.js و R
مقدمة
في هذا المقال سنتحدث عن جانب مهم من جوانب تحليل البيانات وهوتصوير البيانات (Data Visualization). وكالعادة سوف نتجنب السطحية في الطرح لذلك راح نركز على قسم محدد من تصوير البيانات كي نتمكن مع إرفاق مثال تطبيقي للموضوع. مقالنا سوف يستعرض “تصوير البيانات الجغرافية المكانية” او ما يعرف بالـ (Geovisualization). هذا النطاق من تصوير البيانات ازدادت شعبيته بشكل كبير في الآونة الأخيرة. حيث بدأت الكثير من الشركات والمؤسسات والحكومات تعتمده في صنع القرار. يا ترى لماذا؟
اهم سبب يعود إلى نقطة بسيطة، وهو رغم التقدم التكنلوجي إلا أن الإداة المألوفة جدا بقيت نفسها وهي الخارطة. منذ كنا صغار كان لدينا خارطة الكنز ثم كبرنا قليل و بدأنا نتابع احوال الطقس بالنظر للخارطة على التلفزيون ثم وصل بنا الأمر أنه بإمكان اي منا إرسال موقعه الحالي عبر برامج التواصل (كمثال WhatsApp). العامل المشترك بين كل تلك المراحل هو أن الخارطة هي بطلة الموقف. قبل أن نبدأ في الموضوع أحب ان اشكر الأخ أسامة على حواره معي حول كيفية الحصول على ملفات الخرائط البيانية وتصويرها. أيضا دافع آخر لهذا المقال هو أنه خلال زيارتي للرياض في شهر نوفمبر حضرت مؤتمر البيانات الضخمة. هناك قام الدكتور آنس الفارس بعرض أحد ابحاث مركز الملك عبد العزيز للتقنية والذي كان يتناول موضوع تحليل وتصوير للبيانات بجميع اصعدتها لمدينة الرياض.
الهدف
هدفنا اليوم بسيط وكلاسيكي وهو تصوير الكثافة السكانية في المملكة حسب المناطق الإدارية الثلاثة عشر بطريقة خارطة حرارية. بالإضافة إلى ذلك، نريد ان تكون تلك الخارطة تفاعلية ايضا.
الأدوات المستخدمة و المهارات المطلوبة
القائمة التالية هي مجموعة من الأدوات التي استخدمتها لتصميم الخارطة التفاعلية. الإلمام بها ولو بشكل بسيط هو أمر آختياري ولكنه سيساعدك كثيرا إلى فهم الكود و طريقة تصميم الخارطة.
- R
- JavaScript
- HTML
- D3.js
- TopoJson.js
- Data : Shapefiles, excel
المصادر:
لرسم هذه الخريطة استعنت بالمصادر التالية
خطة العمل
- جمع البيانات المطلوبة وهي (البيانات الجيوغرافية للمملكة على المستوى المناطق الإدارية، عدد سكان المملكة على مستوى المناطق الإدارية، مساحات المناطق الإدارية)
- دمج البيانات في ملفات بتنسيق TopoJson
- تصميم الخريطة وتهيئتها لتصوير البيانات
- من الجدير بالذكر هو أن رغم بساطة الخريطة التي سنصممها إلا أنه بداية جيدة لشمل معلومات أخرى في الخارطة (مثال: عدد الذكور والإناث لكل منطقة حسب الجنسية).
جمع البيانات
بإمكاننا الحصول على بيانات عدد السكان لكل منطقة بتفاصيلها من موقع الهيئة العامة للإحصاء كذلك يمكننا الحصول على مساحة كل منطقة في المملكة من موقع هيئة المساحة الجيولوجية السعودية. كل ما تبقى لنا هنا هو الحصول على ملف الخارطة الرقمي بصيغة 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
ارجو أنك مستعد لأنه سنقوم بإستعراض بقية الأكواد بشكل اسرع. سوف نتجنب شرح الأجزاء الجمالية والغير محورية هنا نقوم بكتابة الدالة الرئيسية والتي سوف تحوي مجمل الكود للتصوير البياني الذي نأمل به. هذه الدالة سوف تقوم بالتالي:
- تحميل الملف
- تحويلها إلى صيغة GeoJSON عن طريق إستخدام مكتبة Topojson.js
- تحديد مقياس الرسم ووضع المركز في نقطة الأصل (في الوقت الحالي)
- حساب مقياس ومركز الخارطة المناسبان لحجم الخريطة
- جمع كل محتوى الخريطة في مجموعة سنطلق عليها map
- رسم كل محتوى الخريطة وإفاق دالتين خاصتين بعملية اللمس او المرور بالماوس والخروج بالماوس على مناطق المملكة
- حساب الكثافات السكانية تلوين المناطق وفقا لذلك
- رسم المقياس للكثافة السكانية للخريطة
- تلوين مقياس الخريطة
//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);
في دالة المرور بالماوس سوف نقوم بالتالي
- تغير مستوى شفافية الألوان لكل لمنطقة ما عدا الممنطقة المفعلّة
- إستخراج جزء من البيانات وإعادة ترتيبه
- إدراج مجموعة من عناصر الويب للإضافة الرسم البياني الخاص بتصانيف السكان واجناسهم
- رسم الأعمدة البيانية وإضافة البيانات.
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]})
};
في دالة خروج الماوس سوف نقوم بالتالي
- إعادة مستوى شفافية جميع المناطق إلى حالها السابق
- وإعادة إعدادات مقياس الخريطة
- و إزالة آعمدة الرسم البياني
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