مقدمة في D3.js

D3.js (أو Data-Driven Documents) هي مكتبة JavaScript قوية لتصور البيانات وإنشاء رسومات تفاعلية في متصفح الويب. تتيح للمطورين إنشاء تصورات بيانية معقدة وتفاعلية باستخدام معايير الويب الحديثة مثل SVG وHTML5 وCSS.

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

المفاهيم الأساسية في D3.js

  • التحديد والتلاعب بالعناصر - اختيار وتعديل عناصر DOM
  • ربط البيانات - ربط البيانات بعناصر DOM
  • التحويلات - تحويل وتحديث عناصر DOM بناءً على البيانات
  • المقاييس - تحويل البيانات إلى قيم مرئية
  • الرسوم المتحركة - إنشاء انتقالات سلسة بين حالات مختلفة

البداية مع D3.js

تثبيت D3.js

يمكنك تضمين D3.js في مشروعك بطرق مختلفة:

1. باستخدام CDN (أسهل طريقة)

<!-- آخر إصدار (v7) -->
<script src="https://d3js.org/d3.v7.min.js"></script>

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

2. باستخدام npm (للمشاريع التي تستخدم Node.js)

npm install d3

ثم استيراد D3 في ملفات JavaScript الخاصة بك:

import * as d3 from 'd3';
// أو استيراد وحدات محددة فقط
import { select, selectAll } from 'd3';

هذه الطريقة مناسبة للمشاريع التي تستخدم أدوات بناء مثل Webpack أو Rollup، حيث تتيح استيراد المكتبة كاملة أو أجزاء محددة منها فقط لتقليل حجم الملف النهائي.

3. تنزيل الملفات مباشرة

يمكنك تنزيل ملفات D3.js من صفحة الإصدارات وتضمينها مباشرة:

<script src="path/to/d3.min.js"></script>

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

مثال D3.js الأول

لنبدأ بمثال بسيط لإنشاء رسم دائري:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>مثال D3.js الأول</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
    <div id="visualization"></div>
    
    <script>
        // إنشاء عنصر SVG
        const svg = d3.select("#visualization")
            .append("svg")
            .attr("width", 500)
            .attr("height", 300);
        
        // إنشاء دائرة
        svg.append("circle")
            .attr("cx", 250)
            .attr("cy", 150)
            .attr("r", 100)
            .attr("fill", "steelblue");
    </script>
</body>
</html>

شرح المثال سطرًا بسطر

  1. script src="https://d3js.org/d3.v7.min.js" - تضمين مكتبة D3.js في المستند
  2. div id="visualization" - إنشاء حاوية للرسم البياني
  3. d3.select("#visualization") - تحديد عنصر DOM باستخدام D3
  4. append("svg") - إضافة عنصر SVG (رسم متجهي) إلى العنصر المحدد
  5. attr("width", 500) - تعيين عرض عنصر SVG
  6. attr("height", 300) - تعيين ارتفاع عنصر SVG
  7. svg.append("circle") - إضافة دائرة إلى عنصر SVG
  8. attr("cx", 250) - تعيين إحداثي x لمركز الدائرة
  9. attr("cy", 150) - تعيين إحداثي y لمركز الدائرة
  10. attr("r", 100) - تعيين نصف قطر الدائرة
  11. attr("fill", "steelblue") - تعيين لون ملء الدائرة

تحديد العناصر

واحدة من أهم وظائف D3.js هي القدرة على تحديد وتلاعب بعناصر DOM. تعتبر التحديدات (Selections) حجر الأساس لمعظم عمليات D3.

طرق التحديد الأساسية

// تحديد عنصر واحد باستخدام محدد CSS
const paragraph = d3.select("p");

// تحديد جميع العناصر المطابقة باستخدام محدد CSS
const allParagraphs = d3.selectAll("p");

// تحديد عنصر باستخدام معرف (ID)
const chart = d3.select("#chart");

// تحديد عناصر باستخدام فئة (Class)
const bars = d3.selectAll(".bar");

// تحديد باستخدام سمة (Attribute)
const inputs = d3.selectAll("input[type=checkbox]");

شرح أساسيات التحديد

  • d3.select(selector) - يحدد أول عنصر مطابق لمحدد CSS المعطى.
  • d3.selectAll(selector) - يحدد جميع العناصر المطابقة لمحدد CSS المعطى.
  • المحدد يمكن أن يكون اسم عنصر أو معرف أو فئة أو أي محدد CSS صالح.
  • ناتج التحديد يكون كائن من نوع Selection يقدم واجهة برمجية غنية للتلاعب بالعناصر.

التلاعب بالعناصر المحددة

بعد تحديد العناصر، يمكنك استخدام مجموعة متنوعة من الطرق للتعامل معها:

// تعديل نص العنصر
d3.select("#header").text("عنوان جديد");

// تعديل سمات العنصر
d3.select("circle")
  .attr("r", 30)               // تغيير نصف القطر
  .attr("fill", "red")         // تغيير اللون
  .attr("stroke", "black")     // إضافة حدود
  .attr("stroke-width", 2);    // تعيين عرض الحدود

// تعديل نمط CSS
d3.selectAll(".item")
  .style("color", "blue")
  .style("font-size", "14px")
  .style("opacity", 0.8);

// إضافة وحذف الفئات (Classes)
d3.select("div")
  .classed("active", true)     // إضافة فئة
  .classed("hidden", false);   // إزالة فئة

// استخدام الدوال كقيم للسمات
d3.selectAll("circle")
  .attr("cx", function(d, i) {
    return i * 100 + 50;       // تحديد الموضع بناءً على فهرس العنصر
  });

شرح طرق التلاعب بالعناصر

  • .text(value) - تعيين أو تغيير محتوى النص للعناصر المحددة.
  • .attr(name, value) - تعيين أو تغيير قيمة سمة معينة للعناصر المحددة.
  • .style(name, value) - تعيين أو تغيير خاصية CSS للعناصر المحددة.
  • .classed(name, boolean) - إضافة أو إزالة فئة CSS من العناصر المحددة.
  • .property(name, value) - تعيين خصائص DOM مثل checked و value.

إنشاء وحذف العناصر

// إنشاء عناصر جديدة
d3.select("#container")
  .append("div")               // إضافة عنصر div
  .attr("class", "new-item")  
  .text("عنصر جديد");

// إدراج عناصر قبل عناصر أخرى
d3.select("#list")
  .insert("li", ":first-child") // إدراج قبل العنصر الأول
  .text("عنصر في البداية");

// نسخ عناصر
d3.select(".template")
  .clone(true)                 // نسخ العنصر مع الأحداث المرتبطة به
  .attr("class", "copy");

// حذف عناصر
d3.select(".temp").remove();   // حذف العنصر من DOM

شرح طرق إنشاء وحذف العناصر

  • .append(type) - إضافة عنصر جديد كآخر عنصر ابن للعناصر المحددة.
  • .insert(type, before) - إدراج عنصر جديد قبل عنصر محدد.
  • .clone(deep) - نسخ العناصر المحددة (deep تحدد ما إذا كان سيتم نسخ الأحداث والبيانات).
  • .remove() - إزالة العناصر المحددة من DOM.

سلاسل التحديد (Method Chaining)

ميزة أساسية في D3 هي إمكانية تسلسل الطرق، حيث تعيد معظم طرق التحديد كائن التحديد نفسه:

// سلسلة من العمليات على تحديد واحد
d3.select("#chart")
  .append("svg")
  .attr("width", 600)
  .attr("height", 400)
  .append("rect")
  .attr("x", 50)
  .attr("y", 50)
  .attr("width", 200)
  .attr("height", 100)
  .attr("fill", "orange")
  .attr("stroke", "black")
  .attr("stroke-width", 2);

ملاحظة هامة عن تسلسل الطرق

عند استخدام .append() أو طرق مشابهة، يتغير سياق التحديد الحالي. في المثال أعلاه:

  • يبدأ بتحديد #chart
  • بعد .append("svg")، يصبح التحديد هو عنصر SVG الجديد
  • بعد .append("rect")، يصبح التحديد هو عنصر Rectangle الجديد

ربط البيانات (Data Binding)

ربط البيانات هو جوهر D3.js. يتيح لك ربط مجموعة من البيانات بعناصر DOM، ثم التلاعب بخصائص هذه العناصر استنادًا إلى البيانات المرتبطة بها.

الربط الأساسي للبيانات

// مجموعة بيانات بسيطة
const data = [5, 10, 15, 20, 25];

// ربط البيانات بعناصر
d3.selectAll("p")
  .data(data)
  .text(function(d) {
    return "القيمة هي: " + d;
  });

شرح الربط الأساسي للبيانات

  • .data(values) - يربط مصفوفة البيانات المعطاة بالعناصر المحددة.
  • بعد ربط البيانات، يمكن الوصول إلى قيمة البيانات لكل عنصر من خلال معلمة d في دالة.
  • المعلمة الثانية i تمثل فهرس العنصر في المجموعة.

Enter, Update, Exit

أحد المفاهيم الأساسية في D3 هو نمط Enter/Update/Exit. يتيح لك التعامل بشكل مختلف مع الحالات التالية:

  • Enter: عندما يكون هناك عناصر بيانات أكثر من عناصر DOM المتاحة.
  • Update: عناصر البيانات التي لها عناصر DOM مقابلة.
  • Exit: عناصر DOM التي لم تعد لها عناصر بيانات مقابلة.
// مجموعة بيانات
const data = [10, 20, 30, 40, 50];

// تحديد الحاوية
const container = d3.select("#container");

// ربط البيانات بعناصر div حالية أو محتملة
const selection = container.selectAll("div")
    .data(data);

// Enter: إنشاء عناصر جديدة للبيانات التي ليس لها عناصر مقابلة
selection.enter()
    .append("div")
    .attr("class", "bar")
    .style("height", d => d + "px")
    .style("width", "50px")
    .style("background-color", "steelblue")
    .style("margin", "5px")
    .text(d => d);

// Update: تحديث العناصر الموجودة
selection
    .style("background-color", "orange")
    .text(d => "قيمة محدثة: " + d);

// Exit: إزالة العناصر الزائدة
selection.exit()
    .style("background-color", "red")
    .transition()
    .duration(500)
    .style("opacity", 0)
    .remove();

شرح نمط Enter/Update/Exit

  • .enter() - يعيد تحديدًا يمثل العناصر التي يجب إنشاؤها.
  • .exit() - يعيد تحديدًا يمثل العناصر التي يجب إزالتها.
  • التحديد الأصلي (بعد .data()) يمثل العناصر التي تحتاج إلى تحديث.
  • هذا النمط يتيح إنشاء تحديثات ديناميكية للرسوم البيانية عند تغير البيانات.

مثال عملي: إنشاء مخطط أعمدة بسيط

// البيانات
const data = [50, 80, 120, 160, 200, 140, 90];

// إنشاء عنصر SVG
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", 500)
    .attr("height", 300);

// تحديد خصائص الرسم البياني
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 500 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;

// إنشاء مجموعة (g) مع هامش
const g = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// إنشاء مقياس x (المجال والنطاق)
const x = d3.scaleBand()
    .domain(d3.range(data.length))
    .range([0, width])
    .padding(0.1);

// إنشاء مقياس y
const y = d3.scaleLinear()
    .domain([0, d3.max(data)])
    .range([height, 0]);

// إنشاء الأعمدة
g.selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("x", (d, i) => x(i))
    .attr("y", d => y(d))
    .attr("width", x.bandwidth())
    .attr("height", d => height - y(d))
    .attr("fill", "steelblue");

المقاييس (Scales)

المقاييس هي دوال تحويل تساعد في تحويل البيانات إلى قيم مرئية (مثل الإحداثيات أو الألوان). تعتبر أساسية في D3 لأنها تساعد في تمثيل البيانات بشكل مرئي مناسب.

أنواع المقاييس الأساسية

// مقياس خطي
const linearScale = d3.scaleLinear()
    .domain([0, 100])      // مجال البيانات الأصلي (المدخلات)
    .range([0, 500]);      // نطاق القيم الناتجة (المخرجات)

// استخدام المقياس
console.log(linearScale(50));  // ناتج: 250

// مقياس فئوي (للقيم المنفصلة)
const bandScale = d3.scaleBand()
    .domain(['A', 'B', 'C', 'D'])
    .range([0, 400])
    .padding(0.1);         // تحديد المساحة بين الفئات

// مقياس زمني
const timeScale = d3.scaleTime()
    .domain([new Date(2022, 0, 1), new Date(2022, 11, 31)])
    .range([0, 800]);

// مقياس لوني
const colorScale = d3.scaleLinear()
    .domain([0, 50, 100])
    .range(["blue", "purple", "red"]);

// مقياس لوني متدرج
const categoryScale = d3.scaleOrdinal()
    .domain(['فئة1', 'فئة2', 'فئة3', 'فئة4', 'فئة5'])
    .range(d3.schemeCategory10);

شرح أنواع المقاييس

  • scaleLinear - يخطط قيم من مجال خطي إلى نطاق خطي. مناسب للبيانات العددية المستمرة.
  • scaleBand - يقسم النطاق إلى نطاقات متساوية. مفيد للرسوم البيانية ذات الأعمدة.
  • scaleTime - مقياس خطي مخصص للتعامل مع البيانات الزمنية.
  • scaleOrdinal - يخطط القيم الفئوية إلى نطاق محدد. مناسب للبيانات الاسمية.
  • scaleQuantize - يقسم المجال المستمر إلى نطاقات منفصلة.

إعداد نطاقات المقاييس

// تعيين النطاق يدويًا
const scale1 = d3.scaleLinear()
    .domain([0, 100])
    .range([0, 500]);

// استخدام الحدود الدنيا والقصوى للبيانات
const data = [12, 31, 45, 63, 28, 59];
const scale2 = d3.scaleLinear()
    .domain([0, d3.max(data)])      // من 0 إلى القيمة القصوى
    .range([0, 500]);

// استخدام الحدود الفعلية للبيانات
const scale3 = d3.scaleLinear()
    .domain([d3.min(data), d3.max(data)])
    .range([0, 500]);

// تعيين نطاق بالأبعاد المحددة للرسم البياني (مع هوامش)
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

// مقياس لمحور X (أفقي)
const xScale = d3.scaleBand()
    .domain(d3.range(data.length))
    .range([0, width])
    .padding(0.1);

// مقياس لمحور Y (رأسي)
const yScale = d3.scaleLinear()
    .domain([0, d3.max(data)])
    .range([height, 0]);     // ملاحظة: النطاق معكوس (من ارتفاع إلى 0)

نصائح عملية حول المقاييس

  • استخدم .nice() لتقريب حدود النطاق إلى قيم "لطيفة" سهلة القراءة.
  • للمحور Y، عادة ما يكون النطاق معكوسًا ([height, 0]) لأن إحداثيات SVG تبدأ من الأعلى.
  • المقاييس ليست مقتصرة على الإحداثيات، يمكن استخدامها للألوان والأحجام والشفافية وغيرها.
  • استخدم .clamp(true) لتحديد القيم التي تقع خارج النطاق.

المحاور (Axes)

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

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

// إنشاء مقاييس
const x = d3.scaleLinear()
    .domain([0, 100])
    .range([0, 400]);

const y = d3.scaleLinear()
    .domain([0, 500])
    .range([300, 0]);

// إنشاء محاور
const xAxis = d3.axisBottom(x);     // محور أفقي في الأسفل
const yAxis = d3.axisLeft(y);       // محور رأسي على اليسار

// إضافة المحاور إلى SVG
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", 500)
    .attr("height", 400);

// إضافة مجموعة للمحور الأفقي وتحديد موضعه
svg.append("g")
    .attr("transform", "translate(50, 350)")  // الهامش الأيسر، الارتفاع - الهامش السفلي
    .call(xAxis);                            // تنفيذ الدالة على التحديد

// إضافة مجموعة للمحور الرأسي وتحديد موضعه
svg.append("g")
    .attr("transform", "translate(50, 50)")  // الهامش الأيسر، الهامش العلوي
    .call(yAxis);

شرح أساسيات المحاور

  • d3.axisBottom(scale) - ينشئ محورًا أفقيًا مع تسميات في الأسفل.
  • d3.axisTop(scale) - ينشئ محورًا أفقيًا مع تسميات في الأعلى.
  • d3.axisLeft(scale) - ينشئ محورًا رأسيًا مع تسميات على اليسار.
  • d3.axisRight(scale) - ينشئ محورًا رأسيًا مع تسميات على اليمين.
  • .call(axis) - طريقة مختصرة لتطبيق دالة على تحديد.

تخصيص المحاور

// تخصيص عدد العلامات المرجعية
const xAxis = d3.axisBottom(x)
    .ticks(5);              // عدد العلامات التقريبي

// تخصيص تنسيق القيم
const yAxis = d3.axisLeft(y)
    .tickFormat(d => d + "%");  // إضافة % إلى القيم

// تخصيص حجم العلامات
const xAxis2 = d3.axisBottom(x)
    .tickSize(10)           // حجم العلامات الصغيرة
    .tickSizeOuter(15);     // حجم العلامات الخارجية

// تحديد علامات معينة
const yAxis2 = d3.axisLeft(y)
    .tickValues([0, 100, 250, 400, 500]);  // علامات بقيم محددة

// إخفاء الخط الرئيسي للمحور
svg.append("g")
    .call(xAxis)
    .select(".domain")      // تحديد الخط الرئيسي (domain)
    .remove();              // إزالته

// تطبيق نمط CSS على المحور
svg.append("g")
    .call(yAxis)
    .attr("font-size", "12px")
    .attr("font-family", "sans-serif");

خيارات تخصيص المحاور

  • .ticks(count) - يحدد العدد المستهدف للعلامات (قد لا يكون دقيقًا).
  • .tickFormat(format) - يحدد تنسيق النص للعلامات.
  • .tickSize(size) - يحدد حجم العلامات الداخلية والخارجية.
  • .tickSizeInner(size) و .tickSizeOuter(size) - يحددان حجم العلامات الداخلية والخارجية بشكل منفصل.
  • .tickPadding(padding) - يحدد المسافة بين العلامات والنص.
  • .tickValues([values]) - يحدد قيمًا محددة للعلامات.

إضافة عناوين وتسميات للمحاور

// إضافة عنوان للمحور X
svg.append("text")
    .attr("x", width / 2)
    .attr("y", height + margin.bottom - 5)
    .attr("text-anchor", "middle")
    .text("الفترة الزمنية (بالأيام)");

// إضافة عنوان للمحور Y
svg.append("text")
    .attr("transform", "rotate(-90)")  // تدوير النص
    .attr("x", -height / 2)            // ملاحظة: بعد التدوير، تبدل x و y
    .attr("y", -margin.left + 15)
    .attr("text-anchor", "middle")
    .text("القيمة (بالدولار)");

الأشكال والرسومات

توفر D3.js مجموعة متنوعة من الدوال لإنشاء أشكال ورسومات في SVG، من الأشكال البسيطة إلى المسارات المعقدة.

الأشكال الأساسية

// إنشاء عنصر SVG
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", 500)
    .attr("height", 300);

// إضافة مستطيل
svg.append("rect")
    .attr("x", 50)
    .attr("y", 50)
    .attr("width", 200)
    .attr("height", 100)
    .attr("fill", "steelblue")
    .attr("stroke", "black")
    .attr("stroke-width", 2)
    .attr("rx", 10)           // نصف قطر زوايا المستطيل
    .attr("ry", 10);          // تدوير الزوايا

// إضافة دائرة
svg.append("circle")
    .attr("cx", 350)          // مركز الدائرة x
    .attr("cy", 100)          // مركز الدائرة y
    .attr("r", 50)            // نصف القطر
    .attr("fill", "coral");

// إضافة خط
svg.append("line")
    .attr("x1", 50)           // نقطة البداية x
    .attr("y1", 200)          // نقطة البداية y
    .attr("x2", 450)          // نقطة النهاية x
    .attr("y2", 200)          // نقطة النهاية y
    .attr("stroke", "green")
    .attr("stroke-width", 3)
    .attr("stroke-dasharray", "5,5");  // خط متقطع

// إضافة شكل بيضاوي
svg.append("ellipse")
    .attr("cx", 350)
    .attr("cy", 220)
    .attr("rx", 100)          // نصف القطر الأفقي
    .attr("ry", 30)           // نصف القطر الرأسي
    .attr("fill", "purple");

خصائص الأشكال الأساسية

  • مستطيل (rect): x، y، width، height، rx، ry
  • دائرة (circle): cx، cy، r
  • خط (line): x1، y1، x2، y2
  • شكل بيضاوي (ellipse): cx، cy، rx، ry
  • مضلع (polygon): points - سلسلة من أزواج الإحداثيات

المسارات (Paths)

المسارات هي الأدوات الأكثر مرونة وقوة لإنشاء أشكال معقدة في SVG. توفر D3 مولدات لإنشاء المسارات تلقائيًا:

// إنشاء مولد خط
const lineGenerator = d3.line()
    .x(d => d.x)
    .y(d => d.y)
    .curve(d3.curveLinear);  // نوع المنحنى (خطي)

// بيانات النقاط
const points = [
    {x: 50, y: 100},
    {x: 100, y: 50},
    {x: 150, y: 150},
    {x: 200, y: 80},
    {x: 250, y: 120}
];

// إنشاء مسار باستخدام المولد
svg.append("path")
    .datum(points)                  // ربط البيانات
    .attr("d", lineGenerator)       // استخدام مولد الخط
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-width", 2);

// إنشاء مسار منحنى
const curveGenerator = d3.line()
    .x(d => d.x)
    .y(d => d.y)
    .curve(d3.curveCatmullRom);     // منحنى أكثر نعومة

svg.append("path")
    .datum(points)
    .attr("d", curveGenerator)
    .attr("fill", "none")
    .attr("stroke", "orange")
    .attr("stroke-width", 2);

مولدات المسارات في D3

  • d3.line() - ينشئ مولد خط.
  • d3.area() - ينشئ مولد منطقة.
  • d3.arc() - ينشئ مولد قوس (مفيد للمخططات الدائرية).
  • d3.pie() - يحول البيانات إلى زوايا للمخططات الدائرية.
  • d3.stack() - يحول البيانات للمخططات المكدسة.

مثال: إنشاء مخطط خطي بسيط

// بيانات للمخطط
const data = [
    {month: "يناير", value: 30},
    {month: "فبراير", value: 45},
    {month: "مارس", value: 35},
    {month: "أبريل", value: 60},
    {month: "مايو", value: 40},
    {month: "يونيو", value: 80}
];

// إعداد أبعاد المخطط
const margin = {top: 20, right: 30, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

// إنشاء عنصر SVG
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// إنشاء مقاييس
const x = d3.scaleBand()
    .domain(data.map(d => d.month))
    .range([0, width])
    .padding(0.1);

const y = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .nice()
    .range([height, 0]);

// إضافة المحاور
svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x));

svg.append("g")
    .call(d3.axisLeft(y));

// إنشاء مولد خط
const line = d3.line()
    .x(d => x(d.month) + x.bandwidth() / 2)
    .y(d => y(d.value))
    .curve(d3.curveMonotoneX);

// إضافة الخط
svg.append("path")
    .datum(data)
    .attr("fill", "none")
    .attr("stroke", "steelblue")
    .attr("stroke-width", 2)
    .attr("d", line);

// إضافة نقاط البيانات
svg.selectAll(".dot")
    .data(data)
    .enter()
    .append("circle")
    .attr("class", "dot")
    .attr("cx", d => x(d.month) + x.bandwidth() / 2)
    .attr("cy", d => y(d.value))
    .attr("r", 5)
    .attr("fill", "steelblue");

الانتقالات والرسوم المتحركة

الانتقالات في D3.js تتيح لك إنشاء حركات سلسة وتأثيرات بصرية جذابة عند تغيير خصائص العناصر. هذا يساعد في إظهار التغييرات في البيانات بشكل أكثر وضوحًا.

أساسيات الانتقالات

// انتقال بسيط
d3.select("circle")
    .transition()                // بدء انتقال
    .duration(1000)             // المدة بالمللي ثانية
    .attr("r", 50)              // تغيير نصف القطر
    .attr("fill", "red");       // تغيير اللون

// تأخير الانتقال
d3.select("rect")
    .transition()
    .delay(500)                 // تأخير 500 مللي ثانية
    .duration(2000)
    .attr("width", 300);

// دالة التوقيت (Easing)
d3.selectAll(".bar")
    .transition()
    .duration(1000)
    .ease(d3.easeElastic)       // دالة توقيت مرنة
    .attr("height", d => d * 2);

// تسلسل الانتقالات
d3.select("circle")
    .transition()               // الانتقال الأول
    .duration(1000)
    .attr("r", 60)
    .transition()               // الانتقال الثاني
    .duration(1000)
    .attr("cx", 300)
    .transition()               // الانتقال الثالث
    .duration(1000)
    .attr("r", 30);

شرح طرق الانتقال

  • .transition() - يبدأ انتقال على العناصر المحددة.
  • .duration(ms) - يحدد مدة الانتقال بالمللي ثانية.
  • .delay(ms) - يحدد تأخير قبل بدء الانتقال.
  • .ease(function) - يحدد دالة التوقيت للانتقال.

تحديثات البيانات مع الانتقالات

// مثال: تحديث مخطط أعمدة مع انتقالات
function updateChart(newData) {
    // تحديث مقياس Y للبيانات الجديدة
    const y = d3.scaleLinear()
        .domain([0, d3.max(newData)])
        .range([height, 0]);

    // تحديث المحور Y
    svg.select(".y-axis")
        .transition()
        .duration(1000)
        .call(d3.axisLeft(y));

    // ربط البيانات الجديدة
    const bars = svg.selectAll(".bar")
        .data(newData);

    // تحديث الأعمدة الموجودة
    bars.transition()
        .duration(1000)
        .attr("y", d => y(d))
        .attr("height", d => height - y(d));

    // إضافة أعمدة جديدة
    bars.enter()
        .append("rect")
        .attr("class", "bar")
        .attr("x", (d, i) => x(i))
        .attr("width", x.bandwidth())
        .attr("fill", "steelblue")
        .attr("y", height)           // بدء من الأسفل للحصول على تأثير التحريك لأعلى
        .attr("height", 0)
        .transition()                // تطبيق انتقال
        .duration(1000)
        .attr("y", d => y(d))
        .attr("height", d => height - y(d));

    // إزالة الأعمدة الزائدة
    bars.exit()
        .transition()
        .duration(1000)
        .attr("y", height)
        .attr("height", 0)
        .remove();
}

ملاحظات حول الانتقالات

  • الانتقالات تعمل على معظم الخصائص العددية والألوان.
  • يمكن إنشاء انتقالات لخصائص CSS أيضًا باستخدام .style().
  • تأكد من أن البيانات قبل وبعد الانتقال لها نفس "المفتاح" عند استخدام انتقالات مع مجموعات البيانات.
  • يمكنك استخدام دوال استرجاع (callbacks) للتعامل مع بداية ونهاية الانتقالات.

دوال التوقيت (Easing Functions)

دوال التوقيت تتحكم في سرعة الانتقال خلال مدته، مما يتيح تأثيرات مختلفة:

// أنواع مختلفة من دوال التوقيت
const easingFunctions = [
    d3.easeLinear,      // خطي (سرعة ثابتة)
    d3.easeQuad,        // تربيعي
    d3.easeCubic,       // تكعيبي
    d3.easeSin,         // جيبي
    d3.easeExp,         // أسي
    d3.easeElastic,     // مرن
    d3.easeBounce       // ارتدادي
];

// مثال على استخدام دالة توقيت
d3.select("circle")
    .transition()
    .duration(2000)
    .ease(d3.easeBounce)    // تأثير الارتداد
    .attr("cy", 300);

معالجة الأحداث

يتيح D3.js ربط معالجات الأحداث بالعناصر، مما يجعل الرسومات البيانية تفاعلية. يمكنك الاستجابة لأحداث المستخدم مثل النقر، التحويم، السحب، وغيرها.

ربط معالجات الأحداث الأساسية

// ربط حدث النقر
d3.select("circle")
    .on("click", function(event, d) {
        console.log("تم النقر على الدائرة!");
        console.log("البيانات:", d);
        console.log("الحدث:", event);
        
        // يمكن الوصول إلى العنصر الذي تم النقر عليه
        d3.select(this).attr("fill", "red");
    });

// ربط أحداث الفأرة
d3.selectAll(".bar")
    .on("mouseover", function(event, d) {
        // عند تحويم الفأرة فوق العنصر
        d3.select(this)
            .transition()
            .duration(200)
            .attr("fill", "orange");
    })
    .on("mouseout", function(event, d) {
        // عند إزالة الفأرة من العنصر
        d3.select(this)
            .transition()
            .duration(500)
            .attr("fill", "steelblue");
    });

// إزالة معالج حدث
d3.select("rect").on("click", null);  // إزالة معالج النقر

شرح طرق معالجة الأحداث

  • .on(eventName, listener) - يربط معالج حدث بالعناصر المحددة.
  • داخل دالة المعالج، يشير this إلى عنصر DOM الذي حدث فيه الحدث.
  • المعلمة الأولى event تحتوي على معلومات الحدث.
  • المعلمة الثانية d تحتوي على البيانات المرتبطة بالعنصر.

مثال: إنشاء تلميحات (Tooltips) تفاعلية

// إضافة حاوية للتلميح
const tooltip = d3.select("body")
    .append("div")
    .attr("class", "tooltip")
    .style("position", "absolute")
    .style("background-color", "white")
    .style("border", "1px solid #ddd")
    .style("border-radius", "4px")
    .style("padding", "10px")
    .style("box-shadow", "0 2px 4px rgba(0,0,0,0.1)")
    .style("visibility", "hidden");  // مخفي بشكل افتراضي

// ربط أحداث بعناصر الرسم البياني
d3.selectAll(".data-point")
    .on("mouseover", function(event, d) {
        // اظهار التلميح
        tooltip
            .style("visibility", "visible")
            .html(`
                ${d.name}
القيمة: ${d.value}
نسبة التغيير: ${d.change}% `); }) .on("mousemove", function(event) { // تحريك التلميح مع الفأرة tooltip .style("left", (event.pageX + 15) + "px") .style("top", (event.pageY - 30) + "px"); }) .on("mouseout", function() { // إخفاء التلميح tooltip.style("visibility", "hidden"); });

السحب والتفاعل المتقدم

// تمكين السحب باستخدام d3.drag
const drag = d3.drag()
    .on("start", function(event, d) {
        // عند بدء السحب
        d3.select(this).raise().attr("stroke", "black");
    })
    .on("drag", function(event, d) {
        // أثناء السحب
        d3.select(this)
            .attr("cx", d.x = event.x)
            .attr("cy", d.y = event.y);
    })
    .on("end", function(event, d) {
        // عند انتهاء السحب
        d3.select(this).attr("stroke", null);
    });

// تطبيق السلوك على العناصر
d3.selectAll("circle").call(drag);

// مثال آخر: التكبير والتصغير (Zoom)
const zoom = d3.zoom()
    .scaleExtent([0.5, 5])  // حدود التكبير (0.5x إلى 5x)
    .on("zoom", function(event) {
        // تحديث المجموعة عند التكبير/التصغير
        g.attr("transform", event.transform);
    });

// تطبيق سلوك التكبير على SVG
svg.call(zoom);

سلوكيات التفاعل المتقدمة

D3 يوفر عدة سلوكيات (behaviors) جاهزة للتفاعل:

  • d3.drag() - للتعامل مع سحب العناصر.
  • d3.zoom() - للتعامل مع التكبير والتصغير والتحريك.
  • d3.brush() - لتحديد مناطق بالسحب.

التخطيطات (Layouts)

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

مخطط دائري (Pie Chart)

// البيانات
const data = [
    {category: "فئة أ", value: 30},
    {category: "فئة ب", value: 15},
    {category: "فئة ج", value: 25},
    {category: "فئة د", value: 10},
    {category: "فئة هـ", value: 20}
];

// إنشاء SVG
const width = 450;
const height = 450;
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

// حساب نصف القطر
const radius = Math.min(width, height) / 2;

// إنشاء مجموعة وتحريكها إلى المنتصف
const g = svg.append("g")
    .attr("transform", `translate(${width/2}, ${height/2})`);

// إنشاء مقياس للألوان
const color = d3.scaleOrdinal()
    .domain(data.map(d => d.category))
    .range(d3.schemeCategory10);

// إنشاء تخطيط الدائرة
const pie = d3.pie()
    .value(d => d.value)
    .sort(null); // لا ترتيب

// إنشاء مولد قوس
const arc = d3.arc()
    .innerRadius(0)       // للمخطط الدائري التقليدي
    .outerRadius(radius - 10);

// إنشاء مولد قوس للتسميات
const labelArc = d3.arc()
    .innerRadius(radius - 80)
    .outerRadius(radius - 80);

// إنشاء العناصر المرئية
const arcs = g.selectAll(".arc")
    .data(pie(data))
    .enter()
    .append("g")
    .attr("class", "arc");

// إضافة المسارات
arcs.append("path")
    .attr("d", arc)
    .attr("fill", d => color(d.data.category))
    .attr("stroke", "white")
    .style("stroke-width", "2px");

// إضافة التسميات
arcs.append("text")
    .attr("transform", d => `translate(${labelArc.centroid(d)})`)
    .attr("text-anchor", "middle")
    .text(d => d.data.category);

// إضافة تلميحات عند التحويم
arcs.on("mouseover", function(event, d) {
    d3.select(this).select("path")
        .transition()
        .duration(200)
        .attr("d", d3.arc()
            .innerRadius(0)
            .outerRadius(radius - 5));
})
.on("mouseout", function() {
    d3.select(this).select("path")
        .transition()
        .duration(200)
        .attr("d", arc);
});

خريطة الشجرة (Treemap)

// بيانات التسلسل الهرمي
const data = {
    name: "الجذر",
    children: [
        {
            name: "المجموعة أ",
            children: [
                { name: "العنصر أ1", value: 100 },
                { name: "العنصر أ2", value: 150 }
            ]
        },
        {
            name: "المجموعة ب",
            children: [
                { name: "العنصر ب1", value: 200 },
                { name: "العنصر ب2", value: 120 },
                { name: "العنصر ب3", value: 80 }
            ]
        },
        { name: "العنصر ج", value: 300 }
    ]
};

// إنشاء تسلسل هرمي من البيانات
const root = d3.hierarchy(data)
    .sum(d => d.value)
    .sort((a, b) => b.value - a.value);

// إنشاء تخطيط خريطة الشجرة
const treemap = d3.treemap()
    .size([width, height])
    .padding(2);

// تطبيق التخطيط على البيانات
treemap(root);

// إنشاء مقياس للألوان
const color = d3.scaleOrdinal()
    .domain(["المجموعة أ", "المجموعة ب", "العنصر ج"])
    .range(["#66c2a5", "#fc8d62", "#8da0cb"]);

// إضافة العقد إلى SVG
const cell = svg.selectAll("g")
    .data(root.leaves())
    .join("g")
    .attr("transform", d => `translate(${d.x0},${d.y0})`);

// إضافة المستطيلات
cell.append("rect")
    .attr("width", d => d.x1 - d.x0)
    .attr("height", d => d.y1 - d.y0)
    .attr("fill", d => {
        while (d.depth > 1) d = d.parent;
        return color(d.data.name);
    })
    .attr("stroke", "#fff");

// إضافة النص
cell.append("text")
    .attr("x", 3)
    .attr("y", 15)
    .text(d => d.data.name)
    .attr("font-size", "10px")
    .attr("fill", "white");

تخطيطات أخرى

توفر D3 العديد من التخطيطات الأخرى:

  • مخطط مكدس (Stack): لعرض البيانات المكدسة
  • مخطط الأعمدة المصفوفة (Histogram): لتقسيم البيانات المستمرة إلى فئات
  • مخطط القوة (Force): لعرض العلاقات والشبكات
  • مخطط حزمة (Bundle): لعرض الاتصالات بين العناصر
  • مخطط الدائرة (Pack): لعرض التسلسل الهرمي كدوائر متداخلة
  • مخطط سانكي (Sankey): لعرض تدفقات البيانات

مفاهيم متقدمة

بعد إتقان المفاهيم الأساسية، يمكنك الانتقال إلى موضوعات أكثر تقدمًا في D3.js.

المكونات القابلة لإعادة الاستخدام

// إنشاء مكون مخطط الأعمدة قابل لإعادة الاستخدام
function barChart() {
    // الإعدادات الافتراضية
    let width = 600;
    let height = 400;
    let margin = { top: 20, right: 20, bottom: 30, left: 40 };
    let xValue = d => d[0];
    let yValue = d => d[1];
    let xScale = d3.scaleBand();
    let yScale = d3.scaleLinear();
    let color = "steelblue";
    
    // الدالة الرئيسية للمخطط
    function chart(selection) {
        selection.each(function(data) {
            // تحديث نطاقات المقاييس
            xScale
                .domain(data.map(xValue))
                .range([0, width - margin.left - margin.right])
                .padding(0.1);
                
            yScale
                .domain([0, d3.max(data, yValue)])
                .nice()
                .range([height - margin.top - margin.bottom, 0]);
                
            // تحديد SVG
            const svg = d3.select(this)
                .selectAll("svg")
                .data([data]);
                
            // إنشاء العناصر الأولية
            const svgEnter = svg.enter().append("svg");
            const gEnter = svgEnter.append("g");
            
            // إعداد المحاور
            gEnter.append("g").attr("class", "x-axis");
            gEnter.append("g").attr("class", "y-axis");
            
            // تحديث حجم SVG
            svg.merge(svgEnter)
                .attr("width", width)
                .attr("height", height);
                
            // تحديث موضع المجموعة
            const g = svg.merge(svgEnter).select("g")
                .attr("transform", `translate(${margin.left},${margin.top})`);
                
            // تحديث محور X
            g.select(".x-axis")
                .attr("transform", `translate(0,${height - margin.top - margin.bottom})`)
                .call(d3.axisBottom(xScale));
                
            // تحديث محور Y
            g.select(".y-axis")
                .call(d3.axisLeft(yScale));
                
            // ربط البيانات بالأعمدة
            const bars = g.selectAll(".bar")
                .data(data);
                
            // إدخال الأعمدة الجديدة
            bars.enter()
                .append("rect")
                .attr("class", "bar")
                .attr("x", d => xScale(xValue(d)))
                .attr("y", height - margin.top - margin.bottom)
                .attr("width", xScale.bandwidth())
                .attr("height", 0)
                .attr("fill", color)
                .merge(bars)
                .transition()
                .duration(1000)
                .attr("x", d => xScale(xValue(d)))
                .attr("y", d => yScale(yValue(d)))
                .attr("width", xScale.bandwidth())
                .attr("height", d => height - margin.top - margin.bottom - yScale(yValue(d)));
                
            // إزالة الأعمدة الزائدة
            bars.exit()
                .transition()
                .duration(1000)
                .attr("y", height - margin.top - margin.bottom)
                .attr("height", 0)
                .remove();
        });
    }
    
    // الدوال المساعدة لضبط الخصائص
    chart.width = function(value) {
        if (!arguments.length) return width;
        width = value;
        return chart;
    };
    
    chart.height = function(value) {
        if (!arguments.length) return height;
        height = value;
        return chart;
    };
    
    chart.margin = function(value) {
        if (!arguments.length) return margin;
        margin = value;
        return chart;
    };
    
    chart.x = function(value) {
        if (!arguments.length) return xValue;
        xValue = value;
        return chart;
    };
    
    chart.y = function(value) {
        if (!arguments.length) return yValue;
        yValue = value;
        return chart;
    };
    
    chart.color = function(value) {
        if (!arguments.length) return color;
        color = value;
        return chart;
    };
    
    return chart;
}

// استخدام المكون
const myChart = barChart()
    .width(800)
    .height(500)
    .margin({ top: 30, right: 30, bottom: 40, left: 50 })
    .color("#3498db");

// تطبيق المخطط على البيانات
d3.select("#chart")
    .datum(data)
    .call(myChart);

فوائد المكونات القابلة لإعادة الاستخدام

  • تقليل تكرار الشيفرة وتحسين إمكانية الصيانة.
  • تسهيل اختبار وتطوير الرسوم البيانية المنفصلة.
  • إمكانية ضبط العديد من الخصائص من خلال API متسق.
  • إمكانية إنشاء عدة نسخ من نفس المخطط بإعدادات مختلفة.

الرسوم البيانية التفاعلية المتعددة (Coordinated Views)

يمكنك إنشاء عدة رسوم بيانية تتفاعل مع بعضها البعض:

// مثال بسيط للرسوم البيانية المتناسقة
function updateCharts(selectedData) {
    // تحديث المخطط الأول
    chart1.update(selectedData);
    
    // تحديث المخطط الثاني
    chart2.update(selectedData);
    
    // تحديث المخطط الثالث
    chart3.update(selectedData);
}

// ربط حدث التحديد بالمخطط الأول
d3.select("#chart1")
    .selectAll(".bar")
    .on("click", function(event, d) {
        // تصفية البيانات بناءً على العنصر المحدد
        const filtered = fullData.filter(item => 
            item.category === d.category
        );
        
        // تحديث جميع المخططات
        updateCharts(filtered);
    });

تقنيات الأداء

اعتبارات الأداء في D3

  • استخدم Canvas للرسوم البيانية الكبيرة: SVG يصبح بطيئًا مع آلاف العناصر، بينما Canvas أكثر كفاءة.
  • تقليل عدد العناصر في DOM: دمج البيانات المتشابهة، استخدام تقنيات تجميع البيانات.
  • تجنب إعادة رسم كل شيء عند التحديث: استخدم الانتقالات على العناصر المتأثرة فقط.
  • تنفيذ التحديثات بالدفعات: تجنب تحديثات DOM المتكررة.
  • تحسين وظائف الاختيار: استخدم محددات دقيقة بدلاً من المحددات العامة.

دمج D3 مع أطر العمل الأخرى

يمكن دمج D3.js مع أطر عمل مثل React أو Vue أو Angular:

// مثال بسيط لدمج D3 مع React
import React, { useRef, useEffect } from 'react';
import * as d3 from 'd3';

function BarChart({ data }) {
    const chartRef = useRef();
    
    useEffect(() => {
        // إنشاء المخطط عند تحميل المكون
        const svg = d3.select(chartRef.current)
            .append('svg')
            .attr('width', 600)
            .attr('height', 400);
            
        // إنشاء المقاييس والمحاور
        // ...
        
        // رسم الأعمدة
        svg.selectAll('rect')
            .data(data)
            .enter()
            .append('rect')
            .attr('x', (d, i) => i * 70)
            .attr('y', d => 400 - d.value * 10)
            .attr('width', 65)
            .attr('height', d => d.value * 10)
            .attr('fill', 'steelblue');
            
        // التنظيف عند إزالة المكون
        return () => {
            d3.select(chartRef.current).selectAll('*').remove();
        };
    }, [data]);  // إعادة الرسم عند تغيير البيانات
    
    return 
; }

استراتيجيات الدمج

  • نهج D3 للرسم، إطار العمل للتطبيق: استخدم D3 للتصور البياني وإطار العمل لباقي التطبيق.
  • استخدام D3 للحسابات فقط: استخدم D3 لحساب الإحداثيات والتخطيطات، وإطار العمل لإنشاء العناصر.
  • مكتبات الدمج: مثل react-d3-library أو @visx/visx لـ React توفر واجهات جاهزة.
  • مكونات صغيرة: تقسيم التصورات إلى مكونات أصغر يسهل إدارتها.