matplotlib

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)matplotlib

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) matplotlib

John Hunter و Michael Droettboom

إذا استمتعت بهذه الكتب، فقد يعجبك أيضًا التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.

matplotlib مكتبة رسم مبنيّة على Python، تدعم الرسوم ثنائية الأبعاد (2D) دعمًا كاملًا والرسوم ثلاثية الأبعاد (3D) دعمًا محدودًا، وتُستخدم على نطاق واسع في مجتمع الحوسبة العلمية على Python. تستهدف المكتبة طيفًا واسعًا من حالات الاستخدام. فهي تستطيع تضمين الرسوم في مجموعة أدوات واجهة المستخدم التي تختارها، وتدعم حاليًا الرسوم التفاعلية على جميع أنظمة تشغيل سطح المكتب الرئيسية باستخدام مجموعات الأدوات GTK+ وQt وTk وFLTK وwxWidgets وCocoa. ويمكن استدعاؤها تفاعليًا من صدفة Python التفاعلية لإنتاج رسوم بأوامر إجرائية بسيطة، مثل Mathematica أو IDL أو MATLAB إلى حدٍّ بعيد. ويمكن أيضًا تضمين matplotlib في خادم ويب بلا واجهة رسومية (headless) لتوفير نسخ مطبوعة (hardcopy) بصيغ نقطية (raster) مثل Portable Network Graphics (PNG)، وصيغ متجهية (vector) مثل PostScript وPortable Document Format (PDF) وScalable Vector Graphics (SVG)، تبدو ممتازة على الورق.

11.1. مشكلة مفتاح الحماية

تعود نشأة matplotlib إلى محاولة من أحدنا (John Hunter) للتحرّر هو وبقية باحثي الصرع لديه من حزمة برمجية مملوكة تُستخدم في تحليل التخطيط الكهربائي القِشريّ (ECoG). وكان المختبر الذي يعمل فيه يملك ترخيصًا واحدًا فقط لتلك البرمجية، فكان طلاب الدراسات العليا وطلاب الطب والباحثون ما بعد الدكتوراه والمتدربون يتناوبون على مشاركة مفتاح الحماية العتادي. وانتشر استخدام MATLAB على نطاق واسع في الأوساط الطب الحيوي لتحليل البيانات وتصورها، فشرع Hunter — بقدرٍ من النجاح — في استبدال البرمجية المملوكة بإصدار مبني على MATLAB يمكن لعدة باحثين استخدامه وتوسيعه. غير أنّ MATLAB، بطبيعتها، تنظر إلى العالم بوصفه مصفوفة من الأعداد العشرية، وقد دفعت تعقيدات سجلات المستشفيات الواقعية لمرضى جراحة الصرع ذوي أنماط بيانات متعدّدة (CT وMRI وECoG وEEG) والمخزَّنة في خوادم مختلفة، MATLAB إلى حدودها بوصفها نظام إدارة بيانات. ولم يقتنع Hunter بملاءمة MATLAB لهذه المهمة، فانطلق في العمل على تطبيق Python جديد مبنيّ فوق مجموعة أدوات واجهة المستخدم GTK+، التي كانت آنذاك نظام النوافذ الرائد لأجهزة سطح المكتب على Linux.

فقد طُوِّر matplotlib في الأصل بالتالي كأداة لتصوير EEG وECoG لهذا التطبيق المبنيّ على GTK+، وقد وجّه هذا الاستخدام بنيةَه الأولى. وصُمِّم matplotlib في الأصل أيضًا لهدف ثانٍ: أن يكون بديلًا عن توليد الرسوم التفاعلي بالأوامر، وهو ما تتقنه MATLAB بإتقان. فتصميم MATLAB يجعل المهمة البسيطة، أي تحميل ملف بيانات ورسمه، مسألةً بديهية، في حين تكون واجهة برمجية كائنٍ موجّهة بالكامل (object-oriented API) ثقيلة نحويًا أكثر مما ينبغي. ولذلك يوفّر matplotlib أيضًا واجهة برمجة نصية ذات حالة (stateful) لتوليد الرسوم بسرعة وسهولة على نحو يشبه MATLAB. ولأنّ matplotlib مكتبة، فإنّ لدى المستخدمين وصول إلى جميع بنى بيانات Python الغنية والمدمجة، مثل القوائم والقواميس والمجموعات وغيرها.

الشكل 11.1: تطبيق matplotlib الأصلي: عارض ECoG

11.2. نظرة عامة على بنية matplotlib

يُسمّى كائن matplotlib الأسمى الذي يحتوي جميع عناصر رسم معيّن ويتولّى إدارته اسم Figure. ومن المهام المعمارية الجوهرية التي على matplotlib حلّها هي تنفيذ إطار عمل لتمثيل Figure والتعامل معه، مع فصله عن عملية التصيير (rendering) للـFigure إلى نافذة واجهة مستخدم أو إلى نسخة مطبوعة. يتيح هذا لنا بناء ميزات ومنطق أكثر تعقيدًا داخل الـFigureات، مع الإبقاء على «الواجهات الخلفية» (backends)، أي أجهزة الإخراج، بسيطة نسبيًا. لا يغلّف matplotlib واجهات الرسم فحسب من أجل السماح بالتصيير على أجهزة متعددة، بل يغلّف أيضًا المعالجة الأساسية للأحداث وإدارة النوافذ في معظم مجموعات أدوات واجهة المستخدم الشائعة. وبفضل ذلك يستطيع المستخدمون إنشاء رسوم تفاعلية غنية إلى حدٍّ لا بأس به، ومجموعات أدوات تتضمّن إدخال الفأرة ولوحة المفاتيح، يمكن توصيلها دون أي تعديل بمجموعات أدوات واجهة المستخدم الستّ التي ندعمها.

تنقسم البنية التي تحقّق ذلك منطقيًا إلى ثلاث طبقات، يمكن النظر إليها على هيئة مكدّس. كل طبقة تقع فوق طبقة أخرى تعرف كيف تتحدّث مع الطبقة التي تحتها، في حين لا تكون الطبقة الأدنى مطّلعةً على الطبقات التي فوقها. والطبقات الثلاث من الأسفل إلى الأعلى هي: الواجهة الخلفية (backend)، والفنان (artist)، والبرمجة النصية (scripting).

طبقة الواجهة الخلفية

في أسفل المكدّس تقع طبقة الواجهة الخلفية (backend)، وهي توفّر تطبيقات ملموسة (concrete implementations) لصنفَي الواجهة المجرّدين (abstract):

  • FigureCanvas يلخّص مفهوم السطح الذي يُرسَم عليه (مثلًا «الورق»).
  • Renderer يتولّى الرسم الفعلي (مثلًا «الفرشاة»).
  • Event يعالج مُدخلات المستخدم مثل أحداث لوحة المفاتيح والفأرة.

بالنسبة إلى مجموعة أدوات واجهة مستخدم مثل Qt، لدى FigureCanvas تطبيق ملموس يعرف كيف يُدرج نفسه في نافذة Qt أصلية (QtGui.QMainWindow)، وكيف ينقل أوامر مُصيِّر matplotlib إلى لوحة الرسم (QtGui.QPainter)، وكيف يترجم أحداث Qt الأصلية إلى إطار عمل Event في matplotlib، الذي يُشعر مُوزِّع الاستدعاءات بتوليد الأحداث، حتى يتمكّن المستمعون في المستويات الأعلى من معالجتها. وتقع أصناف الأساس المجرّدة في matplotlib.backend_bases، وتقع جميع الأصناف المشتقّة في وحدات مخصّصة مثل matplotlib.backends.backend_qt4agg. أما الواجهة الخلفية الصافية الخاصة بالصور، والمخصّصة لإنتاج مخرجات مطبوعة مثل PDF أو PNG أو SVG أو PS، فقد يكون تطبيق FigureCanvas فيها مجرّدًا تجهيز كائن يشبه الملف، تُعرَّف فيه الترويسات الافتراضية والخطوط والدوال الماكرو، فضلًا عن الكائنات المنفردة (الخطوط والنصوص والمستطيلات وما شابه) التي ينشئها Renderer.

مهمّة Renderer هي توفير واجهة رسم منخفضة المستوى لوضع الحبر على لوحة الرسم. وكما ذُكر أعلاه، كان تطبيق matplotlib الأصلي عارض ECoG داخل تطبيق GTK+، وقد استُلهم كثير من التصميم الأصلي من واجهة GDK/GTK+ البرمجية المتاحة في ذلك الحين. وقد استُمدّت واجهة Renderer الأصلية من واجهة GDK Drawable، التي تنفّذ توابع أوّلية مثل draw_point وdraw_line وdraw_rectangle وdraw_image وdraw_polygon وdraw_glyphs. وكل واجهة خلفية إضافية نفّذناها — وكان أقدمها الواجهة الخلفية لـ PostScript وواجهة GD الخلفية — طبّقت واجهة GDK Drawable البرمجية وترجمت هذه التوابع إلى أوامر رسم أصلية تعتمد على الواجهة الخلفية. وكما سنناقش أدناه، أدّى هذا بلا داعٍ إلى تعقيد تنفيذ الواجهات الخلفية الجديدة مع تكاثر كبير في عدد التوابع، وقد أُبسطت هذه الواجهة البرمجية في وقت لاحق بصورة جذرية، مما جعل عملية نقل matplotlib إلى مجموعة أدوات واجهة مستخدم جديدة أو مواصفة ملفات جديدة أمرًا بسيطًا.

ومن قرارات التصميم التي أثبتت نجاحها إلى حدٍّ كبير في matplotlib الدعمُ لمُصيِّر أساسي قائم على البكسلات يستخدم مكتبة قوالب C++ المعروفة بـ Anti-Grain Geometry أو «agg» She06. وهذه مكتبة عالية الأداء لتصيير الرسوم ثنائية الأبعاد المستعملة للتخلّص من تعرية الحوافّ (anti-aliasing)، وتنتج صورًا جذّابة. ويوفّر matplotlib دعمًا لإدراج مخازن البكسلات التي يصيّرها محرّك agg داخل كل مجموعة أدوات واجهة مستخدم ندعمها، بحيث يمكن الحصول على رسوم مطابقة تمامًا للبكسل عبر بيئات المستخدم وأنظمة التشغيل. ولأنّ مخرجات PNG التي تنتجها matplotlib تستخدم أيضًا محرّك agg، فإنّ النسخة المطبوعة مطابقة تمامًا لما يُعرض على الشاشة؛ فما تراه هو ما تحصل عليه عبر بيئات المستخدم وأنظمة التشغيل ومخرجات PNG.

يربط إطار عمل Event في matplotlib أحداث واجهة المستخدم السفلية مثل key-press-event أو mouse-motion-event بأصناف matplotlib KeyEvent أو MouseEvent. ويستطيع المستخدمون ربط هذه الأحداث بدوال استدعاء (callback) والتفاعل مع الرسم بياناتهم؛ مثلًا لالتقاط (pick) نقطة بيانات أو مجموعة نقاط، أو للتعامل مع جانب ما من الرسم أو مكوّناته. ويوضّح مقتطف الشيفرة التالي كيف يمكن تبديل حالة جميع الخطوط في نافذة Axes عندما يكتب المستخدم `t'.

import numpy as np
import matplotlib.pyplot as plt

def on_press(event):
    if event.inaxes is None: return
    for line in event.inaxes.lines:
        if event.key=='t':
            visible = line.get_visible()
            line.set_visible(not visible)
    event.inaxes.figure.canvas.draw()

fig, ax = plt.subplots(1)

fig.canvas.mpl_connect('key_press_event', on_press)

ax.plot(np.random.rand(2, 20))

plt.show()

يسمح تجريد إطار عمل أحداث مجموعة أدوات واجهة المستخدم السفلية لكلٍّ من مطوّري matplotlib والمستخدمين النهائيين بكتابة شيفرة معالجة أحداث واجهة المستخدم بأسلوب «اكتبها مرة واحدة وشغّلها في كل مكان». فمثلًا، التنقّل التفاعلي والتكبير في رسوم matplotlib، والذي يعمل عبر جميع مجموعات أدوات واجهة المستخدم، منفَّذ داخل إطار عمل أحداث matplotlib.

طبقة الفنان

تُعدّ تسلسلية Artist الطبقة الوسطى من مكدّس matplotlib، وهي المكان الذي يجري فيه معظم العمل الجاد. ومتابعةً للتشبيه الذي يجعل FigureCanvas الخاص بالواجهة الخلفية هو الورق، فإنّ Artist هو الكائن الذي يعرف كيف يأخذ Renderer (الفرشاة) ويضع الحبر على لوحة الرسم. وكل ما تراه في Figure لدى matplotlib هو نسخة من Artist؛ فالعنوان والخطوط وتسميات علامات التجزئة والصور وما شابه كلها تقابل نسخ Artist منفردة (انظر الشكل 11.3). والصنف الأساس هو matplotlib.artist.Artist، ويحتوي على الخصائص التي تشترك فيها كل نسخ Artist: التحويل (transformation) الذي يترجم نظام إحداثيات الفنان إلى نظام إحداثيات لوحة الرسم (وتُناقَش بالتفصيل أدناه)، والرؤية، وصندوق القصّ (clip box) الذي يعرّف المنطقة التي يستطيع الفنان الرسم فيها، والتسمية، والواجهة التي تعالج تفاعل المستخدم مثل «الالتقاط» (picking)؛ أي كشف وقوع نقرة فأرة فوق الفنان.

الشكل 11.2: رسم الشكل 11.3: تسلسل نسخ الفنان المستخدَمة لرسم الشكل 11.2.

يحدث الاقتران بين تسلسلية Artist والواجهة الخلفية داخل تابع draw. فمثلًا، في صنف النموذج التجريبي أدناه حيث ننشئ SomeArtist المشتقّ من Artist، فإنّ تابع SomeArtist الأساسي الذي يجب أن ينفّذه هو draw، ويُمرَّر إليه مُصيِّر من الواجهة الخلفية. لا يعرف Artist ما نوع الواجهة الخلفية التي سيُرسَم عليها المُصيِّر (PDF أو SVG أو GTK+ DrawingArea أو ما شابه)، لكنه يعرف واجهة Renderer البرمجية وسيستدعي التابع المناسب (draw_text أو draw_path). وبما أنّ Renderer يملك مؤشّرًا إلى لوحة الرسم الخاصة به ويعرف كيف يلوّن عليها، فإنّ تابع draw يحوّل التمثيل المجرّد لـArtist إلى ألوان داخل مخزن بكسلات، أو إلى مسارات داخل ملف SVG، أو إلى أي تمثيل ملموس آخر.

class SomeArtist(Artist):
    'An example Artist that implements the draw method'

    def draw(self, renderer):
        """Call the appropriate renderer methods to paint self onto canvas"""
        if not self.get_visible():  return

        # create some objects and use renderer to draw self here
        renderer.draw_path(graphics_context, path, transform)

هناك نوعان من نسخ Artist في التسلسلية. فالفنانان أوّليان (primitive) يمثّلان أنواع الكائنات التي تراها في رسم بياني: Line2D وRectangle وCircle وText. أما الفنانون المركّبون (composite) فهم مجموعات من نسخ Artist مثل Axis وTick وAxes وFigure. وقد يحتوي كل فنان مركّب على فنانون مركّبون آخرون إضافةً إلى الفنانون الأوّليين. فمثلًا، يحتوي Figure على واحد أو أكثر من Axes المركّبة، وخلفية Figure هي Rectangle أوّلي.

أهمّ فنان مركّب هو Axes، وفيه تُعرَّف معظم توابع الرسم في واجهة matplotlib البرمجية. لا يحتوي Axes على معظم العناصر الرسومية التي تشكّل خلفية الرسم البياني — علامات التجزئة وخطوط المحاور وشبكة الخطوط والرقعة اللونية التي هي خلفية الرسم — فحسب، بل يحتوي أيضًا على توابع مساعدة كثيرة تنشئ فنانون أوّليين وتضيفهم إلى نسخة Axes. فمثلًا، يبيّن الجدول 11.1 عيّنة صغيرة من توابع Axes التي تنشئ كائنات رسم وتخزّنها في نسخة Axes.

التابع ما ينشئه أين يُخزَّن
Axes.imshow واحد أو أكثر من matplotlib.image.AxesImage Axes.images
Axes.hist كثير من matplotlib.patch.Rectangle Axes.patches
Axes.plot واحد أو أكثر من matplotlib.lines.Line2D Axes.lines

فيما يلي نص برمجي بسيط بلغة Python يوضّح البنية أعلاه. وهو يعرّف الواجهة الخلفية، ويربط Figure بها، ويستخدم مكتبة المصفوفات numpy لإنشاء 10000 عدد عشوائي موزَّعًا طبيعيًا، ويرسم مدرّجًا تكراريًا لها.

# Import the FigureCanvas from the backend of your choice
#  and attach the Figure artist to it.
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
fig = Figure()
canvas = FigureCanvas(fig)

# Import the numpy library to generate the random numbers.
import numpy as np
x = np.random.randn(10000)

# Now use a figure method to create an Axes artist; the Axes artist is
#  added automatically to the figure container fig.axes.
# Here "111" is from the MATLAB convention: create a grid with 1 row and 1
#  column, and use the first cell in that grid for the location of the new
#  Axes.
ax = fig.add_subplot(111)

# Call the Axes method hist to generate the histogram; hist creates a
#  sequence of Rectangle artists for each histogram bar and adds them
#  to the Axes container.  Here "100" means create 100 bins.
ax.hist(x, 100)

# Decorate the figure with a title and save it.
ax.set_title('Normal distribution with $\mu=0, \sigma=1$')
fig.savefig('matplotlib_histogram.png')

طبقة البرمجة النصية (pyplot)

يعمل النص البرمجي الذي يستخدم الواجهة البرمجية أعلاه بصورة ممتازة، ولا سيما للمبرمجين، وهو عادةً نموذج البرمجة المناسب عند كتابة خادم تطبيق ويب، أو تطبيق بواجهة مستخدم، أو ربما نص برمجي يُشارَك مع مطوّرين آخرين. وللاستخدامات اليومية، ولا سيما في العمل الاستكشافاني التفاعلي على يد باحثي المخبر الذين ليسوا مبرمجين محترفين، فإنّه ثقيل نحويًا بعض الشيء. وتوفّر معظم اللغات المتخصّصة في تحليل البيانات والتصوير واجهة برمجة نصية أخفّ لتبسيط المهام الشائعة، ويفعل matplotlib ذلك أيضًا في واجهته matplotlib.pyplot. والشيفرة نفسها أعلاه، باستخدام pyplot، تقرأ

import matplotlib.pyplot as plt
import numpy as np

x = np.random.randn(10000)
plt.hist(x, 100)
plt.title(r'Normal distribution with $\mu=0, \sigma=1$')
plt.savefig('matplotlib_histogram.png')
plt.show()

الشكل 11.4: مدرّج تكراري أُنشئ باستخدام pyplot

‏pyplot واجهة ذات حالة (stateful) تتولّى كثيرًا من الشيفرة التكرارية (boilerplate) الخاصة بإنشاء الأشكال والمحاور وربطها بالواجهة الخلفية التي تختارها، وتحتفظ ببنى بيانات داخلية على مستوى الوحدة تمثّل الشكل والمحاور الحالية التي تُوجَّه إليها أوامر الرسم.

لنحلّل الأسطر المهمّة في النص البرمجي لنرى كيف تُدار هذه الحالة الداخلية.

  • import matplotlib.pyplot as plt: عند تحميل وحدة pyplot فإنّها تحلّل ملف إعدادات محلي يذكر فيه المستخدم، ضمن أمور كثيرة، تفضيله لواجهة خلفية افتراضية. قد تكون هذه واجهة خلفية لواجهة مستخدم مثل QtAgg، وعندئذٍ ستستورد الشيفرة أعلاه إطار عمل الواجهة الرسومية وتشغّل نافذة Qt مضمّنًا فيها الرسم، أو قد تكون واجهة خلفية صافية مثل Agg، وعندئذٍ ستولّد الشيفرة مخرجات النسخة المطبوعة ثم تخرج.
  • plt.hist(x, 100): هذا هو أول أمر رسم في النص البرمجي. ستتحقّق pyplot من بنيها الداخلية لترى ما إذا كانت هناك نسخة Figure حالية. إن وُجدت، فستستخرج Axes الحالية وتوجّه الرسم إلى استدعاء الواجهة Axes.hist. وفي حالتنا هذه لا توجد، ولذلك ستنشئ Figure وAxes، وتضبطهما كالحاليتين، وتوجّه الرسم إلى Axes.hist.
  • plt.title(r'Normal distribution with $\mu=0, \sigma=1$'): كما سبق، ستبحث pyplot عن وجود Figure وAxes حاليتين. ولما كانتا موجودتين، فلن تنشئ نسخًا جديدة، بل ستمرّر الاستدعاء إلى تابع نسخة Axes الموجودة، وهو Axes.set_title.
  • plt.show(): سيفرض هذا تصيير Figure، وإذا كان المستخدم قد حدّد واجهة خلفية رسومية افتراضية في ملف إعداداته، فسيشغّل الحلقة الرئيسية للواجهة الرسومية (GUI mainloop) ويرفع أي أشكال أُنشئت إلى الشاشة.

وفيما يلي نسخة مختزلة ومبسّطة إلى حدٍّ ما من تابع رسم الخطوط matplotlib.pyplot.plot كثير الاستخدام في pyplot، لتوضيح كيف يغلّف تابع في pyplot الوظائف المتوافرة في نواة matplotlib الموجّهة بالكائنات. وتتبع جميع توابع واجهة البرمجة النصية pyplot الأخرى التصميم نفسه.

@autogen_docstring(Axes.plot)
def plot(*args, **kwargs):
    ax = gca()

    ret = ax.plot(*args, **kwargs)
    draw_if_interactive()

    return ret

يستخرج مُزخرِف (decorator) بايثون @autogen_docstring(Axes.plot) سلسلة التوثيق من تابع الواجهة البرمجية المقابل ويرفق صيغةً منها منسّقةً بشكل صحيح مع تابع pyplot.plot؛ ولدينا وحدة مخصّصة هي matplotlib.docstring للتعامل مع هذه السحرية. أما *args و**kwargs في توقيع التوثيق فهما اصطلاحان خاصان في Python يعنيان كل الوسائط ووسائط الكلمات المفتاحية التي تُمرَّر إلى التابع. وهذا يتيح لنا تمريرها إلى تابع الواجهة البرمجية المقابل. والاستدعاء ax = gca() يستدعي الآلية ذات الحالة للحصول على «محاور الـAxes الحالية» (لا يمكن لكل مفسّر Python أن يمتلك سوى «محاور حالية» واحدة)، وينشئ Figure وAxes عند الحاجة. أمّا الاستدعاء ret = ax.plot(*args, **kwargs) فيمرّر استدعاء الدالة ووسائطها إلى تابع Axes المناسب، ويخزّن القيمة المُعادة لاستخدامها لاحقًا. وهكذا فإنّ واجهة pyplot هي غلاف رقيق إلى حدٍّ كبير حول واجهة Artist الأساسية، وتحاول تفادي تكرار الشيفرة قدر الإمكان عبر إتاحة الدالة وتوقيع الاستدعاء وسلسلة التوثيق في واجهة البرمجة النصية بأقل قدر ممكن من الشيفرة التكرارية.

11.3. إعادة هيكلة الواجهات الخلفية

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

draw_arc, draw_image, draw_line_collection, draw_line, draw_lines, draw_point,
draw_quad_mesh, draw_polygon_collection, draw_polygon, draw_rectangle,
draw_regpoly_collection

ولأسف، فإنّ ازدياد توابع الواجهة الخلفية يعني أنّ كتابة واجهة خلفية جديدة استغرقت زمنًا أطول بكثير، وحين كانت ميزات جديدة تُضاف إلى النواة، كان تحديث الواجهات الخلفية القائمة يتطلّب عملًا كبيرًا. وبما أنّ كل واجهة خلفية كان ينفّذها مطوّر واحد خبير في صيغة ملفات إخراج معيّنة، فإنّ وصول ميزة جديدة إلى جميع الواجهات الخلفية كان يستغرق أحيانًا زمنًا طويلًا، مما يسبّب ارتباكًا للمستخدم حول أي الميزات متاحة وأين.

مع الإصدار 0.98 من matplotlib، أُعيدت هيكلة الواجهات الخلفية بحيث تطلّب الحد الأدنى الضروري من الوظائف داخلها نفسها، ونُقل كل ما عدا ذلك إلى النواة. وخُفِّض عدد التوابع المطلوبة في واجهة الواجهة الخلفية البرمجية بصورة كبيرة، ليقتصر على:

  • draw_path: يرسم مضلعات مركّبة مكوّنة من مقاطع خطوط وBézier. وتُحلّ هذه الواجهة محلّ العديد من التوابع القديمة: draw_arc وdraw_line وdraw_lines وdraw_rectangle.
  • draw_image: يرسم صورًا نقطية.
  • draw_text: يرسم نصًا بخصائص الخط المعطاة.
  • get_text_width_height_descent: يعطى سلسلة نصية، يعيد مقاييسها.

من الممكن تنفيذ كل عمليات الرسم اللازمة لواجهة خلفية جديدة باستخدام هذه التوابع وحدها. (ومن الممكن أيضًا المضي خطوة أبعد ورسم النص باستخدام draw_path، مما يُغني عن الحاجة إلى تابع draw_text، لكننا لم نرقّ إلى إجراء هذا التبسيط. ومن ثمّ تستطيع الواجهة الخلفية طبعًا أن تنفّذ تابع draw_text خاصًا بها لإخراج «نص حقيقي».) وهذا مفيد لتشغيل واجهة خلفية جديدة بيسر أكبر. غير أنّه في بعض الحالات قد ترغب الواجهة الخلفية في تجاوز سلوك النواة لإنتاج مخرجات أكثر كفاءة. فمثلًا، عند رسم العلامات (markers) — الرموز الصغيرة المستخدمة للدلالة على الرؤوس في الرسم الخطي — فإنّ كتابة شكل العلامة في الملف مرة واحدة فقط ثم تكرارها بوصفها «ختمًا» (stamp) في كل موضع يُستخدَم فيه أكثر كفاءة من ناحية المساحة. وفي هذه الحالة يمكن أن تنفّذ الواجهة الخلفية تابعًا اسمه draw_markers. فإن كان منفَّذًا، تكتب الواجهة الخلفية شكل العلامة مرة واحدة ثم تكتب أمرًا أقصر بكثير لإعادة استخدامه في مواضع عديدة. وإن لم يكن منفَّذًا، فإنّ النواة ببساطة ترسم العلامة عدّة مرّات باستخدام عدّة استدعاءات لـdraw_path.

والقائمة الكاملة لتوابع واجهة الواجهة الخلفية البرمجية الاختيارية هي:

  • draw_markers: يرسم مجموعة من العلامات.
  • draw_path_collection: يرسم مجموعة من المسارات.
  • draw_quad_mesh: يرسم شبكة من الأشكال الرباعية.

11.4. التحويلات

يقضي matplotlib وقتًا طويلًا في تحويل الإحداثيات من نظام إلى آخر. وتشمل أنظمة الإحداثيات هذه:

  • البيانات: قيم البيانات الخام الأصلية
  • المحاور: الفضاء الذي يحدّده مستطيل محاور معيّن
  • الشكل: الفضاء الذي يحتوي الشكل بأسره
  • العرض: الإحداثيات الفيزيائية المستخدمة في الإخراج (مثل النقاط في PostScript والبكسلات في PNG)

يملك كل Artist عقدة تحويل تعرف كيف تحوّل من نظام إحداثيات إلى آخر. وتُوصل عقد التحويل هذه ببعضها في رسم بياني موجّه (directed graph)، بحيث تعتمد كل عقدة على أباها. وباتّباع الحواف حتى جذر الرسم البياني، يمكن تحويل الإحداثيات في فضاء البيانات جميعًا إلى الإحداثيات في ملف الإخراج النهائي. ومعظم التحويلات قابلة للعكس أيضًا. وهذا يجعل من الممكن النقر على عنصر في الرسم البياني وإعادة إحداثياته في فضاء البيانات. ينشئ رسم التحويلات التبعيات بين عقد التحويل: حين يتغيّر تحويل عقدة أب، مثلًا حين تتغيّر حدود Axes، فإنّ أي تحويلات مرتبطة بتلك Axes تصبح غير صالحة لأنها ستحتاج إلى إعادة الرسم. أما التحويلات المرتبطة بـAxes أخرى في الشكل، فيمكن بطبيعة الحال تركها وشأنها، مما يمنع إعادة الحساب غير الضرورية ويسهم في أداء تفاعلي أفضل.

قد تكون عقد التحويل إمّا تحويلات affine بسيطة وإمّا تحويلات لا affine. وتحويلات affine هي عائلة التحويلات التي تحفظ الاستقيمات ونسب المسافات، ومنها الدوران والإزاحة والقياس والانحراف. أمّا تحويلات affine ثنائية الأبعاد فتُمثَّل بمصفوفة تحويل affine بالأبعاد 3×3. النقطة المحوَّلة (x', y') تُحصل بضرب النقطة الأصلية (x, y) في هذه المصفوفة:

يمكن بعد ذلك تحويل الإحداثيات ثنائية الأبعاد بسهولة بمجرد ضربها في مصفوفة التحويل. وتتمتع تحويلات affine أيضًا بخاصية مفيدة وهي أنها يمكن تركيبها معًا باستخدام ضرب المصفوفات. وهذا يعني أنّه لتنفيذ سلسلة تحويلات affine يمكن أولًا ضرب مصفوفات التحويل معًا مرة واحدة فقط، ثم استخدام المصفوفة الناتجة لتحويل الإحداثيات. ويُركِّب إطار تحويلات matplotlib تلقائيًا مصفوفات التحويل affine (يثبّتها) قبل تحويل الإحداثيات لتقليل حجم الحساب. ووجود تحويلات affine سريعة مهم، لأنه يجعل التنقّل التفاعلي والتكبير داخل نافذة واجهة رسومية أكثر كفاءة.

تُعرَّف التحويلات غير affine في matplotlib باستخدام دوال Python، فهي إذن عشوائية إلى حدٍّ كبير. وداخل نواة matplotlib، تُستخدم التحويلات غير affine للقياس اللوغاريتمي، وللرسوم القطبية، والإسقاطات الجغرافية (الشكل 11.5). ويمكن خلط هذه التحويلات غير affine بحرّية مع تحويلات affine داخل رسم التحويلات. وسيبسّط matplotlib تلقائيًا الجزء affine ولا يعود إلى الدوال العشوائية إلا في الجزء غير affine.

الشكل 11.5: البيانات نفسها مرسومة بثلاث تحويلات غير affine مختلفة: لوغاريتمي وقطبي ولامبرت

من هذه القطع البسيطة يستطيع matplotlib إنجاز أمور متقدّمة إلى حدٍّ لا بأس به. التحويل المدمج (blended transformation) هو عقدة تحويل خاصة تستخدم تحويلًا واحدًا لمحور x وتحويلًا آخر لمحور y. وبطبيعة الحال فإنّ ذلك ممكن فقط إذا كانت التحويلات المعطاة «قابلة للفصل» (separable)، أي إنّ إحداثيتي x وy مستقلّتان، لكنّ التحويلات نفسها قد تكون affine أو غير affine. يُستخدم هذا مثلًا لرسم الرسوم اللوغاريتمية حيث قد يكون لأحد محوري x وy أو لكليهما مقياس لوغاريتمي. ووجود عقدة تحويل مدمجة يتيح دمج المقاييس المتاحة بطرق اعتباطية. وشيء آخر يسمح به رسم التحويلات هو مشاركة المحاور. فمن الممكن «ربط» حدود رسم بياني بآخر والتأكد من أنّ أحدهما يُحدَّث ليطابق الآخر عند تنقّل أحدهما أو تكبيره. وفي هذه الحالة تُشارَك عقدة التحويل نفسها ببساطة بين محورين، وقد يكونان حتى على شكلين مختلفين. ويبيّن الشكل 11.6 مثالًا على رسم تحويلات تعمل فيه بعض هذه الميزات المتقدّمة. فمحور x لوغاريتمي، بينما يتشارك المحوران y نفسه.

الشكل 11.6: مثال على رسم تحويلات

11.5. خط أنابيب الخطوط المتعددة المقاطع

عند رسم رسوم خطية، تُنفَّذ عدّة خطوات للانتقال من البيانات الخام إلى الخط المرسوم على الشاشة. وفي إصدار أقدم من matplotlib كانت جميع هذه الخطوات متشابكة معًا. وقد أُعيدت هيكلتها منذ ذلك الحين لتصبح خطوات منفصلة في خط أنابيب «تحويل المسار» (path conversion). وهذا يتيح لكل واجهة خلفية أن تختار أي أجزاء من خط الأنابيب لتنفيذها، إذ إنّ بعضها مفيد فقط في سياقات معيّنة.

  • التحويل: تُحوَّل الإحداثيات من إحداثيات البيانات إلى إحداثيات الشكل. فإن كان هذا تحويلًا affine خالصًا، كما هو موضَّح أعلاه، فالأمر بسيط مثل ضرب مصفوفة. وإن تضمّن تحويلات عشوائية، فإنّ دوال التحويل تُستدعى لتحويل الإحداثيات إلى فضاء الشكل.
  • معالجة البيانات المفقودة: قد يحتوي مصفوفة البيانات على أجزاء تكون فيها البيانات مفقودة أو غير صالحة. وقد يعمد المستخدم إلى ذلك إمّا بوضع تلك القيم على NaN أو باستخدام مصفوفات numpy المقنّعة. أمّا صيغ الإخراج المتجهية مثل PDF ومكتبات التصيير مثل Agg، فليست لديها عادةً مفهوم للبيانات المفقودة عند رسم خط متعدد المقاطع، ولذلك يجب أن تتخطّى هذه الخطوة من خط الأنابيب مقاطع البيانات المفقودة باستخدام أوامر MOVETO، التي تطلب من المُصيِّر رفع القلم وبدء الرسم من جديد عند نقطة جديدة.
  • القصّ: يمكن للنقاط الواقعة خارج حدود الشكل أن تزيد حجم الملف عبر تضمين نقاط كثيرة غير مرئية. والأهم من ذلك أنّ قيم الإحداثيات الكبيرة جدًّا أو الصغيرة جدًّا قد تتسبّب في أخطاء التجاوز (overflow) أثناء تصيير ملف الإخراج، مما ينتج عنه مخرجات مشوّهة تمامًا. تقصّ هذه الخطوة من خط الأنابيب الخط المتعدد المقاطع عند دخوله إلى حواف الشكل وخروجها منه لمنع المشكلتين معًا.
  • المحاذاة إلى البكسل: قد تبدو الخطوط العمودية والأفقية تمامًا ضبابية بسبب تنعيم الحواف (anti-aliasing) عندما لا تكون مراكزها محاذية لمركز بكسل (انظر الشكل 11.7). تحدّد خطوة المحاذاة في خط الأنابيب أولًا ما إذا كان الخط المتعدد المقاطع بأكمله مكوّنًا من مقاطع أفقية ورأسية (مثل مستطيل محاذٍ للمحاور)، وإن كان كذلك فتقرّب كل رأس ناتج إلى أقرب مركز بكسل. ولا تُستخدم هذه الخطوة إلا مع الواجهات الخلفية النقطية، لأنّ الواجهات الخلفية المتجهية ينبغي أن تحتفظ بنقاط بيانات دقيقة. وهناك بعض مُصيّرات صيغ الملفات المتجهية، مثل Adobe Acrobat، تؤدّي محاذاة البكسل عند العرض على الشاشة. الشكل 11.7: عرض مقرّب لأثر محاذاة البكسل. على اليسار، من دون محاذاة البكسل؛ وعلى اليمين، مع محاذاة البكسل.
  • التبسيط: عند رسم رسوم شديدة الكثافة، قد لا تكون كثير من النقاط على الخط مرئية فعليًا. وينطبق هذا بصفة خاصة على الرسوم التي تمثّل شكل موجة مشوّشة. وإدراج هذه النقاط في الرسم يزيد حجم الملف، وقد يتجاوز حتى الحدود المسموح بها لعدد النقاط في صيغة الملف. ولذلك تُحذف أي نقاط تقع تمامًا على الوتر بين نقطتيها المجاورتين (انظر الشكل 11.8). ويعتمد هذا التحديد على عتبة مبنية على ما سيكون مرئيًا عند دقّة معيّنة يحدّدها المستخدم. الشكل 11.8: الشكل على اليمين هو عرض مقرّب للشكل على اليسار. وتُحذف الرأس المأحاط بدائرة تلقائيًا بواسطة خوارزمية تبسيط المسار، لأنه يقع تمامًا على الوتر بين رأسيه المجاورين، وبالتالي فهو زائد عن الحاجة.

11.6. النص الرياضي

لأنّ مستخدمي matplotlib غالبًا ما يكونون علماء، فإنّ من المفيد وضع تعبيرات رياضية غنية التنسيق مباشرةً على الرسم. ولعلّ أكثر صياغات التعبيرات الرياضية انتشارًا هي الصياغة المستمدّة من نظام التنضيد TeX لدونالد نوث. وهي طريقة لتحويل مُدخل بلغة نصية عادية مثل هذه:

δxδy \sqrt{\frac{\delta x}{\delta y}}

إلى تعبير رياضي منسَّق على نحو صحيح.

يوفّر matplotlib طريقتين لتصيير التعبيرات الرياضية. الأولى هي usetex، التي تستخدم نسخة كاملة من TeX على جهاز المستخدم لتصيير التعبير الرياضي. يُخرج TeX مواضع المحارف والأسطر داخل التعبير بصيغته الأصلية DVI (device independent). ثم يحلّل matplotlib ملف DVI ويحوّله إلى مجموعة من أوامر الرسم التي يصيّرها إحدى واجهاته الخلفية مباشرةً على الرسم. وهذا الأسلوب يتعامل مع قدر كبير من الصياغات الرياضية الغامضة. غير أنّه يتطلّب أن يكون لدى المستخدم تثبيت كامل وسليم لـTeX. ولذلك يضمّ matplotlib أيضًا محرّك تصيير رياضي داخلي خاصًا به، يُدعى mathtext.

‏mathtext هو منفذ مباشر لمحرّك تصيير الرياضيات في TeX، مركَّب فوق محلّل أبسط بكثير كُتب باستخدام إطار عمل التحليل pyparsing McG07. وقد كُتب هذا المنفذ استنادًا إلى النسخة المنشورة من شيفرة مصدر TeX Knu86. يبني المحلّل البسيط شجرة من الصناديق (boxes) واللصق (glue) — بتسمية TeX — ثم يخطيطها محرّك التخطيط. وبالرغم من تضمين محرّك تصيير الرياضيات الكامل الخاص بـTeX، فإنّ المجموعة الكبيرة من مكتبات الرياضيات الخاصة بـTeX وLaTeX من أطراف ثالثة غير مضمّنة. وتُنقل ميزات هذه المكتبات بحسب الحاجة، مع التركيز أولًا على الميزات كثيرة الاستخدام وغير الخاصة بمجال معيّن. ويتيح هذا طريقة لطيفة وخفيفة الوزن لتصيير معظم التعبيرات الرياضية.

11.7. اختبار الانحدار

تاريخيًا، لم يكن لدى matplotlib عدد كبير من اختبارات الوحدة منخفضة المستوى. فإذا بُلِّغ عن خطأ فادح، كان يُضاف نص برمجي لإعادة إنتاجه إلى مجلد من هذا النوع في شجرة المصدر. وخلق غياب الاختبارات الآلية جميع المشكلات المعتادة، وأهمّها الانحدارات في ميزات كانت تعمل في السابق. (ولعلّنا لا نحتاج إلى بيع فكرة أنّ الاختبار الآلي شيء جيّد.) ومن ثمّ، مع كل هذه الشيفرة وكثرة خيارات الإعداد والقطع القابلة للتبادل (مثل الواجهات الخلفية)، يمكن القول إنّ اختبارات الوحدة منخفضة المستوى وحدها ما كانت لتكفي أبدًا؛ وبدلًا من ذلك اتّبعنا قناعة بأنّ اختبار جميع القطع معًا ودون أنصافها هو الأكثر جدوى من حيث الكلفة.

ولهذا الغرض، وكخطوة أولى، كُتب نص برمجي يولّد عددًا من الرسوم البيانية تُمارس ميزات مختلفة من matplotlib، ولا سيما تلك الصعبة الضبط. وقد جعل ذلك كشف ما إذا كان تغيير جديد قد أحدث كسرًا غير مقصود أسهل قليلًا، لكنّ صحة الصور ظلّت بحاجة إلى التحقّق اليدوي. ولأنّ ذلك يتطلّب جهدًا يدويًا كبيرًا، فلم يُجرَى كثيرًا.

وفي مرحلة ثانية، تمّ أتمتة هذا الأسلوب العام. فالنص البرمجي الحالي لاختبار matplotlib يولّد عددًا من الرسوم، لكن بدلًا من الحاجة إلى تدخل يدوي، تُقارَن تلك الرسوم آليًا بصور خط الأساس (baseline). وتُنفَّذ جميع الاختبارات داخل إطار عمل الاختبار nose، مما يجعل توليد تقرير بالاختبارات الفاشلة سهلًا جدًّا.

وتتعقّد الأمور لأنّ مقارنة الصور لا يمكن أن تكون مطابقة تمامًا. فتحتاج التغييرات الدقيقة في إصدارات مكتبة تصيير الخطوط Freetype أن تجعل مخرجات النص تختلف قليلًا بين الأجهزة المختلفة. وهذه الفروق لا تكفي لاعتبارها «خاطئة»، لكنها تكفي لإفساد أي مقارنة مطابقة بت-بت (bit-for-bit). وبدلًا من ذلك، يحسب إطار الاختبار المدرّج التكراري (histogram) للصورتين، ويحسب الجذر التربيعي للمتوسط (root-mean-square) لفرقهما. فإن كان هذا الفرق أكبر من عتبة معيّنة، تُعدّ الصور مختلفتين أكثر من اللازم ويفشل اختبار المقارنة. وحين تفشل الاختبارات، تُولَّد صور للفرق تُظهر أين وقع التغيير في الرسم (انظر الشكل 11.9). ويستطيع المطوّر بعد ذلك أن يقرّر ما إذا كان الفشل ناتجًا عن تغيير مقصود فيحدّث صورة خط الأساس لتطابق الصورة الجديدة، أو يقرّر أنّ الصورة غير صحيحة فعلًا فيتتبّع الخلل الذي سبّب التغيير ويصلحه.

الشكل 11.9: مقارنة صور في اختبار انحدار. من اليسار إلى اليمين: أ) الصورة المتوقَّعة، ب) نتيجة خلل في موضع مفتاح التسميات التوضيحية (legend)، ج) الفرق بين الصورتين.

وبما أنّ الواجهات الخلفية المختلفة قد تسهم بأخطاء مختلفة، فإنّ إطار الاختبار يختبر عدّة واجهات خلفية لكل رسم: PNG وPDF وSVG. أما الصيغ المتجهية فلا نقارن فيها معلومات التجه مباشرة، لأنّ هناك طرقًا متعدّدة لتمثيل الشيء نفسه بحيث تكون النتيجة النهائية واحدة عند التحويل إلى صورة نقطية. وينبغي أن تكون الواجهات الخلفية المتجهية حرّة في تغيير تفاصيل مخرجاتها لزيادة الكفاءة دون أن تتسبّب في فشل جميع الاختبارات. ولذلك، في حالة الواجهات الخلفية المتجهية، يصيّر إطار الاختبار الملف أولًا إلى صورة نقطية باستخدام أداة خارجية (Ghostscript لملفات PDF وInkscape لملفات SVG) ثم يستخدم تلك الصور النقطية في المقارنة.

وباستخدام هذا الأسلوب، تمكّنّا من إطلاق إطار اختبار فعّال إلى حدٍّ معقول من الصفر بميسرة أكبر مما لو ذهبنا إلى كتابة كثير من اختبارات الوحدة منخفضة المستوى. ومع ذلك فهو ليس مثاليًا؛ فغطاء الشيفرة للاختبارات ليس كاملًا جدًّا، واستغرق تشغيل جميع الاختبارات وقتًا طويلًا. (نحو 15 دقيقة على معالج Intel Core 2 E6550 بتردد 2.33 غيغاهرتز.) لذلك ما زالت بعض حالات الانحدار تتسلّل دون انتباه، لكنّ جودة الإصدارات قد تحسّنت إلى حدٍّ كبير منذ تنفيذ إطار الاختبار.

11.8. الدروس المستفادة

ومن الدروس المهمّة المستفادة من تطوير matplotlib، كما قال لوكوربوزييه، «المهندسون الجيّدون يستعيرون». فقد كان المؤلّفان الأوّلان لـmatplotlib إلى حدٍّ كبير عالمَين ومبرمجَين تعلّما مهنتَهما ذاتيًا ويحاولان إنجاز عملهما، لا عالمَي حاسوب مدرَّبين رسميًا. ومن ثمّ لم نصل بالتصميم الداخلي إلى الصواب من المحاولة الأولى. وقد أفاد المشروع قرارُ تنفيذ طبقة برمجة نصية موجّهة للمستخدم ومتوافقة إلى حدٍّ كبير مع واجهة MATLAB البرمجية بثلاث طرق مهمّة: فقد وفّر واجهةً مجرَّبة على مدار الزمن لإنشاء الرسوم وتخصيصها، وجعل الانتقال إلى matplotlib سهلًا بالنسبة لقاعدة المستخدمين الكُبرى في MATLAB، و— وهو الأهمّ بالنسبة لنا في سياق بنية matplotlib — حرّر المطوّرين لإعادة هيكلة واجهة الكائنات الداخلية عدّة مرّات بأثر ضئيل على معظم المستخدمين لأنّ واجهة البرمجة النصية لم تتغيّر. وبينما كان لدينا مستخدمو الواجهة البرمجية (بخلاف مستخدمي البرمجة النصية) منذ البداية، فإنّ معظمهم مستخدمو طاقة (power users) أو مطوّرون قادرون على التأقلم مع تغيّرات الواجهة البرمجية. أما مستخدمو البرمجة النصية، من ناحيتهم، فيمكنهم كتابة الشيفرة مرة واحدة وتقريبًا أن يفترضوا أنها ستظلّ مستقرّة في جميع الإصدارات اللاحقة.

وفيما يخصّ واجهة الرسم الداخلية، فبينما استعَرنا فعلًا من GDK، لم نبذل جهدًا كافيًا لتحديد ما إذا كانت هذه هي واجهة الرسم الصحيحة، واضطررنا إلى إنفاق جهد كبير لاحقًا — بعد أن كُتبت واجهات خلفية كثيرة حول هذه الواجهة البرمجية — لتوسيع الوظائف حول واجهة رسم أبسط وأكثر مرونة. وقد كان من دواعي سعادتنا لو تبنّينا مواصفة الرسم في PDF Ent11b، التي طُوّرت بدورها من عقود الخبرة التي راكمتها Adobe في مواصفة PostScript الخاصة بها؛ فقد كان ذلك سيمتحنا إلى حدٍّ كبير توافقًا جاهزًا مع PDF نفسه، وإطار Quartz Core Graphics، وطقم الرسم Enthought Enable Kiva Ent11a.

ومن مساوئ Python واحدة أنّه لغة سهلة ومعبِّرة إلى هذا الحدّ، بحيث يجد المطوّرون غالبًا من الأسهل إعادة اختراع الوظائف الموجودة في حزم أخرى وإعادة تنفيذها بدلًا من العمل على تكامل الشيفرة من حزم أخرى. وكان matplotlib كان سيستفاد في بدايات تطوره من إنفاق جهد أكبر على التكامل مع الوحدات والواجهات البرمجية القائمة، مثل طقمَي Enthought Kiva وEnable اللذين يحلان مشكلات كثيرة مماثلة، بدلًا من إعادة اختراع الوظائف. غير أنّ التكامل مع الوظائف القائمة سلاح ذو حدّين، إذ قد يجعل البناءات والإصدارات أكثر تعقيدًا ويقلّل المرونة في التطوير الداخلي.