ایجاد نقشه حبابی (bubble map) با استفاده از کتابخانه D3

در این مقاله، قصد داریم با استفاده از کتابخانه D3.js یک نقشه حبابی (Bubble Map) براساس جمعیت استان‌های ایران ایجاد کنیم. این نقشه حبابی به ما امکان می‌دهد که توزیع جمعیت در استان‌های مختلف ایران را به صورت بصری و جذاب مشاهده کنیم.

 

کد:

 

var width = 800;
var height = 800;
 
var svg = d3.create("svg")
.attr('width', width)
.attr('height', height)
 
var projection = d3.geoMercator()
.center([54, 32])
.scale(2200)
.translate([width / 2, height / 2]);
 
var path = d3.geoPath().projection(projection);
 
svg.append("g")
.selectAll("path")
.data(iranMap.features)
.enter().append("path")
.attr("d", path)
.attr("fill", "#BFBFBF")
.attr("stroke", "white");
 
iranMap.features.forEach(function(feature) {
var centroid = path.centroid(feature);
feature.properties.centroid = centroid;
});
 
var radiusScale = d3.scaleSqrt()
.domain([0, d3.max(iranPopulation, d => d.population)])
.range([0, 50]);
 
svg.append("g")
.selectAll("circle")
.data(iranPopulation)
.enter().append("circle")
.attr("cx", d => {
var feature = iranMap.features.find(f => f.properties.name === d.province);
return feature ? feature.properties.centroid[0] : 0;
})
.attr("cy", d => {
var feature = iranMap.features.find(f => f.properties.name === d.province);
return feature ? feature.properties.centroid[1] : 0;
})
.attr("r", d => radiusScale(d.population))
.attr("fill", "steelblue")
.attr("opacity", 0.7)
.append("title")
.text(d => `${d.province}: ${d.population.toLocaleString()}`);
 
svg.node()

 

خروجی:

 

 

 تنظیم ابعاد SVG

 

ابتدا ابعاد نقشه را تعیین می‌کنیم:

 

var width = 800;
var height = 800;
 
var svg = d3.create("svg")
.attr('width', width)
.attr('height', height)

 

تنظیم پروجکشن و مسیر نقشه

 

برای نمایش نقشه ایران از پروجکشن `geoMercator` استفاده می‌کنیم و مرکز و مقیاس آن را تنظیم می‌کنیم:

 

var projection = d3.geoMercator()
.center([54, 32])
.scale(2200)
.translate([width / 2, height / 2]);
 
var path = d3.geoPath().projection(projection);

 

 بارگذاری و رسم نقشه ایران

 

با استفاده از داده‌های GeoJSON نقشه ایران را رسم می‌کنیم:

 

svg.append("g")
.selectAll("path")
.data(iranMap.features)
.enter().append("path")
.attr("d", path)
.attr("fill", "#BFBFBF")
.attr("stroke", "white");

 

  • svg.append("g"): این خط یک عنصر گروهی (group) g به عنصر svg اضافه می‌کند. عنصر g به عنوان یک کانتینر برای سایر عناصر SVG عمل می‌کند و به شما امکان می‌دهد تا چندین عنصر را به طور همزمان جابجا یا تغییر دهید.
  • selectAll("path"): این خط تمامی عناصر path موجود در گروه g را انتخاب می‌کند. در این مرحله، هیچ عنصری وجود ندارد زیرا ما تازه عنصر گروهی g را اضافه کرده‌ایم.
  • data(iranMap.features): این خط داده‌های iranMap.features را به عناصر path اتصال می‌دهد. iranMap.features شامل داده‌های GeoJSON برای نقشه ایران است که هر feature نشان‌دهنده یک استان است.
  • enter().append("path"): این خط برای هر داده‌ای که به هیچ عنصری اتصال نیافته، یک عنصر path جدید ایجاد می‌کند. در این مورد، برای هر استان یک عنصر path ایجاد می‌شود.
  • attr("d", path): این خط خصوصیت d را برای هر عنصر path تنظیم می‌کند. خصوصیت d مسیر (path) را تعریف می‌کند و مقدار آن از تابع path که قبلاً تعریف شده بود، به دست می‌آید. تابع path هر ویژگی (feature) را به یک مسیر SVG تبدیل می‌کند که شکل استان را ترسیم می‌کند.
  • attr("fill", "#BFBFBF"): این خط خصوصیت fill را برای هر عنصر path تنظیم می‌کند و رنگ داخلی (پر) استان‌ها را به خاکستری روشن #BFBFBF تغییر می‌دهد.
  • attr("stroke", "white"): این خط خصوصیت stroke را برای هر عنصر path تنظیم می‌کند و رنگ حاشیه (stroke) استان‌ها را سفید تعیین می‌کند. این خط باعث می‌شود که استان‌ها به وضوح از هم جدا شوند.

 

محاسبه مراکز استان‌ها

 

برای قرار دادن حباب‌ها بر روی نقشه، ابتدا مراکز هندسی هر استان را محاسبه می‌کنیم:

 

iranMap.features.forEach(function(feature) {
var centroid = path.centroid(feature);
feature.properties.centroid = centroid;
});

 

  • iranMap.features.forEach(function(feature): این خط یک forEach را بر روی آرایه features از شیء iranMap اجرا می‌کند. iranMap.features شامل تمام ویژگی‌های (استان‌ها) نقشه ایران است. function(feature) یک تابع ناشناس است که برای هر عنصر feature در آرایه features فراخوانی می‌شود.
  • var centroid = path.centroid(feature): این خط مرکز جغرافیایی (centroid) هر ویژگی را محاسبه می‌کند. تابع path.centroid(feature) مرکز (مختصات x و y) هر ویژگی (استان) را به صورت یک آرایه [x, y] برمی‌گرداند. این مختصات مرکز جغرافیایی استان را نشان می‌دهد.
  • feature.properties.centroid = centroid: این خط مختصات مرکز جغرافیایی محاسبه شده را به عنوان یک ویژگی جدید centroid به شیء properties هر ویژگی اضافه می‌کند. بنابراین، هر feature اکنون دارای مختصات مرکز جغرافیایی خود به عنوان feature.properties.centroid است.

 

تنظیم مقیاس اندازه دایره‌ها

 

یک مقیاس برای اندازه دایره‌ها بر اساس جمعیت استان‌ها تعریف می‌کنیم:

 

var radiusScale = d3.scaleSqrt()
.domain([0, d3.max(iranPopulation, d => d.population)])
.range([0, 50]);

 

  • var radiusScale = d3.scaleSqrt(): این خط یک مقیاس جدید از نوع d3.scaleSqrt ایجاد می‌کند. مقیاس scaleSqrt در D3.js برای نقشه‌برداری مقادیر خطی به مقادیر ریشه دوم استفاده می‌شود. این نوع مقیاس برای نمودارهایی که مقادیر زیادی دارند و نیاز به یک توزیع یکنواخت‌تر دارند، مناسب است.
  • domain([0, d3.max(iranPopulation, d => d.population)]): این خط دامنه (domain) مقیاس را تعیین می‌کند. دامنه مقادیر ورودی به مقیاس را مشخص می‌کند.
  • [0, d3.max(iranPopulation, d => d.population)]: دامنه از 0 تا حداکثر جمعیت در داده‌های iranPopulation است.
  • d3.max(iranPopulation, d => d.population): این تابع مقدار حداکثر جمعیت در آرایه iranPopulation را پیدا می‌کند. iranPopulation آرایه‌ای از اشیاء است که هر شیء دارای یک ویژگی population است. تابع d3.max این ویژگی population را برای پیدا کردن بزرگترین مقدار در آرایه جستجو می‌کند.
  • range([0, 50]): این خط محدوده (range) مقیاس را تعیین می‌کند. محدوده مقادیر خروجی از مقیاس را مشخص می‌کند.
  • [0, 50]: مقادیر ورودی در دامنه [0, d3.max(...)] به مقادیر خروجی در محدوده [0, 50] نقشه‌برداری می‌شوند. به عبارت دیگر، جمعیت 0 به شعاع دایره 0 و حداکثر جمعیت به شعاع دایره 50 تبدیل می‌شود. مقادیر میانی نیز به صورت نسبی به شعاع‌های بین 0 و 50 نقشه‌برداری می‌شوند.

 

افزودن دایره‌ها براساس جمعیت

 

دایره‌ها را براساس جمعیت استان‌ها بر روی نقشه اضافه می‌کنیم و موقعیت آنها را بر اساس مراکز هندسی که قبلا محاسبه کردیم، تنظیم می‌کنیم:

 

svg.append("g")
.selectAll("circle")
.data(iranPopulation)
.enter().append("circle")
.attr("cx", d => {
var feature = iranMap.features.find(f => f.properties.name === d.province);
return feature ? feature.properties.centroid[0] : 0;
})
.attr("cy", d => {
var feature = iranMap.features.find(f => f.properties.name === d.province);
return feature ? feature.properties.centroid[1] : 0;
})
.attr("r", d => radiusScale(d.population))
.attr("fill", "steelblue")
.attr("opacity", 0.7)
.append("title")
.text(d => `${d.province}: ${d.population.toLocaleString()}`);

 

در این مرحله، حباب‌ها بر روی نقشه ایران قرار می‌گیرند و اندازه هر حباب براساس جمعیت استان مربوطه تنظیم می‌شود. همچنین با قرار گرفتن ماوس روی هر حباب، نام استان و جمعیت آن به صورت tooltip نمایش داده می‌شود.

 

  • svg.append("g"): یک گروه (<g>) جدید به عنصر svg اضافه می‌کند که در آن تمامی دایره‌ها قرار می‌گیرند. گروه‌ها به شما امکان می‌دهند تا چندین عنصر را با هم دسته‌بندی کنید.
  • selectAll("circle"): انتخاب تمام عناصر circle در گروه جدید. چون در ابتدا هیچ دایره‌ای وجود ندارد، این انتخاب یک انتخاب خالی است.
  • .data(iranPopulation): اتصال داده‌های iranPopulation به دایره‌ها. هر عنصر در iranPopulation به یک دایره تبدیل می‌شود.
  • enter(): ایجاد یک عنصر جدید برای هر داده در iranPopulation که به یک عنصر circle تبدیل نشده است.
  • append("circle"): افزودن یک عنصر circle جدید به گروه برای هر داده.
  • attr("cx", d => {...}): تنظیم موقعیت cx (مختصات x) دایره بر اساس داده‌های متصل.
  • find(f => f.properties.name === d.province): جستجوی ویژگی (feature) در iranMap.features که نام استان (properties.name) با نام استان در داده‌ها (d.province) مطابقت دارد.
  • feature ? feature.properties.centroid[0] : 0: اگر ویژگی یافت شد، مقدار cx برابر با اولین مقدار از مختصات مرکز (centroid) آن ویژگی خواهد بود، در غیر این صورت 0 تنظیم می‌شود.
  • attr("cy", d => {...}): تنظیم موقعیت cy (مختصات y) دایره بر اساس داده‌های متصل.
  • find(f => f.properties.name === d.province): جستجوی ویژگی در iranMap.features که نام استان با نام استان در داده‌ها مطابقت دارد.
  • feature ? feature.properties.centroid[1] : 0: اگر ویژگی یافت شد، مقدار cy برابر با دومین مقدار از مختصات مرکز آن ویژگی خواهد بود، در غیر این صورت 0 تنظیم می‌شود.
  • attr("r", d => radiusScale(d.population)): تنظیم شعاع (radius) دایره بر اساس جمعیت استان. از مقیاس radiusScale که قبلاً تعریف شده استفاده می‌کند تا جمعیت را به شعاع تبدیل کند.
  • attr("fill", "steelblue"): تنظیم رنگ دایره‌ها به steelblue.
  • attr("opacity", 0.7): تنظیم شفافیت دایره‌ها به 0.7 (70%).
  • append("title"): افزودن یک عنصر title به دایره. این عنوان به عنوان یک tooltip نمایش داده می‌شود.
  • .text(d => ${d.province}: ${d.population.toLocaleString()}): تنظیم متن عنوان به نام استان و جمعیت آن به صورت فرمت شده با کاماها.

 

نمایش SVG

 

در نهایت، SVG ایجاد شده را به صفحه اضافه می‌کنیم:

 

 

svg.node()

 

نتیجه نهایی یک نقشه حبابی زیبا و کاربردی است که به ما امکان می‌دهد توزیع جمعیت استان‌های ایران را به صورت بصری مشاهده کنیم. با استفاده از D3.js، این نوع نمودارها به سادگی قابل پیاده‌سازی و سفارشی‌سازی هستند.