ایجاد نمودار دونات (donut) با کتابخانه D3

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

 

کد کامل نمودار:

 

var width = 620,
height = 450,
margin = 40;
 
var radius = Math.min(width, height) / 2 - margin
 
var svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
 
var g = svg.append("g")
.attr("transform", `translate(${width/2},${height/2})`);
 
var data = {"علوم پزشکی": 562, "تامین اجتماعی": 68, "ساير نهادها و ارگان‌ها":93, "خيريه":30, "خصوصی":146}
 
var color = d3.scaleOrdinal()
.domain(["علوم پزشکی", "تامین اجتماعی", "ساير نهادها و ارگان‌ها", "خيريه", "خصوصی"])
.range(d3.schemeDark2);
 
var pie = d3.pie()
.sort(null)
.value(d => d[1])
var data_ready = pie(Object.entries(data))
 
var arc = d3.arc()
.innerRadius(radius * 0.5)
.outerRadius(radius * 0.8)
 
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)
 
g.selectAll('allSlices')
.data(data_ready)
.join('path')
.attr('d', arc)
.attr('fill', d => color(d.data[1]))
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
.append("title")
.text(d => d.data[1]);
 
g.selectAll('allPolylines')
.data(data_ready)
.join('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d)
var posB = outerArc.centroid(d)
var posC = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1);
return [posA, posB, posC]
})
 
g.selectAll('allLabels')
.data(data_ready)
.join('text')
.text(d => d.data[0])
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return `translate(${pos})`;
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
.style("font-size", "13px")
 
svg.node()

 

خروجی کد:

 

 

تنظیمات اولیه و ایجاد SVG

 

ابتدا ابعاد و حاشیه‌های نمودار را تنظیم می‌کنیم. سپس یک عنصر SVG ایجاد می‌کنیم که نمودار دونات درون آن رسم می‌شود. از متد d3.create("svg") استفاده می‌کنیم و عرض، ارتفاع و حاشیه‌های آن را مشخص می‌کنیم.

 

var width = 620,
height = 450,
margin = 40;
 
var radius = Math.min(width, height) / 2 - margin
 
var svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
 
var g = svg.append("g")
.attr("transform", `translate(${width/2},${height/2})`);

 

آماده‌سازی داده‌ها

 

در این بخش، داده‌های مربوط به تعداد بیمارستان‌های ایران بر اساس نوع وابستگی را به صورت یک شیء جاوا اسکریپت تعریف می‌کنیم. این داده‌ها شامل بیمارستان‌های وابسته به علوم پزشکی، تامین اجتماعی، نهادها و ارگان‌های مختلف، خیریه و خصوصی هستند.

 

var data = {"علوم پزشکی": 562, "تامین اجتماعی": 68, "ساير نهادها و ارگان‌ها":93, "خيريه":30, "خصوصی":146}

 

مقیاس رنگ‌ها

 

برای هر دسته از داده‌ها، یک رنگ مجزا تعریف می‌کنیم. از رنگ‌های پیش‌فرض d3.schemeDark2 برای مشخص کردن رنگ هر بخش از نمودار استفاده می‌کنیم.

 

var color = d3.scaleOrdinal()
.domain(["علوم پزشکی", "تامین اجتماعی", "ساير نهادها و ارگان‌ها", "خيريه", "خصوصی"])
.range(d3.schemeDark2);

 

  • d3.scaleOrdinal() یک تابع برای ایجاد یک مقیاس ترتیبی (ordinal scale) در D3.js است. مقیاس‌های ترتیبی به ما اجازه می‌دهند که به هر دسته از داده‌ها (مثلاً دسته‌های متناظر با نام بیمارستان‌ها) یک مقدار مشخص (در اینجا رنگ) اختصاص دهیم. این مقیاس در مقابل مقیاس‌های پیوسته قرار دارد که برای داده‌های عددی استفاده می‌شود.

  • domain() دامنه‌ی مقیاس را مشخص می‌کند، یعنی مقادیر ورودی‌ای که به تابع مقیاس داده می‌شود.

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

  • d3.schemeDark2 یک آرایه از رنگ‌ها است که برای نمایش دسته‌های مختلف داده به کار می‌رود. این مجموعه از رنگ‌ها، رنگ‌های نسبتاً تیره‌ای را ارائه می‌دهد که معمولاً برای دسته‌بندی داده‌ها در نمودارها استفاده می‌شود.

 

ایجاد نمودار دونات

 

در این قسمت از متدهای d3.pie() و d3.arc() برای رسم نمودار استفاده می‌کنیم. d3.pie() داده‌ها را به بخش‌های دایره‌ای تقسیم می‌کند و d3.arc() برای ایجاد بخش‌های قوسی از این داده‌ها استفاده می‌شود.

 

var pie = d3.pie()
.sort(null)
.value(d => d[1])
var data_ready = pie(Object.entries(data))
 
var arc = d3.arc()
.innerRadius(radius * 0.5)
.outerRadius(radius * 0.8)
 
var outerArc = d3.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9)

 

  • d3.pie() یک تابع در D3.js است که داده‌های ورودی را به فرمت مناسب برای رسم نمودار دونات یا پای (pie chart) تبدیل می‌کند.
  • sort(null) به D3 می‌گوید که ترتیب قطعات (slices) در نمودار دونات را تغییر ندهد. این یعنی ترتیب بخش‌های نمودار دونات همان ترتیبی خواهد بود که داده‌ها در آرایه ورودی دارند.
  • value(d => d[1]) مشخص می‌کند که برای هر داده، چه مقداری باید در نظر گرفته شود. در اینجا d[1] به مقدار مربوط به هر دسته اشاره می‌کند. به عنوان مثال، برای دسته‌ی "علوم پزشکی"، این مقدار 562 است. به عبارت دیگر، این تابع مشخص می‌کند که چه مقدار از نمودار به هر دسته اختصاص داده شود.
  • Object.entries(data) داده‌ها را به صورت آرایه‌ای از آرایه‌ها تبدیل می‌کند. هر آرایه شامل دو مقدار است: [نام دسته، مقدار]. برای مثال، یکی از ورودی‌های این تابع به صورت ["علوم پزشکی", 562] خواهد بود.
  • pie(Object.entries(data)) این داده‌ها را به فرمت مناسب برای رسم نمودار دونات تبدیل می‌کند. خروجی این تابع یک آرایه از اشیاء است که هر کدام نمایانگر یک بخش از نمودار است و شامل اطلاعاتی مانند startAngle و endAngle (زاویه شروع و پایان هر بخش) می‌شود.
  • d3.arc() تابعی است که برای تولید مسیرهای قوسی (arc paths) استفاده می‌شود. این مسیرها برای رسم قطعات نمودار دونات به کار می‌روند.
  • innerRadius(radius * 0.5) شعاع داخلی قوس را تعیین می‌کند. این شعاع داخلی باعث می‌شود که نمودار به شکل دونات (با یک حفره در مرکز) باشد. شعاع داخلی در اینجا نصف شعاع کلی تعیین شده است.
  • outerRadius(radius * 0.8) شعاع خارجی قوس را تعیین می‌کند که نشان‌دهنده لبه‌ی بیرونی هر بخش از نمودار دونات است. شعاع خارجی کمی کوچکتر از شعاع کلی انتخاب شده است تا فضا برای اضافه کردن متن یا خطوط خارجی فراهم شود.
  • outerArc برای رسم خطوط اتصال (polylines) استفاده می‌شود که برچسب‌ها را به قطعات دونات متصل می‌کند.
  • innerRadius(radius * 0.9) و .outerRadius(radius * 0.9) هر دو شعاع داخلی و خارجی این قوس را برابر با 90 درصد شعاع کلی تعیین می‌کنند. این بدان معناست که این قوس یک دایره تقریباً کامل است که از آن برای تعیین نقاط شروع و پایان خطوط اتصال استفاده می‌شود.

 

رسم بخش‌های نمودار

 

برای رسم هر بخش از نمودار دونات، از داده‌های آماده‌شده استفاده می‌کنیم و با استفاده از تابع arc() این بخش‌ها را به SVG اضافه می‌کنیم. همچنین، خطوطی که برچسب‌ها را به بخش‌های نمودار متصل می‌کنند و متن برچسب‌ها را نیز اضافه می‌کنیم.

 

g.selectAll('allSlices')
.data(data_ready)
.join('path')
.attr('d', arc)
.attr('fill', d => color(d.data[1]))
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 0.7)
.append("title")
.text(d => d.data[1]);
 
g.selectAll('allPolylines')
.data(data_ready)
.join('polyline')
.attr("stroke", "black")
.style("fill", "none")
.attr("stroke-width", 1)
.attr('points', function(d) {
var posA = arc.centroid(d)
var posB = outerArc.centroid(d)
var posC = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
posC[0] = radius * 0.95 * (midangle < Math.PI ? 1 : -1);
return [posA, posB, posC]
})
 
g.selectAll('allLabels')
.data(data_ready)
.join('text')
.text(d => d.data[0])
.attr('transform', function(d) {
var pos = outerArc.centroid(d);
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1);
return `translate(${pos})`;
})
.style('text-anchor', function(d) {
var midangle = d.startAngle + (d.endAngle - d.startAngle) / 2
return (midangle < Math.PI ? 'start' : 'end')
})
.style("font-size", "13px")

 

  • g.selectAll('allSlices'): ابتدا همه‌ی قطعات (slices) را انتخاب می‌کند. در اینجا، allSlices یک کلاس فرضی است که در واقعیت وجود ندارد، اما D3 این انتخاب را به عنوان نقطه شروع در نظر می‌گیرد.
  • data(data_ready): داده‌های پردازش‌شده توسط تابع pie به این انتخاب متصل می‌شوند. این داده‌ها شامل اطلاعاتی مانند زاویه شروع و پایان هر قطعه هستند.
  • join('path'): برای هر داده یک عنصر path (مسیر) ایجاد می‌کند که نشان‌دهنده‌ی یک قطعه از دونات است.
  • attr('d', arc): مسیر (d attribute) هر قطعه را با استفاده از تابع arc تعیین می‌کند. این تابع با استفاده از شعاع داخلی و خارجی مشخص شده، مسیرهای قوسی را برای هر قطعه تولید می‌کند.
  • attr('fill', d => color(d.data[1])): رنگ هر قطعه را براساس مقدار داده‌های آن قطعه و با استفاده از تابع color تنظیم می‌کند. هر قطعه رنگ مخصوص به خود را دریافت می‌کند.
  • attr("stroke", "white"): حاشیه هر قطعه را سفید تنظیم می‌کند تا قطعات از هم جدا شوند.
  • style("stroke-width", "2px"): عرض حاشیه را به 2 پیکسل تنظیم می‌کند.
  • style("opacity", 0.7): میزان شفافیت (opacity) هر قطعه را به 0.7 تنظیم می‌کند.
  • append("title").text(d => d.data[1]): یک تگ title به هر قطعه اضافه می‌کند که وقتی کاربر موس را روی قطعه قرار می‌دهد، مقدار مربوط به آن قطعه (مانند تعداد بیمارستان‌ها) نمایش داده می‌شود.
  • g.selectAll('allPolylines'): همه خطوط اتصال (polylines) را انتخاب می‌کند.
  • data(data_ready): داده‌های پردازش‌شده را به خطوط اتصال متصل می‌کند.
  • join('polyline'): برای هر قطعه یک عنصر polyline ایجاد می‌کند که برای رسم خطوط اتصال استفاده می‌شود.
  • attr("stroke", "black"): رنگ خط را مشکی تنظیم می‌کند.
  • style("fill", "none"): هیچ رنگ پرکننده‌ای (fill) برای خطوط وجود ندارد.
  • attr("stroke-width", 1): عرض خط را به 1 پیکسل تنظیم می‌کند.
  • attr('points', function(d) {...}): مختصات نقاط برای رسم هر خط اتصال را تعیین می‌کند:
  • posA = arc.centroid(d): مرکز قطعه‌ی قوس (slice) را محاسبه می‌کند.
  • posB = outerArc.centroid(d): مختصات نقطه‌ی میانی در قوس بیرونی (outerArc) را محاسبه می‌کند.
  • posC: نقطه‌ی نهایی خط را روی محور افقی تنظیم می‌کند. این نقطه با توجه به زاویه میانی (midangle) محاسبه می‌شود. اگر زاویه میانی کمتر از π باشد، نقطه در سمت راست قرار می‌گیرد و اگر بیشتر باشد، در سمت چپ.
  • return [posA, posB, posC]: خط اتصال با استفاده از این سه نقطه رسم می‌شود که برچسب‌ها را به قطعات دونات متصل می‌کند.    g.selectAll('allLabels'): همه برچسب‌ها را انتخاب می‌کند.
  • data(data_ready): داده‌های پردازش‌شده را به برچسب‌ها متصل می‌کند.
  • join('text'): برای هر قطعه یک عنصر text ایجاد می‌کند که برچسب مربوط به آن قطعه را نمایش می‌دهد.
  • text(d => d.data[0]): متن هر برچسب را نام دسته مربوطه (مثلاً "علوم پزشکی") تنظیم می‌کند.
  • attr('transform', function(d) {...}): موقعیت هر برچسب را تنظیم می‌کند:
  • pos = outerArc.centroid(d): موقعیت برچسب را براساس مختصات قوس بیرونی (outerArc) تعیین می‌کند.
  • pos[0] = radius * 0.99 * (midangle < Math.PI ? 1 : -1): موقعیت افقی برچسب را تنظیم می‌کند تا به درستی در سمت چپ یا راست قطعه قرار گیرد، بسته به زاویه میانی.
  • return translate(${pos})``: موقعیت نهایی برچسب را با استفاده از تابع translate() تنظیم می‌کند.
  • style('text-anchor', function(d) {...}): تنظیم می‌کند که برچسب‌ها چگونه نسبت به موقعیت خود ترازبندی شوند:
  • اگر زاویه میانی کمتر از π باشد، متن در سمت چپ قرار می‌گیرد ('start')، و در غیر این صورت، در سمت راست ('end').
  • style("font-size", "13px"): اندازه فونت برچسب‌ها را به 13 پیکسل تنظیم می‌کند.

 

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

 

پایه کدها بر گرفته از این آموزش می‌باشد.