مقال تقني

مخططات Excel Waterfall وTreemap في Delphi مع HotXLS

يكتب HotXLS مخططات Excel من نوع waterfall وtreemap كأجزاء cx:chartSpace مستقلة — xl/charts/chartExN.xml الذي يحمل نوع المحتوى application/vnd.ms-office.chartex+xml — ويرسيها عبر غلاف mc:AlternateContent، فتعرض Excel 2016 وما بعدها المخطط بينما يحصل القرّاء الأقدم على شكل احتياطي. اختر xlsxChartWaterfall أو xlsxChartTreemap وسيسير خط أنابيب الأجزاء بالكامل خلف ذلك

الطريقة التي يعلن بها هذا النوع من المشاكل عن نفسه عادةً هي تذكرة دعم. يرسل عميل مصنفاً بناه فريق المالية لديه في Excel 2019، تفتحه أداة التقارير لديك، تحفظه، فيختفي مخطط waterfall. لا استثناء، لا تحذير، لا شيء في السجل — المخطط ببساطة غير موجود في المخرجات. وإن ذهبت تبحث عنه في مسار كود المخططات التقليدي فستبحث طويلاً، لأن المخطط لم يكن هناك من الأساس

مرساة mc:AlternateContent التي يكتبها HotXLS لمخطط chartEx: فرع mc:Choice يتطلب cx1 ويحمل xdr:graphicFrame يشير a:graphicData فيه إلى مساحة أسماء chartex مع علاقة cx:chart عارية، بينما يحمل mc:Fallback مستطيل ملاحظة يرسمه القرّاء الأقدم بدلاً من ذلك
مرساة mc:AlternateContent توجه مخطط chartEx بين Excel 2016 والقرّاء الأقدم: فرع Choice يتطلب cx1 ويحمل إطار chartex، بينما يحمل Fallback مستطيل الملاحظة الذي يجب على القرّاء الأقدم رسمه

لماذا ليست مخططات waterfall وtreemap داخل c:chartSpace؟

لأن Microsoft لم تستطع وضعها هناك. مخطط عناصر الرسم في ECMA-376 الجزء 1 §21.2 يعدد عناصر المخطط — c:barChart وc:lineChart وc:pieChart والباقي — كاختيار مغلق، لذا فإن إضافة waterfall وtreemap وsunburst وfunnel في Excel 2016 كانت ستعني كسر مخطط يعتمد عليه كل قارئ موجود. ذهبت الأنواع الجديدة إلى مستند موازٍ بدلاً من ذلك: جزء cx:chartSpace في مساحة أسماء chartex لعام 2014، وهي http://schemas.microsoft.com/office/drawing/2014/chartex، مخزّن بجانب المخططات التقليدية لكنه غير مرتبط بها بنيوياً. الجانب الرسومي يربط العالمين معاً بـ mc:AlternateContent، آلية التوافق من ECMA-376 الجزء 3: فرع mc:Choice يصرح بـ Requires="cx1" ويحمل xdr:graphicFrame يكون فيه a:graphicData/@uri هو مساحة أسماء chartex وحمولته إشارة عارية <cx:chart r:id="rIdN"/>، بينما يحمل mc:Fallback مستطيلاً بالملاحظة التي تكتبها Excel نفسها هناك. القارئ الذي لا يفهم cx1 ملزَم بموجب عقد الجزء 3 بتبني الفرع الاحتياطي

نوع العلاقة هو المؤشر الآخر. المخطط التقليدي يتدلى من جزء الرسم بعلاقة مخطط OOXML القياسية؛ أما جزء chartEx فيستخدم http://schemas.microsoft.com/office/2014/relationships/chartEx بدلاً منها، وهو موثق في [MS-ODRAWXML]. يكشف HotXLS العالمين كثوابت — XlsxRtChartEx لتلك العلاقة، وXlsxCtChartEx لنوع المحتوى، وXlsxNsMsChartEx وXlsxNsMsChartEx2015 لمساحتي الأسماء اللتين تحتاجهما المرساة — والدالة المنطقية XlsxChartIsChartEx هي التي ترسل المخطط إلى المسار الموسع عند الحفظ. المخططات المدمجة وXML المخطط التقليدي المحفوظ تسلك المسار الآخر، وهو مشروح في مقال XML المخطط المحفوظ والمخططات المدمجة

أربع قواعد في المخطط تحدد ما إذا كان Excel سيفتح الملف

مخطط chartex أكثر تشدداً من التقليدي في أربعة مواضع محددة، وكل واحد منها ينتج نفس مطالبة الإصلاح غير المفيدة عندما تخطئ. يدمج BuildChartExKnownXml وBuildChartExRelsXml في HotXLS الأربعة كلها، لكن معرفتها تستحق العناء إن فحصت XML يدوياً يوماً ما

  • الترتيب. يجب أن يأتي cx:chartData قبل cx:chart في المستند. وهذا عكس الحدس الذي تبنيه من المخططات التقليدية، حيث تعيش المخازن المؤقتة داخل السلسلة داخل منطقة الرسم؛ أما في جزء chartEx فالبيانات تجلس في المقدمة داخل كتلة خاصة بها ولا يشير إليها جسم المخطط إلا بمعرّف
  • العددية. قد يحتوي cx:plotArea على cx:plotAreaRegion واحد بالضبط. لا صفر ولا اثنان. لا حكاية مخططات مدمجة هنا — منطقة واحدة، والسلسلة بداخلها
  • مكان النوع. نوع المخطط ليس اسم عنصر إطلاقاً. إنه سمة layoutId على cx:series، وتأخذ رمزاً من تعداد ST_SeriesLayout في مخطط chartex من Microsoft. يربط XlsxChartExLayoutId تعداد HotXLS بذلك الرمز ويعيد 'waterfall' أو 'treemap'؛ ويعيد XlsxChartExLayoutIdToType التوجيه بالعكس عند القراءة
  • العلاقة التي لا تحتاجها المخططات التقليدية أبداً. يحمل cx:chartData عنصر <cx:externalData r:id="rId1"/>، وتلك العلاقة يجب أن توجد في xl/charts/_rels/chartExN.xml.rels مشيرةً إلى ورقة العمل التي تملك البيانات، لأن Excel يستخدمها لحل صيغ النطاقات cx:f. احذف جزء rels وسيفتح المخطط فارغاً بلا ما يفسر السبب

كيف تضيف مخطط waterfall أو treemap في Delphi؟

بالطريقة نفسها التي تضيف بها أي مخطط آخر في HotXLS — الأنواع الموسعة أعضاء في TXLSXChartType، لذا يستقبلها TXLSXWorksheet.AddChart مباشرةً ويعتمد كل ما بعد ذلك على قيمة التعداد. لا توجد واجهة chartEx منفصلة لتتعلمها

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Chart: TXLSXChart;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('cashflow.xlsx');
    Sheet := Book.Sheets[0];

    // مستطيل التثبيت بإحداثيات صف/عمود، تمامًا كالمخطط الكلاسيكي
    Chart := Sheet.AddChart(xlsxChartWaterfall, 'Q3 Cash Flow',
      2, 5, 20, 12);
    Chart.AddSeries('Movement', 'Sheet1!$A$2:$A$9', 'Sheet1!$B$2:$B$9');

    Book.SaveAs('cashflow-waterfall.xlsx');
  finally
    Book.Free;
  end;
end;
خط أنابيب الحفظ في HotXLS للمخططات الموسعة: AddChart مع xlsxChartWaterfall أو xlsxChartTreemap يصل إلى XlsxChartIsChartEx، التي ترسل المخططات التقليدية إلى عداد مشترك ومخططات chartEx إلى عداد مستقل يكتب chartEx1.xml وملف rels الذي يشير إلى ورقة العمل وتجاوز نوع محتوى chartex وحمولة مرساة AlternateContent
نموذج مخطط واحد ودالة توجيه واحدة يغذيان عدادين عند الحفظ: المخططات التقليدية تتشارك chart1.xml وchart2.xml، بينما يهبط waterfall باسم chartEx1.xml لأن عداد chartEx مستقل عن العداد التقليدي

عند الحفظ، يرى HotXLS XlsxChartIsChartEx تعيد True، ويخصص اسم جزء من عداد chartEx مستقل، ويكتب xl/charts/chartEx1.xml وملف rels الخاص به وتجاوز نوع المحتوى ومرساة mc:AlternateContent التي ينتجها XlsxChartExAnchorPayloadXml. العداد المستقل مهم: مصنف فيه مخططان تقليديان ومخطط waterfall واحد يحصل على chart1.xml وchart2.xml وchartEx1.xml، لا chartEx3.xml. ثم يختلف treemap عن waterfall في تفصيلين، كلاهما يعالجه نفس الباني. البعد الرقمي يغيّر نوعه — cx:numDim type="val" في waterfall يصبح type="size" في treemap، لأن مستطيل treemap يرمّز المقدار كمساحة لا كقيمة مرسومة — وtreemap عادةً يريد تسلسلاً هرمياً للفئات، وهو ما يعبّر عنه النموذج عبر TXLSXChartSeries.CategoriesMultiLevel. فعّل تلك العلامة وسيقرأ S.ResolveCategoryLevels النطاق من ورقة العمل وقت الكتابة، ويقسمه إلى TXLSXChartDataCache واحد لكل مستوى خلف S.CategoryLevelCount وS.CategoryLevels[]، ويصدر BuildChartExLevelXml عنصر cx:lvl واحداً لكل مستوى داخل cx:strDim واحد، مستوى الورقة أولاً — الترتيب الفيزيائي نفسه الذي يستخدمه ECMA-376 الجزء 1 §21.2 مع c:multiLvlStrCache، لذا تنتقل تلك العادة حتى وإن لم ينتقل أي من الترميز

Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
  2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
  'Sheet1!$A$2:$B$40',   // عمودان: تسمية الورقة، ثم المجموعة الأم
  'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;

شكل المخزن المؤقت الذي يكسر كل افتراضات المخططات التقليدية

إن كتبت قارئ مخازن مؤقتة لمخطط تقليدي من قبل، فثلاث عادات ستخونك مع جزء chartEx، والثلاث جميعها نابعة من قرار تصميمي واحد: chartex يخزن في السمات ومحتوى العناصر ما يخزنه DrawingML في عناصر فرعية

  • النقطة المخزنة هي <cx:pt idx="0">42</cx:pt>. القيمة هي محتوى العنصر. لا يوجد عنصر فرعي c:v تنزل إليه، لذا فالمحلل الذي يمشي بحثاً عن عقد v لا يجد شيئاً ويبلغ عن مخزن مؤقت فارغ
  • ptCount وformatCode سمتان على cx:lvl. في c:numCache التقليدي كلاهما عنصران فرعيان
  • مستويات التسلسل الهرمي المتعددة هي عدة عناصر cx:lvl داخل cx:strDim واحد. لا يوجد عنصر مخزن مؤقت متعدد المستويات منفصل كما أن c:multiLvlStrCache منفصل عن c:strCache

يكتب BuildChartExLevelXml ذلك الشكل ويعيد ParseChartExXml قراءته، مرراً كل قيمة عبر XlsxEscapeText في طريق الخروج حتى تنجو تسمية فئة تحوي علامة عطف أو قوس زاوية من رحلة الذهاب والإياب. يجدر ملاحظة ما يفعله الباني حين لا يحمل مستوى رقمي رمز تنسيق: يكتب formatCode="General" بدلاً من إسقاط السمة، لأن السمة هنا ليست اختيارية كما هي نظيرتها التقليدية

أشكال مخازن مؤقتة جنباً إلى جنب في أجزاء chartEx من HotXLS: c:numCache التقليدي يخزن ptCount وformatCode كعنصرين فرعيين بقيم داخل c:v، بينما يخزنهما cx:numDim كسمتين على cx:lvl مع القيمة كمحتوى عنصر، وتصدر CategoriesMultiLevel عنصر cx:lvl لكل مستوى فئة، الورقة أولاً، داخل cx:strDim واحد
chartex يخزن في السمات ومحتوى العناصر ما يخزنه DrawingML في عناصر فرعية: القيمة تجلس مباشرة داخل cx:pt، ويصبح ptCount وformatCode سمتين على cx:lvl، والتسلسل الهرمي للفئات عدة عناصر cx:lvl في cx:strDim واحد

بادئة مساحة الأسماء التي كسرت قراءة النوع

النسخة الأولى من محلل chartEx في HotXLS كتبت ملفات جيدة تماماً، وأعادت قراءة كل واحد منها كمخطط أعمدة. السبب الجذري كان سطراً واحداً، والدرس يعمم على أي محلل OOXML ستكتبه يوماً: TXMLReader.Name يعيد الاسم كما ورد في المستند، بادئته معه. في جزء مخطط تقليدي تصل معظم العناصر بلا بادئة، لذا يعمل SameText(Reader.Name, 'series') وتتوقف عن التفكير في الأمر. أما في جزء chartEx فكل عنصر مسبوق — يعطيك القارئ 'cx:series'، وتفشل المقارنة بصمت على جميعها، ولا تحصل على استثناء ولا XML تالف، فقط قائمة سلاسل فارغة ونوع مخطط يتراجع إلى الافتراضي. الإصلاح هو المقارنة على الاسم المحلي، في مكان واحد لا في كل موضع استدعاء

// Reader.Name يحمل بادئة مساحة الاسم الخاصة به: 'cx:series'، وليس 'series'
function IsEl(const LocalName: string): Boolean;
var
  NodeName: string;
begin
  NodeName := String(Reader.Name);
  Result := SameText(NodeName, LocalName) or
            EndsText(':' + LocalName, NodeName);
end;

يستخدم ParseChartExXml تلك الدالة المساعدة في كل فحص عنصر، ودالة مساعدة مكافئة AttrVal للسمات، لأن cx:autoUpdate وأصدقاءه مسبوقون أيضاً. ما إن تُرى عناصر السلسلة فعلاً حتى يعود النوع من أول layoutId يُصادف، وتُربط كل cx:series بكتلة cx:data الخاصة بها بمطابقة cx:dataId/@val مع cx:data/@id. المخططات المفتوحة دون تعديل تسلك المسار الأقدم بدلاً من ذلك: البايتات الأصلية تُعاد من FRawChartXml، لذا يخرج ملف العميل غير الملموس كما دخل — نفس انضباط الكاتب المتحفظ الذي يحكم امتدادات مستوى الورقة في مقال امتداد sparkline

ما لا يفعله HotXLS عمداً مع هذه المخططات

حدّان يستحقان أن يُذكرا بوضوح، لأن كليهما خيار لا ثغرة بانتظار سدها. عارضتا PDF وSVG المدمجتان لا ترسمان سلاسل waterfall أو treemap: الأنواع لا تطابق أي فرع رسم، لذا يُتخطى رسم السلسلة ويُصدَّر باقي الورقة بشكل طبيعي — تزييف treemap ذي مظهر مقنع في PDF مصدّر أسوأ من عدم رسم أي شيء، لأنه سيناقض بصمت ما تعرضه Excel للملف نفسه. وxlsxChartWaterfall وxlsxChartTreemap هما فقط النوعان الموسعان الوحيدان اللذان يبنيهما HotXLS من النموذج؛ كما يعرّف ST_SeriesLayout sunburst وfunnel وbox-and-whisker وhistogram، ومصنف يحوي أياً منها ما زال يمر برحلة الذهاب والإياب عبر مسار إعادة تشغيل XML الخام، فقط لا يمكنك تأليف أحدها من كود Delphi اليوم. رسم أنواع المخططات التقليدية لم يتغير ومشروح في مقال المخططات والصور والرسومات، ويأتي خط أنابيب chartEx بجانبه في HotXLS Delphi Excel Component لـ Delphi وC++Builder