Chart   F
last analyzed

Complexity

Total Complexity 341

Size/Duplication

Total Lines 1914
Duplicated Lines 0 %

Test Coverage

Coverage 97.77%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 341
eloc 1248
dl 0
loc 1914
ccs 1227
cts 1255
cp 0.9777
rs 0.8
c 1
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A writeView3D() 0 9 4
A writeCalculatedTitle() 0 49 4
A writeSerAxis() 0 43 1
F writePlotArea() 0 173 46
B writeTitle() 0 44 6
B writeLegend() 0 56 5
F writeCategoryAxis() 0 220 33
A writeDataLabelsBool() 0 6 3
B writeDataLabels() 0 61 9
C writeChart() 0 102 10
F writeValueAxis() 0 215 35
A writePlotSeriesValuesElement() 0 16 3
B writeLineStyles() 0 48 8
A writeEffects() 0 12 4
A writeNotEmpty() 0 4 3
A writeBubbles() 0 35 5
A getChartType() 0 22 6
F writeLabelFont() 0 49 13
D writePlotSeriesValues() 0 84 18
A writePrintSettings() 0 21 1
B writeColor() 0 30 7
A writeGlow() 0 10 2
A writeAlternateContent() 0 21 1
F writePlotGroup() 0 307 88
A writePlotSeriesLabel() 0 25 3
B writeLayout() 0 60 9
A writeSoftEdge() 0 9 2
C writeShadow() 0 42 12

How to fix   Complexity   

Complex Class

Complex classes like Chart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Chart, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4
5
use PhpOffice\PhpSpreadsheet\Chart\Axis;
6
use PhpOffice\PhpSpreadsheet\Chart\ChartColor;
7
use PhpOffice\PhpSpreadsheet\Chart\DataSeries;
8
use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues;
9
use PhpOffice\PhpSpreadsheet\Chart\Layout;
10
use PhpOffice\PhpSpreadsheet\Chart\Legend;
11
use PhpOffice\PhpSpreadsheet\Chart\PlotArea;
12
use PhpOffice\PhpSpreadsheet\Chart\Properties;
13
use PhpOffice\PhpSpreadsheet\Chart\Title;
14
use PhpOffice\PhpSpreadsheet\Chart\TrendLine;
15
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
16
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
17
use PhpOffice\PhpSpreadsheet\Style\Font;
18
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
19
20
class Chart extends WriterPart
21
{
22
    private int $seriesIndex;
23
24
    /**
25
     * Write charts to XML format.
26
     *
27
     * @return string XML Output
28
     */
29 86
    public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, bool $calculateCellValues = true): string
30
    {
31
        // Create XML writer
32 86
        $objWriter = null;
33 86
        if ($this->getParentWriter()->getUseDiskCaching()) {
34
            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
35
        } else {
36 86
            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
37
        }
38
        //    Ensure that data series values are up-to-date before we save
39 86
        if ($calculateCellValues) {
40 86
            $chart->refresh();
41
        }
42
43
        // XML header
44 86
        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
45
46
        // c:chartSpace
47 86
        $objWriter->startElement('c:chartSpace');
48 86
        $objWriter->writeAttribute('xmlns:c', Namespaces::CHART);
49 86
        $objWriter->writeAttribute('xmlns:a', Namespaces::DRAWINGML);
50 86
        $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT);
51
52 86
        $objWriter->startElement('c:date1904');
53 86
        $objWriter->writeAttribute('val', '0');
54 86
        $objWriter->endElement();
55 86
        $objWriter->startElement('c:lang');
56 86
        $objWriter->writeAttribute('val', 'en-GB');
57 86
        $objWriter->endElement();
58 86
        $objWriter->startElement('c:roundedCorners');
59 86
        $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0');
60 86
        $objWriter->endElement();
61
62 86
        $this->writeAlternateContent($objWriter);
63
64 86
        $objWriter->startElement('c:chart');
65
66 86
        $this->writeTitle($objWriter, $chart->getTitle());
67
68 86
        $objWriter->startElement('c:autoTitleDeleted');
69 86
        $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted());
70 86
        $objWriter->endElement();
71
72 86
        $objWriter->startElement('c:view3D');
73 86
        $surface2D = false;
74 86
        $plotArea = $chart->getPlotArea();
75 86
        if ($plotArea !== null) {
76 86
            $seriesArray = $plotArea->getPlotGroup();
77 86
            foreach ($seriesArray as $series) {
78 86
                if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) {
79 2
                    $surface2D = true;
80
81 2
                    break;
82
                }
83
            }
84
        }
85 86
        $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90);
86 86
        $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D);
87 86
        $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D);
88 86
        $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D);
89 86
        $objWriter->endElement(); // view3D
90
91 86
        $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY());
92
93 86
        $this->writeLegend($objWriter, $chart->getLegend());
94
95 86
        $objWriter->startElement('c:plotVisOnly');
96 86
        $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly());
97 86
        $objWriter->endElement();
98
99 86
        if ($chart->getDisplayBlanksAs() !== '') {
100 85
            $objWriter->startElement('c:dispBlanksAs');
101 85
            $objWriter->writeAttribute('val', $chart->getDisplayBlanksAs());
102 85
            $objWriter->endElement();
103
        }
104
105 86
        $objWriter->startElement('c:showDLblsOverMax');
106 86
        $objWriter->writeAttribute('val', '0');
107 86
        $objWriter->endElement();
108
109 86
        $objWriter->endElement(); // c:chart
110
111 86
        $objWriter->startElement('c:spPr');
112 86
        if ($chart->getNoFill()) {
113 3
            $objWriter->startElement('a:noFill');
114 3
            $objWriter->endElement(); // a:noFill
115
        }
116 86
        $fillColor = $chart->getFillColor();
117 86
        if ($fillColor->isUsable()) {
118 5
            $this->writeColor($objWriter, $fillColor);
119
        }
120 86
        $borderLines = $chart->getBorderLines();
121 86
        $this->writeLineStyles($objWriter, $borderLines);
122 86
        $this->writeEffects($objWriter, $borderLines);
123 86
        $objWriter->endElement(); // c:spPr
124
125 86
        $this->writePrintSettings($objWriter);
126
127 86
        $objWriter->endElement(); // c:chartSpace
128
129
        // Return
130 86
        return $objWriter->getData();
131
    }
132
133 86
    private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void
134
    {
135 86
        if ($value === null && $surface2D) {
136 1
            $value = $default;
137
        }
138 86
        if ($value !== null) {
139 6
            $objWriter->startElement($tag);
140 6
            $objWriter->writeAttribute('val', "$value");
141 6
            $objWriter->endElement();
142
        }
143
    }
144
145
    /**
146
     * Write Chart Title.
147
     */
148 86
    private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void
149
    {
150 86
        if ($title === null) {
151 12
            return;
152
        }
153 81
        if ($this->writeCalculatedTitle($objWriter, $title)) {
154 2
            return;
155
        }
156
157 79
        $objWriter->startElement('c:title');
158 79
        $caption = $title->getCaption();
159 79
        if ($caption !== null) {
160 79
            $objWriter->startElement('c:tx');
161 79
            $objWriter->startElement('c:rich');
162
163 79
            $objWriter->startElement('a:bodyPr');
164 79
            $objWriter->endElement(); // a:bodyPr
165
166 79
            $objWriter->startElement('a:lstStyle');
167 79
            $objWriter->endElement(); // a:lstStyle
168
169 79
            $objWriter->startElement('a:p');
170 79
            $objWriter->startElement('a:pPr');
171 79
            $objWriter->startElement('a:defRPr');
172 79
            $objWriter->endElement(); // a:defRPr
173 79
            $objWriter->endElement(); // a:pPr
174
175 79
            if (is_array($caption)) {
176 36
                $caption = $caption[0] ?? '';
177
            }
178 79
            $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
179
180 79
            $objWriter->endElement(); // a:p
181 79
            $objWriter->endElement(); // c:rich
182 79
            $objWriter->endElement(); // c:tx
183
        }
184
185 79
        $this->writeLayout($objWriter, $title->getLayout());
186
187 79
        $objWriter->startElement('c:overlay');
188 79
        $objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0');
189 79
        $objWriter->endElement(); // c:overlay
190
191 79
        $objWriter->endElement(); // c:title
192
    }
193
194
    /**
195
     * Write Calculated Chart Title.
196
     */
197 81
    private function writeCalculatedTitle(XMLWriter $objWriter, Title $title): bool
198
    {
199 81
        $calc = $title->getCalculatedTitle($this->getParentWriter()->getSpreadsheet());
200 81
        if (empty($calc)) {
201 79
            return false;
202
        }
203
204 2
        $objWriter->startElement('c:title');
205 2
        $objWriter->startElement('c:tx');
206 2
        $objWriter->startElement('c:strRef');
207 2
        $objWriter->writeElement('c:f', $title->getCellReference());
208 2
        $objWriter->startElement('c:strCache');
209
210 2
        $objWriter->startElement('c:ptCount');
211 2
        $objWriter->writeAttribute('val', '1');
212 2
        $objWriter->endElement(); // c:ptCount
213 2
        $objWriter->startElement('c:pt');
214 2
        $objWriter->writeAttribute('idx', '0');
215 2
        $objWriter->writeElement('c:v', $calc);
216 2
        $objWriter->endElement(); // c:pt
217
218 2
        $objWriter->endElement(); // c:strCache
219 2
        $objWriter->endElement(); // c:strRef
220 2
        $objWriter->endElement(); // c:tx
221
222 2
        $this->writeLayout($objWriter, $title->getLayout());
223
224 2
        $objWriter->startElement('c:overlay');
225 2
        $objWriter->writeAttribute('val', ($title->getOverlay()) ? '1' : '0');
226 2
        $objWriter->endElement(); // c:overlay
227
        // c:spPr
228
229
        // c:txPr
230 2
        $labelFont = $title->getFont();
231 2
        if ($labelFont !== null) {
232 2
            $objWriter->startElement('c:txPr');
233
234 2
            $objWriter->startElement('a:bodyPr');
235 2
            $objWriter->endElement(); // a:bodyPr
236 2
            $objWriter->startElement('a:lstStyle');
237 2
            $objWriter->endElement(); // a:lstStyle
238 2
            $this->writeLabelFont($objWriter, $labelFont, null);
239
240 2
            $objWriter->endElement(); // c:txPr
241
        }
242
243 2
        $objWriter->endElement(); // c:title
244
245 2
        return true;
246
    }
247
248
    /**
249
     * Write Chart Legend.
250
     */
251 86
    private function writeLegend(XMLWriter $objWriter, ?Legend $legend = null): void
252
    {
253 86
        if ($legend === null) {
254 14
            return;
255
        }
256
257 78
        $objWriter->startElement('c:legend');
258
259 78
        $objWriter->startElement('c:legendPos');
260 78
        $objWriter->writeAttribute('val', $legend->getPosition());
261 78
        $objWriter->endElement();
262
263 78
        $this->writeLayout($objWriter, $legend->getLayout());
264
265 78
        $objWriter->startElement('c:overlay');
266 78
        $objWriter->writeAttribute('val', ($legend->getOverlay()) ? '1' : '0');
267 78
        $objWriter->endElement();
268
269 78
        $objWriter->startElement('c:spPr');
270 78
        $fillColor = $legend->getFillColor();
271 78
        if ($fillColor->isUsable()) {
272 3
            $this->writeColor($objWriter, $fillColor);
273
        }
274 78
        $borderLines = $legend->getBorderLines();
275 78
        $this->writeLineStyles($objWriter, $borderLines);
276 78
        $this->writeEffects($objWriter, $borderLines);
277 78
        $objWriter->endElement(); // c:spPr
278
279 78
        $legendText = $legend->getLegendText();
280 78
        $objWriter->startElement('c:txPr');
281 78
        $objWriter->startElement('a:bodyPr');
282 78
        $objWriter->endElement();
283
284 78
        $objWriter->startElement('a:lstStyle');
285 78
        $objWriter->endElement();
286
287 78
        $objWriter->startElement('a:p');
288 78
        $objWriter->startElement('a:pPr');
289 78
        $objWriter->writeAttribute('rtl', '0');
290
291 78
        $objWriter->startElement('a:defRPr');
292 78
        if ($legendText !== null) {
293 3
            $this->writeColor($objWriter, $legendText->getFillColorObject());
294 3
            $this->writeEffects($objWriter, $legendText);
295
        }
296 78
        $objWriter->endElement(); // a:defRpr
297 78
        $objWriter->endElement(); // a:pPr
298
299 78
        $objWriter->startElement('a:endParaRPr');
300 78
        $objWriter->writeAttribute('lang', 'en-US');
301 78
        $objWriter->endElement(); // a:endParaRPr
302
303 78
        $objWriter->endElement(); // a:p
304 78
        $objWriter->endElement(); // c:txPr
305
306 78
        $objWriter->endElement(); // c:legend
307
    }
308
309
    /**
310
     * Write Chart Plot Area.
311
     */
312 86
    private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null): void
313
    {
314 86
        if ($plotArea === null) {
315
            return;
316
        }
317
318 86
        $id1 = $id2 = $id3 = '0';
319 86
        $this->seriesIndex = 0;
320 86
        $objWriter->startElement('c:plotArea');
321
322 86
        $layout = $plotArea->getLayout();
323
324 86
        $this->writeLayout($objWriter, $layout);
325
326 86
        $chartTypes = self::getChartType($plotArea);
327 86
        $catIsMultiLevelSeries = $valIsMultiLevelSeries = false;
328 86
        $plotGroupingType = '';
329 86
        $chartType = null;
330 86
        foreach ($chartTypes as $chartType) {
331 86
            $objWriter->startElement('c:' . $chartType);
332
333 86
            $groupCount = $plotArea->getPlotGroupCount();
334 86
            $plotGroup = null;
335 86
            for ($i = 0; $i < $groupCount; ++$i) {
336 86
                $plotGroup = $plotArea->getPlotGroupByIndex($i);
337 86
                $groupType = $plotGroup->getPlotType();
338 86
                if ($groupType == $chartType) {
339 86
                    $plotStyle = $plotGroup->getPlotStyle();
340 86
                    if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) {
341 2
                        $objWriter->startElement('c:radarStyle');
342 2
                        $objWriter->writeAttribute('val', $plotStyle);
343 2
                        $objWriter->endElement();
344 85
                    } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) {
345 27
                        $objWriter->startElement('c:scatterStyle');
346 27
                        $objWriter->writeAttribute('val', $plotStyle);
347 27
                        $objWriter->endElement();
348 64
                    } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) {
349 2
                        $objWriter->startElement('c:wireframe');
350 2
                        $objWriter->writeAttribute('val', $plotStyle ? '1' : '0');
351 2
                        $objWriter->endElement();
352
                    }
353
354 86
                    $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType);
355
                }
356
            }
357
358 86
            $this->writeDataLabels($objWriter, $layout);
359
360 86
            if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) {
361
                //    Line only, Line3D can't be smoothed
362 19
                $objWriter->startElement('c:smooth');
363 19
                $objWriter->writeAttribute('val', (string) (int) $plotGroup->getSmoothLine());
364 19
                $objWriter->endElement();
365 71
            } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) {
366 27
                $objWriter->startElement('c:gapWidth');
367 27
                $objWriter->writeAttribute('val', '150');
368 27
                $objWriter->endElement();
369
370 27
                if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') {
371 3
                    $objWriter->startElement('c:overlap');
372 3
                    $objWriter->writeAttribute('val', '100');
373 27
                    $objWriter->endElement();
374
                }
375 56
            } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) {
376 2
                $scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle();
377 2
                if ($scale !== '') {
378 1
                    $objWriter->startElement('c:bubbleScale');
379 1
                    $objWriter->writeAttribute('val', $scale);
380 1
                    $objWriter->endElement();
381
                }
382
383 2
                $objWriter->startElement('c:showNegBubbles');
384 2
                $objWriter->writeAttribute('val', '0');
385 2
                $objWriter->endElement();
386 55
            } elseif ($chartType === DataSeries::TYPE_STOCKCHART) {
387 5
                $objWriter->startElement('c:hiLowLines');
388 5
                $objWriter->endElement();
389
390 5
                $gapWidth = $plotArea->getGapWidth();
391 5
                $upBars = $plotArea->getUseUpBars();
392 5
                $downBars = $plotArea->getUseDownBars();
393 5
                if ($gapWidth !== null || $upBars || $downBars) {
394 2
                    $objWriter->startElement('c:upDownBars');
395 2
                    if ($gapWidth !== null) {
396 2
                        $objWriter->startElement('c:gapWidth');
397 2
                        $objWriter->writeAttribute('val', "$gapWidth");
398 2
                        $objWriter->endElement();
399
                    }
400 2
                    if ($upBars) {
401 2
                        $objWriter->startElement('c:upBars');
402 2
                        $objWriter->endElement();
403
                    }
404 2
                    if ($downBars) {
405 2
                        $objWriter->startElement('c:downBars');
406 2
                        $objWriter->endElement();
407
                    }
408 2
                    $objWriter->endElement(); // c:upDownBars
409
                }
410
            }
411
412
            //    Generate 3 unique numbers to use for axId values
413 86
            $id1 = '110438656';
414 86
            $id2 = '110444544';
415 86
            $id3 = '110365312'; // used in Surface Chart
416
417 86
            if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
418 81
                $objWriter->startElement('c:axId');
419 81
                $objWriter->writeAttribute('val', $id1);
420 81
                $objWriter->endElement();
421 81
                $objWriter->startElement('c:axId');
422 81
                $objWriter->writeAttribute('val', $id2);
423 81
                $objWriter->endElement();
424 81
                if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) {
425 2
                    $objWriter->startElement('c:axId');
426 2
                    $objWriter->writeAttribute('val', $id3);
427 81
                    $objWriter->endElement();
428
                }
429
            } else {
430 9
                $objWriter->startElement('c:firstSliceAng');
431 9
                $objWriter->writeAttribute('val', '0');
432 9
                $objWriter->endElement();
433
434 9
                if ($chartType === DataSeries::TYPE_DONUTCHART) {
435 5
                    $objWriter->startElement('c:holeSize');
436 5
                    $objWriter->writeAttribute('val', '50');
437 5
                    $objWriter->endElement();
438
                }
439
            }
440
441 86
            $objWriter->endElement();
442
        }
443
444 86
        if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) {
445 81
            if ($chartType === DataSeries::TYPE_BUBBLECHART) {
446 2
                $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis());
447
            } else {
448 80
                $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis());
449
            }
450
451 81
            $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis());
452 81
            if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) {
453 2
                $this->writeSerAxis($objWriter, $id2, $id3);
454
            }
455
        }
456 86
        $stops = $plotArea->getGradientFillStops();
457 86
        if ($plotArea->getNoFill() || !empty($stops)) {
458 14
            $objWriter->startElement('c:spPr');
459 14
            if ($plotArea->getNoFill()) {
460 12
                $objWriter->startElement('a:noFill');
461 12
                $objWriter->endElement(); // a:noFill
462
            }
463 14
            if (!empty($stops)) {
464 3
                $objWriter->startElement('a:gradFill');
465 3
                $objWriter->startElement('a:gsLst');
466 3
                foreach ($stops as $stop) {
467 3
                    $objWriter->startElement('a:gs');
468 3
                    $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0]));
469 3
                    $this->writeColor($objWriter, $stop[1], false);
470 3
                    $objWriter->endElement(); // a:gs
471
                }
472 3
                $objWriter->endElement(); // a:gsLst
473 3
                $angle = $plotArea->getGradientFillAngle();
474 3
                if ($angle !== null) {
475 3
                    $objWriter->startElement('a:lin');
476 3
                    $objWriter->writeAttribute('ang', Properties::angleToXml($angle));
477 3
                    $objWriter->endElement(); // a:lin
478
                }
479 3
                $objWriter->endElement(); // a:gradFill
480
            }
481 14
            $objWriter->endElement(); // c:spPr
482
        }
483
484 86
        $objWriter->endElement(); // c:plotArea
485
    }
486
487 47
    private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void
488
    {
489 47
        if ($value !== null) {
490 43
            $objWriter->startElement("c:$name");
491 43
            $objWriter->writeAttribute('val', $value ? '1' : '0');
492 43
            $objWriter->endElement();
493
        }
494
    }
495
496
    /**
497
     * Write Data Labels.
498
     */
499 86
    private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void
500
    {
501 86
        if (!isset($chartLayout)) {
502 40
            return;
503
        }
504 47
        $objWriter->startElement('c:dLbls');
505
506 47
        $fillColor = $chartLayout->getLabelFillColor();
507 47
        $borderColor = $chartLayout->getLabelBorderColor();
508 47
        if ($fillColor && $fillColor->isUsable()) {
509 3
            $objWriter->startElement('c:spPr');
510 3
            $this->writeColor($objWriter, $fillColor);
511 3
            if ($borderColor && $borderColor->isUsable()) {
512 2
                $objWriter->startElement('a:ln');
513 2
                $this->writeColor($objWriter, $borderColor);
514 2
                $objWriter->endElement(); // a:ln
515
            }
516 3
            $objWriter->endElement(); // c:spPr
517
        }
518 47
        $labelFont = $chartLayout->getLabelFont();
519 47
        if ($labelFont !== null) {
520 7
            $objWriter->startElement('c:txPr');
521
522 7
            $objWriter->startElement('a:bodyPr');
523 7
            $objWriter->writeAttribute('wrap', 'square');
524 7
            $objWriter->writeAttribute('lIns', '38100');
525 7
            $objWriter->writeAttribute('tIns', '19050');
526 7
            $objWriter->writeAttribute('rIns', '38100');
527 7
            $objWriter->writeAttribute('bIns', '19050');
528 7
            $objWriter->writeAttribute('anchor', 'ctr');
529 7
            $objWriter->startElement('a:spAutoFit');
530 7
            $objWriter->endElement(); // a:spAutoFit
531 7
            $objWriter->endElement(); // a:bodyPr
532
533 7
            $objWriter->startElement('a:lstStyle');
534 7
            $objWriter->endElement(); // a:lstStyle
535 7
            $this->writeLabelFont($objWriter, $labelFont, $chartLayout->getLabelEffects());
536
537 7
            $objWriter->endElement(); // c:txPr
538
        }
539
540 47
        if ($chartLayout->getNumFmtCode() !== '') {
541 3
            $objWriter->startElement('c:numFmt');
542 3
            $objWriter->writeAttribute('formatCode', $chartLayout->getnumFmtCode());
543 3
            $objWriter->writeAttribute('sourceLinked', (string) (int) $chartLayout->getnumFmtLinked());
544 3
            $objWriter->endElement(); // c:numFmt
545
        }
546 47
        if ($chartLayout->getDLblPos() !== '') {
547 4
            $objWriter->startElement('c:dLblPos');
548 4
            $objWriter->writeAttribute('val', $chartLayout->getDLblPos());
549 4
            $objWriter->endElement(); // c:dLblPos
550
        }
551 47
        $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey());
552 47
        $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal());
553 47
        $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName());
554 47
        $this->writeDataLabelsBool($objWriter, 'showSerName', $chartLayout->getShowSerName());
555 47
        $this->writeDataLabelsBool($objWriter, 'showPercent', $chartLayout->getShowPercent());
556 47
        $this->writeDataLabelsBool($objWriter, 'showBubbleSize', $chartLayout->getShowBubbleSize());
557 47
        $this->writeDataLabelsBool($objWriter, 'showLeaderLines', $chartLayout->getShowLeaderLines());
558
559 47
        $objWriter->endElement(); // c:dLbls
560
    }
561
562
    /**
563
     * Write Category Axis.
564
     */
565 80
    private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, string $id1, string $id2, bool $isMultiLevelSeries, Axis $yAxis): void
566
    {
567
        // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc
568
        // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART).
569 80
        $axisType = $yAxis->getAxisType();
570 80
        if ($axisType !== '') {
571 37
            $objWriter->startElement("c:$axisType");
572 45
        } elseif ($yAxis->getAxisIsNumericFormat()) {
573 7
            $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE);
574
        } else {
575 38
            $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY);
576
        }
577 80
        $majorGridlines = $yAxis->getMajorGridlines();
578 80
        $minorGridlines = $yAxis->getMinorGridlines();
579
580 80
        if ($id1 !== '0') {
581 80
            $objWriter->startElement('c:axId');
582 80
            $objWriter->writeAttribute('val', $id1);
583 80
            $objWriter->endElement();
584
        }
585
586 80
        $objWriter->startElement('c:scaling');
587 80
        if (is_numeric($yAxis->getAxisOptionsProperty('logBase'))) {
588
            $logBase = $yAxis->getAxisOptionsProperty('logBase') + 0;
589
            if ($logBase >= 2 && $logBase <= 1000) {
590
                $objWriter->startElement('c:logBase');
591
                $objWriter->writeAttribute('val', (string) $logBase);
592
                $objWriter->endElement();
593
            }
594
        }
595 80
        if ($yAxis->getAxisOptionsProperty('maximum') !== null) {
596 9
            $objWriter->startElement('c:max');
597 9
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum'));
598 9
            $objWriter->endElement();
599
        }
600 80
        if ($yAxis->getAxisOptionsProperty('minimum') !== null) {
601 9
            $objWriter->startElement('c:min');
602 9
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum'));
603 9
            $objWriter->endElement();
604
        }
605 80
        if (!empty($yAxis->getAxisOptionsProperty('orientation'))) {
606 80
            $objWriter->startElement('c:orientation');
607 80
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation'));
608 80
            $objWriter->endElement();
609
        }
610 80
        $objWriter->endElement(); // c:scaling
611
612 80
        $objWriter->startElement('c:delete');
613 80
        $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0');
614 80
        $objWriter->endElement();
615
616 80
        $objWriter->startElement('c:axPos');
617 80
        $objWriter->writeAttribute('val', 'b');
618 80
        $objWriter->endElement();
619
620 80
        if ($majorGridlines !== null) {
621 5
            $objWriter->startElement('c:majorGridlines');
622 5
            $objWriter->startElement('c:spPr');
623 5
            $this->writeLineStyles($objWriter, $majorGridlines);
624 5
            $this->writeEffects($objWriter, $majorGridlines);
625 5
            $objWriter->endElement(); //end spPr
626 5
            $objWriter->endElement(); //end majorGridLines
627
        }
628
629 80
        if ($minorGridlines !== null && $minorGridlines->getObjectState()) {
630 5
            $objWriter->startElement('c:minorGridlines');
631 5
            $objWriter->startElement('c:spPr');
632 5
            $this->writeLineStyles($objWriter, $minorGridlines);
633 5
            $this->writeEffects($objWriter, $minorGridlines);
634 5
            $objWriter->endElement(); //end spPr
635 5
            $objWriter->endElement(); //end minorGridLines
636
        }
637
638 80
        if ($xAxisLabel !== null) {
639 11
            $objWriter->startElement('c:title');
640 11
            $caption = $xAxisLabel->getCaption();
641 11
            if ($caption !== null) {
642 11
                $objWriter->startElement('c:tx');
643 11
                $objWriter->startElement('c:rich');
644
645 11
                $objWriter->startElement('a:bodyPr');
646 11
                $objWriter->endElement(); // a:bodyPr
647
648 11
                $objWriter->startElement('a:lstStyle');
649 11
                $objWriter->endElement(); // a::lstStyle
650
651 11
                $objWriter->startElement('a:p');
652
653 11
                if (is_array($caption)) {
654 6
                    $caption = $caption[0];
655
                }
656 11
                $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
657
658 11
                $objWriter->endElement(); // a:p
659 11
                $objWriter->endElement(); // c:rich
660 11
                $objWriter->endElement(); // c:tx
661
            }
662
663 11
            $layout = $xAxisLabel->getLayout();
664 11
            $this->writeLayout($objWriter, $layout);
665
666 11
            $objWriter->startElement('c:overlay');
667 11
            $objWriter->writeAttribute('val', '0');
668 11
            $objWriter->endElement(); // c:overlay
669
670 11
            $objWriter->endElement(); // c:title
671
        }
672
673 80
        $objWriter->startElement('c:numFmt');
674 80
        $objWriter->writeAttribute('formatCode', $yAxis->getAxisNumberFormat());
675 80
        $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked());
676 80
        $objWriter->endElement();
677
678 80
        if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) {
679 80
            $objWriter->startElement('c:majorTickMark');
680 80
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark'));
681 80
            $objWriter->endElement();
682
        }
683
684 80
        if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) {
685 80
            $objWriter->startElement('c:minorTickMark');
686 80
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark'));
687 80
            $objWriter->endElement();
688
        }
689
690 80
        if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) {
691 80
            $objWriter->startElement('c:tickLblPos');
692 80
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels'));
693 80
            $objWriter->endElement();
694
        }
695
696 80
        $textRotation = $yAxis->getAxisOptionsProperty('textRotation');
697 80
        $axisText = $yAxis->getAxisText();
698
699 80
        if ($axisText !== null || is_numeric($textRotation)) {
700 11
            $objWriter->startElement('c:txPr');
701 11
            $objWriter->startElement('a:bodyPr');
702 11
            if (is_numeric($textRotation)) {
703 8
                $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
704
            }
705 11
            $objWriter->endElement(); // a:bodyPr
706 11
            $objWriter->startElement('a:lstStyle');
707 11
            $objWriter->endElement(); // a:lstStyle
708 11
            $this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
709 11
            $objWriter->endElement(); // c:txPr
710
        }
711
712 80
        $objWriter->startElement('c:spPr');
713 80
        $this->writeColor($objWriter, $yAxis->getFillColorObject());
714 80
        $this->writeLineStyles($objWriter, $yAxis, $yAxis->getNoFill());
715 80
        $this->writeEffects($objWriter, $yAxis);
716 80
        $objWriter->endElement(); // spPr
717
718 80
        if ($yAxis->getAxisOptionsProperty('major_unit') !== null) {
719 4
            $objWriter->startElement('c:majorUnit');
720 4
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit'));
721 4
            $objWriter->endElement();
722
        }
723
724 80
        if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) {
725 2
            $objWriter->startElement('c:minorUnit');
726 2
            $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit'));
727 2
            $objWriter->endElement();
728
        }
729
730 80
        if ($id2 !== '0') {
731 80
            $objWriter->startElement('c:crossAx');
732 80
            $objWriter->writeAttribute('val', $id2);
733 80
            $objWriter->endElement();
734
735 80
            if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) {
736 80
                $objWriter->startElement('c:crosses');
737 80
                $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses'));
738 80
                $objWriter->endElement();
739
            }
740
        }
741
742 80
        $objWriter->startElement('c:auto');
743
        // LineChart with dateAx wants '0'
744 80
        $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1');
745 80
        $objWriter->endElement();
746
747 80
        $objWriter->startElement('c:lblAlgn');
748 80
        $objWriter->writeAttribute('val', 'ctr');
749 80
        $objWriter->endElement();
750
751 80
        $objWriter->startElement('c:lblOffset');
752 80
        $objWriter->writeAttribute('val', '100');
753 80
        $objWriter->endElement();
754
755 80
        if ($axisType === Axis::AXIS_TYPE_DATE) {
756 3
            $property = 'baseTimeUnit';
757 3
            $propertyVal = $yAxis->getAxisOptionsProperty($property);
758 3
            if (!empty($propertyVal)) {
759 3
                $objWriter->startElement("c:$property");
760 3
                $objWriter->writeAttribute('val', $propertyVal);
761 3
                $objWriter->endElement();
762
            }
763 3
            $property = 'majorTimeUnit';
764 3
            $propertyVal = $yAxis->getAxisOptionsProperty($property);
765 3
            if (!empty($propertyVal)) {
766 3
                $objWriter->startElement("c:$property");
767 3
                $objWriter->writeAttribute('val', $propertyVal);
768 3
                $objWriter->endElement();
769
            }
770 3
            $property = 'minorTimeUnit';
771 3
            $propertyVal = $yAxis->getAxisOptionsProperty($property);
772 3
            if (!empty($propertyVal)) {
773 3
                $objWriter->startElement("c:$property");
774 3
                $objWriter->writeAttribute('val', $propertyVal);
775 3
                $objWriter->endElement();
776
            }
777
        }
778
779 80
        if ($isMultiLevelSeries) {
780 18
            $objWriter->startElement('c:noMultiLvlLbl');
781 18
            $objWriter->writeAttribute('val', '0');
782 18
            $objWriter->endElement();
783
        }
784 80
        $objWriter->endElement();
785
    }
786
787
    /**
788
     * Write Value Axis.
789
     *
790
     * @param null|string $groupType Chart type
791
     */
792 81
    private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, ?string $groupType, string $id1, string $id2, bool $isMultiLevelSeries, Axis $xAxis): void
793
    {
794 81
        $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE);
795 81
        $majorGridlines = $xAxis->getMajorGridlines();
796 81
        $minorGridlines = $xAxis->getMinorGridlines();
797
798 81
        if ($id2 !== '0') {
799 81
            $objWriter->startElement('c:axId');
800 81
            $objWriter->writeAttribute('val', $id2);
801 81
            $objWriter->endElement();
802
        }
803
804 81
        $objWriter->startElement('c:scaling');
805 81
        if (is_numeric($xAxis->getAxisOptionsProperty('logBase'))) {
806 1
            $logBase = $xAxis->getAxisOptionsProperty('logBase') + 0;
807 1
            if ($logBase >= 2 && $logBase <= 1000) {
808 1
                $objWriter->startElement('c:logBase');
809 1
                $objWriter->writeAttribute('val', (string) $logBase);
810 1
                $objWriter->endElement();
811
            }
812
        }
813
814 81
        if ($xAxis->getAxisOptionsProperty('maximum') !== null) {
815 1
            $objWriter->startElement('c:max');
816 1
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('maximum'));
817 1
            $objWriter->endElement();
818
        }
819
820 81
        if ($xAxis->getAxisOptionsProperty('minimum') !== null) {
821 1
            $objWriter->startElement('c:min');
822 1
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minimum'));
823 1
            $objWriter->endElement();
824
        }
825
826 81
        if (!empty($xAxis->getAxisOptionsProperty('orientation'))) {
827 81
            $objWriter->startElement('c:orientation');
828 81
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation'));
829 81
            $objWriter->endElement();
830
        }
831
832 81
        $objWriter->endElement(); // c:scaling
833
834 81
        $objWriter->startElement('c:delete');
835 81
        $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0');
836 81
        $objWriter->endElement();
837
838 81
        $objWriter->startElement('c:axPos');
839 81
        $objWriter->writeAttribute('val', 'l');
840 81
        $objWriter->endElement();
841
842 81
        if ($majorGridlines !== null) {
843 39
            $objWriter->startElement('c:majorGridlines');
844 39
            $objWriter->startElement('c:spPr');
845 39
            $this->writeLineStyles($objWriter, $majorGridlines);
846 39
            $this->writeEffects($objWriter, $majorGridlines);
847 39
            $objWriter->endElement(); //end spPr
848 39
            $objWriter->endElement(); //end majorGridLines
849
        }
850
851 81
        if ($minorGridlines !== null && $minorGridlines->getObjectState()) {
852 6
            $objWriter->startElement('c:minorGridlines');
853 6
            $objWriter->startElement('c:spPr');
854 6
            $this->writeLineStyles($objWriter, $minorGridlines);
855 6
            $this->writeEffects($objWriter, $minorGridlines);
856 6
            $objWriter->endElement(); //end spPr
857 6
            $objWriter->endElement(); //end minorGridLines
858
        }
859
860 81
        if ($yAxisLabel !== null) {
861 42
            $objWriter->startElement('c:title');
862 42
            $caption = $yAxisLabel->getCaption();
863 42
            if ($caption !== null) {
864 42
                $objWriter->startElement('c:tx');
865 42
                $objWriter->startElement('c:rich');
866
867 42
                $objWriter->startElement('a:bodyPr');
868 42
                $objWriter->endElement(); // a:bodyPr
869
870 42
                $objWriter->startElement('a:lstStyle');
871 42
                $objWriter->endElement(); // a:lstStyle
872
873 42
                $objWriter->startElement('a:p');
874
875 42
                if (is_array($caption)) {
876 11
                    $caption = $caption[0];
877
                }
878 42
                $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a');
879
880 42
                $objWriter->endElement(); // a:p
881 42
                $objWriter->endElement(); // c:rich
882 42
                $objWriter->endElement(); // c:tx
883
            }
884
885 42
            if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
886 42
                $layout = $yAxisLabel->getLayout();
887 42
                $this->writeLayout($objWriter, $layout);
888
            }
889
890 42
            $objWriter->startElement('c:overlay');
891 42
            $objWriter->writeAttribute('val', '0');
892 42
            $objWriter->endElement(); // c:overlay
893
894 42
            $objWriter->endElement(); // c:title
895
        }
896
897 81
        $objWriter->startElement('c:numFmt');
898 81
        $objWriter->writeAttribute('formatCode', $xAxis->getAxisNumberFormat());
899 81
        $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked());
900 81
        $objWriter->endElement();
901
902 81
        if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) {
903 81
            $objWriter->startElement('c:majorTickMark');
904 81
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark'));
905 81
            $objWriter->endElement();
906
        }
907
908 81
        if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) {
909 81
            $objWriter->startElement('c:minorTickMark');
910 81
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark'));
911 81
            $objWriter->endElement();
912
        }
913
914 81
        if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) {
915 81
            $objWriter->startElement('c:tickLblPos');
916 81
            $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels'));
917 81
            $objWriter->endElement();
918
        }
919
920 81
        $textRotation = $xAxis->getAxisOptionsProperty('textRotation');
921 81
        $axisText = $xAxis->getAxisText();
922
923 81
        if ($axisText !== null || is_numeric($textRotation)) {
924 7
            $objWriter->startElement('c:txPr');
925 7
            $objWriter->startElement('a:bodyPr');
926 7
            if (is_numeric($textRotation)) {
927 1
                $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation));
928
            }
929 7
            $objWriter->endElement(); // a:bodyPr
930 7
            $objWriter->startElement('a:lstStyle');
931 7
            $objWriter->endElement(); // a:lstStyle
932
933 7
            $this->writeLabelFont($objWriter, ($axisText === null) ? null : $axisText->getFont(), $axisText);
934
935 7
            $objWriter->endElement(); // c:txPr
936
        }
937
938 81
        $objWriter->startElement('c:spPr');
939 81
        $this->writeColor($objWriter, $xAxis->getFillColorObject());
940 81
        $this->writeLineStyles($objWriter, $xAxis, $xAxis->getNoFill());
941 81
        $this->writeEffects($objWriter, $xAxis);
942 81
        $objWriter->endElement(); //end spPr
943
944 81
        if ($id1 !== '0') {
945 81
            $objWriter->startElement('c:crossAx');
946 81
            $objWriter->writeAttribute('val', $id1);
947 81
            $objWriter->endElement();
948
949 81
            if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) {
950
                $objWriter->startElement('c:crossesAt');
951
                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value'));
952
                $objWriter->endElement();
953
            } else {
954 81
                $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses');
955 81
                if ($crosses) {
956 81
                    $objWriter->startElement('c:crosses');
957 81
                    $objWriter->writeAttribute('val', $crosses);
958 81
                    $objWriter->endElement();
959
                }
960
            }
961
962 81
            $crossBetween = $xAxis->getCrossBetween();
963 81
            if ($crossBetween !== '') {
964 36
                $objWriter->startElement('c:crossBetween');
965 36
                $objWriter->writeAttribute('val', $crossBetween);
966 36
                $objWriter->endElement();
967
            }
968
969 81
            if ($xAxis->getAxisType() === Axis::AXIS_TYPE_VALUE) {
970 35
                $dispUnits = $xAxis->getAxisOptionsProperty('dispUnitsBuiltIn');
971 35
                $dispUnits = ($dispUnits == Axis::TRILLION_INDEX) ? Axis::DISP_UNITS_TRILLIONS : (is_numeric($dispUnits) ? (Axis::DISP_UNITS_BUILTIN_INT[(int) $dispUnits] ?? '') : $dispUnits);
972 35
                if (in_array($dispUnits, Axis::DISP_UNITS_BUILTIN_INT, true)) {
973 3
                    $objWriter->startElement('c:dispUnits');
974 3
                    $objWriter->startElement('c:builtInUnit');
975 3
                    $objWriter->writeAttribute('val', $dispUnits);
976 3
                    $objWriter->endElement(); // c:builtInUnit
977 3
                    if ($xAxis->getDispUnitsTitle() !== null) {
978
                        // TODO output title elements
979 2
                        $objWriter->writeElement('c:dispUnitsLbl');
980
                    }
981 3
                    $objWriter->endElement(); // c:dispUnits
982
                }
983
            }
984
985 81
            if ($xAxis->getAxisOptionsProperty('major_unit') !== null) {
986
                $objWriter->startElement('c:majorUnit');
987
                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_unit'));
988
                $objWriter->endElement();
989
            }
990
991 81
            if ($xAxis->getAxisOptionsProperty('minor_unit') !== null) {
992 1
                $objWriter->startElement('c:minorUnit');
993 1
                $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_unit'));
994 1
                $objWriter->endElement();
995
            }
996
        }
997
998 81
        if ($isMultiLevelSeries) {
999 1
            if ($groupType !== DataSeries::TYPE_BUBBLECHART) {
1000
                $objWriter->startElement('c:noMultiLvlLbl');
1001
                $objWriter->writeAttribute('val', '0');
1002
                $objWriter->endElement();
1003
            }
1004
        }
1005
1006 81
        $objWriter->endElement();
1007
    }
1008
1009
    /**
1010
     * Write Ser Axis, for Surface chart.
1011
     */
1012 2
    private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void
1013
    {
1014 2
        $objWriter->startElement('c:serAx');
1015
1016 2
        $objWriter->startElement('c:axId');
1017 2
        $objWriter->writeAttribute('val', $id3);
1018 2
        $objWriter->endElement(); // axId
1019
1020 2
        $objWriter->startElement('c:scaling');
1021 2
        $objWriter->startElement('c:orientation');
1022 2
        $objWriter->writeAttribute('val', 'minMax');
1023 2
        $objWriter->endElement(); // orientation
1024 2
        $objWriter->endElement(); // scaling
1025
1026 2
        $objWriter->startElement('c:delete');
1027 2
        $objWriter->writeAttribute('val', '0');
1028 2
        $objWriter->endElement(); // delete
1029
1030 2
        $objWriter->startElement('c:axPos');
1031 2
        $objWriter->writeAttribute('val', 'b');
1032 2
        $objWriter->endElement(); // axPos
1033
1034 2
        $objWriter->startElement('c:majorTickMark');
1035 2
        $objWriter->writeAttribute('val', 'out');
1036 2
        $objWriter->endElement(); // majorTickMark
1037
1038 2
        $objWriter->startElement('c:minorTickMark');
1039 2
        $objWriter->writeAttribute('val', 'none');
1040 2
        $objWriter->endElement(); // minorTickMark
1041
1042 2
        $objWriter->startElement('c:tickLblPos');
1043 2
        $objWriter->writeAttribute('val', 'nextTo');
1044 2
        $objWriter->endElement(); // tickLblPos
1045
1046 2
        $objWriter->startElement('c:crossAx');
1047 2
        $objWriter->writeAttribute('val', $id2);
1048 2
        $objWriter->endElement(); // crossAx
1049
1050 2
        $objWriter->startElement('c:crosses');
1051 2
        $objWriter->writeAttribute('val', 'autoZero');
1052 2
        $objWriter->endElement(); // crosses
1053
1054 2
        $objWriter->endElement(); //serAx
1055
    }
1056
1057
    /**
1058
     * Get the data series type(s) for a chart plot series.
1059
     *
1060
     * @return string[]
1061
     */
1062 86
    private static function getChartType(PlotArea $plotArea): array
1063
    {
1064 86
        $groupCount = $plotArea->getPlotGroupCount();
1065
1066 86
        if ($groupCount == 1) {
1067 84
            $plotType = $plotArea->getPlotGroupByIndex(0)->getPlotType();
1068 84
            $chartType = ($plotType === null) ? [] : [$plotType];
1069
        } else {
1070 3
            $chartTypes = [];
1071 3
            for ($i = 0; $i < $groupCount; ++$i) {
1072 3
                $plotType = $plotArea->getPlotGroupByIndex($i)->getPlotType();
1073 3
                if ($plotType !== null) {
1074 3
                    $chartTypes[] = $plotType;
1075
                }
1076
            }
1077 3
            $chartType = array_unique($chartTypes);
1078
        }
1079 86
        if (count($chartType) == 0) {
1080
            throw new WriterException('Chart is not yet implemented');
1081
        }
1082
1083 86
        return $chartType;
1084
    }
1085
1086
    /**
1087
     * Method writing plot series values.
1088
     */
1089 8
    private function writePlotSeriesValuesElement(XMLWriter $objWriter, int $val, ?ChartColor $fillColor): void
1090
    {
1091 8
        if ($fillColor === null || !$fillColor->isUsable()) {
1092 1
            return;
1093
        }
1094 8
        $objWriter->startElement('c:dPt');
1095
1096 8
        $objWriter->startElement('c:idx');
1097 8
        $objWriter->writeAttribute('val', "$val");
1098 8
        $objWriter->endElement(); // c:idx
1099
1100 8
        $objWriter->startElement('c:spPr');
1101 8
        $this->writeColor($objWriter, $fillColor);
1102 8
        $objWriter->endElement(); // c:spPr
1103
1104 8
        $objWriter->endElement(); // c:dPt
1105
    }
1106
1107
    /**
1108
     * Write Plot Group (series of related plots).
1109
     *
1110
     * @param string $groupType Type of plot for dataseries
1111
     * @param bool $catIsMultiLevelSeries Is category a multi-series category
1112
     * @param bool $valIsMultiLevelSeries Is value set a multi-series set
1113
     * @param string $plotGroupingType Type of grouping for multi-series values
1114
     */
1115 86
    private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, bool &$catIsMultiLevelSeries, bool &$valIsMultiLevelSeries, string &$plotGroupingType): void
1116
    {
1117 86
        if ($plotGroup === null) {
1118
            return;
1119
        }
1120
1121 86
        if (($groupType == DataSeries::TYPE_BARCHART) || ($groupType == DataSeries::TYPE_BARCHART_3D)) {
1122 27
            $objWriter->startElement('c:barDir');
1123 27
            $objWriter->writeAttribute('val', $plotGroup->getPlotDirection());
1124 27
            $objWriter->endElement();
1125
        }
1126
1127 86
        $plotGroupingType = $plotGroup->getPlotGrouping();
1128 86
        if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) {
1129 51
            $objWriter->startElement('c:grouping');
1130 51
            $objWriter->writeAttribute('val', $plotGroupingType);
1131 51
            $objWriter->endElement();
1132
        }
1133
1134
        //    Get these details before the loop, because we can use the count to check for varyColors
1135 86
        $plotSeriesOrder = $plotGroup->getPlotOrder();
1136 86
        $plotSeriesCount = count($plotSeriesOrder);
1137
1138 86
        if (($groupType !== DataSeries::TYPE_RADARCHART) && ($groupType !== DataSeries::TYPE_STOCKCHART)) {
1139 82
            if ($groupType !== DataSeries::TYPE_LINECHART) {
1140 67
                if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) {
1141 59
                    $objWriter->startElement('c:varyColors');
1142 59
                    $objWriter->writeAttribute('val', '1');
1143 59
                    $objWriter->endElement();
1144
                } else {
1145 14
                    $objWriter->startElement('c:varyColors');
1146 14
                    $objWriter->writeAttribute('val', '0');
1147 14
                    $objWriter->endElement();
1148
                }
1149
            }
1150
        }
1151
1152 86
        $plotSeriesIdx = 0;
1153 86
        foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) {
1154 86
            $objWriter->startElement('c:ser');
1155
1156 86
            $objWriter->startElement('c:idx');
1157 86
            $adder = array_key_exists(0, $plotSeriesOrder) ? $this->seriesIndex : 0;
1158 86
            $objWriter->writeAttribute('val', (string) ($adder + $plotSeriesIdx));
1159 86
            $objWriter->endElement();
1160
1161 86
            $objWriter->startElement('c:order');
1162 86
            $objWriter->writeAttribute('val', (string) ($adder + $plotSeriesRef));
1163 86
            $objWriter->endElement();
1164
1165 86
            $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx);
1166 86
            $labelFill = null;
1167 86
            if ($plotLabel && $groupType === DataSeries::TYPE_LINECHART) {
1168 19
                $labelFill = $plotLabel->getFillColorObject();
1169 19
                $labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null;
1170
            }
1171
1172
            //    Values
1173 86
            $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx);
1174
1175 86
            if ($plotSeriesValues !== false && in_array($groupType, self::CUSTOM_COLOR_TYPES, true)) {
1176 32
                $fillColorValues = $plotSeriesValues->getFillColorObject();
1177 32
                if ($fillColorValues !== null && is_array($fillColorValues)) {
1178 8
                    foreach (($plotSeriesValues->getDataValues() ?? []) as $dataKey => $dataValue) {
1179 8
                        $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? null);
1180
                    }
1181
                }
1182
            }
1183 86
            if ($plotSeriesValues !== false && $plotSeriesValues->getLabelLayout()) {
1184 7
                $this->writeDataLabels($objWriter, $plotSeriesValues->getLabelLayout());
1185
            }
1186
1187
            //    Labels
1188 86
            $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx);
1189 86
            if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) {
1190 84
                $objWriter->startElement('c:tx');
1191 84
                $objWriter->startElement('c:strRef');
1192 84
                $this->writePlotSeriesLabel($plotSeriesLabel, $objWriter);
1193 84
                $objWriter->endElement();
1194 84
                $objWriter->endElement();
1195
            }
1196
1197
            //    Formatting for the points
1198
            if (
1199 86
                $plotSeriesValues !== false
1200
            ) {
1201 86
                $objWriter->startElement('c:spPr');
1202 86
                if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) {
1203 71
                    $fillColor = $plotLabel->getFillColorObject();
1204 71
                    if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) {
1205 1
                        $this->writeColor($objWriter, $fillColor);
1206
                    }
1207
                }
1208 86
                $fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject();
1209 86
                $callLineStyles = true;
1210 86
                if ($fillObject instanceof ChartColor && $fillObject->isUsable()) {
1211 16
                    if ($groupType === DataSeries::TYPE_LINECHART) {
1212 4
                        $objWriter->startElement('a:ln');
1213 4
                        $callLineStyles = false;
1214
                    }
1215 16
                    $this->writeColor($objWriter, $fillObject);
1216 16
                    if (!$callLineStyles) {
1217 4
                        $objWriter->endElement(); // a:ln
1218
                    }
1219
                }
1220 86
                $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines());
1221 86
                if ($callLineStyles) {
1222 84
                    $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill);
1223 84
                    $this->writeEffects($objWriter, $plotSeriesValues);
1224
                }
1225 86
                $objWriter->endElement(); // c:spPr
1226
            }
1227
1228 86
            if ($plotSeriesValues) {
1229 86
                $plotSeriesMarker = $plotSeriesValues->getPointMarker();
1230 86
                $markerFillColor = $plotSeriesValues->getMarkerFillColor();
1231 86
                $fillUsed = $markerFillColor->IsUsable();
1232 86
                $markerBorderColor = $plotSeriesValues->getMarkerBorderColor();
1233 86
                $borderUsed = $markerBorderColor->isUsable();
1234 86
                if ($plotSeriesMarker || $fillUsed || $borderUsed) {
1235 27
                    $objWriter->startElement('c:marker');
1236 27
                    $objWriter->startElement('c:symbol');
1237 27
                    if ($plotSeriesMarker) {
1238 27
                        $objWriter->writeAttribute('val', $plotSeriesMarker);
1239
                    }
1240 27
                    $objWriter->endElement();
1241
1242 27
                    if ($plotSeriesMarker !== 'none') {
1243 25
                        $objWriter->startElement('c:size');
1244 25
                        $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize());
1245 25
                        $objWriter->endElement(); // c:size
1246 25
                        $objWriter->startElement('c:spPr');
1247 25
                        $this->writeColor($objWriter, $markerFillColor);
1248 25
                        if ($borderUsed) {
1249 15
                            $objWriter->startElement('a:ln');
1250 15
                            $this->writeColor($objWriter, $markerBorderColor);
1251 15
                            $objWriter->endElement(); // a:ln
1252
                        }
1253 25
                        $objWriter->endElement(); // spPr
1254
                    }
1255
1256 27
                    $objWriter->endElement();
1257
                }
1258
            }
1259
1260 86
            if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) {
1261 28
                $objWriter->startElement('c:invertIfNegative');
1262 28
                $objWriter->writeAttribute('val', '0');
1263 28
                $objWriter->endElement();
1264
            }
1265
            // Trendlines
1266 86
            if ($plotSeriesValues !== false) {
1267 86
                foreach ($plotSeriesValues->getTrendLines() as $trendLine) {
1268 3
                    $trendLineType = $trendLine->getTrendLineType();
1269 3
                    $order = $trendLine->getOrder();
1270 3
                    $period = $trendLine->getPeriod();
1271 3
                    $dispRSqr = $trendLine->getDispRSqr();
1272 3
                    $dispEq = $trendLine->getDispEq();
1273 3
                    $forward = $trendLine->getForward();
1274 3
                    $backward = $trendLine->getBackward();
1275 3
                    $intercept = $trendLine->getIntercept();
1276 3
                    $name = $trendLine->getName();
1277 3
                    $trendLineColor = $trendLine->getLineColor(); // ChartColor
1278
1279 3
                    $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell'
1280 3
                    if ($name !== '') {
1281 3
                        $objWriter->startElement('c:name');
1282 3
                        $objWriter->writeRawData($name);
1283 3
                        $objWriter->endElement(); // c:name
1284
                    }
1285 3
                    $objWriter->startElement('c:spPr');
1286
1287 3
                    if (!$trendLineColor->isUsable()) {
1288
                        // use dataSeriesValues line color as a backup if $trendLineColor is null
1289
                        $dsvLineColor = $plotSeriesValues->getLineColor();
1290
                        if ($dsvLineColor->isUsable()) {
1291
                            $trendLine
1292
                                ->getLineColor()
1293
                                ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType());
1294
                        }
1295
                    } // otherwise, hope Excel does the right thing
1296
1297 3
                    $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill
1298
1299 3
                    $objWriter->endElement(); // spPr
1300
1301 3
                    $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell'
1302 3
                    $objWriter->writeAttribute('val', $trendLineType);
1303 3
                    $objWriter->endElement(); // trendlineType
1304 3
                    if ($backward !== 0.0) {
1305 3
                        $objWriter->startElement('c:backward');
1306 3
                        $objWriter->writeAttribute('val', "$backward");
1307 3
                        $objWriter->endElement(); // c:backward
1308
                    }
1309 3
                    if ($forward !== 0.0) {
1310 3
                        $objWriter->startElement('c:forward');
1311 3
                        $objWriter->writeAttribute('val', "$forward");
1312 3
                        $objWriter->endElement(); // c:forward
1313
                    }
1314 3
                    if ($intercept !== 0.0) {
1315 3
                        $objWriter->startElement('c:intercept');
1316 3
                        $objWriter->writeAttribute('val', "$intercept");
1317 3
                        $objWriter->endElement(); // c:intercept
1318
                    }
1319 3
                    if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) {
1320 3
                        $objWriter->startElement('c:order');
1321 3
                        $objWriter->writeAttribute('val', $order);
1322 3
                        $objWriter->endElement(); // order
1323
                    }
1324 3
                    if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) {
1325 3
                        $objWriter->startElement('c:period');
1326 3
                        $objWriter->writeAttribute('val', $period);
1327 3
                        $objWriter->endElement(); // period
1328
                    }
1329 3
                    $objWriter->startElement('c:dispRSqr');
1330 3
                    $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0');
1331 3
                    $objWriter->endElement();
1332 3
                    $objWriter->startElement('c:dispEq');
1333 3
                    $objWriter->writeAttribute('val', $dispEq ? '1' : '0');
1334 3
                    $objWriter->endElement();
1335 3
                    if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) {
1336 3
                        $objWriter->startElement('c:trendlineLbl');
1337 3
                        $objWriter->startElement('c:numFmt');
1338 3
                        $objWriter->writeAttribute('formatCode', 'General');
1339 3
                        $objWriter->writeAttribute('sourceLinked', '0');
1340 3
                        $objWriter->endElement();  // numFmt
1341 3
                        $objWriter->endElement();  // trendlineLbl
1342
                    }
1343
1344 3
                    $objWriter->endElement(); // trendline
1345
                }
1346
            }
1347
1348
            //    Category Labels
1349 86
            $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx);
1350 86
            if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) {
1351 77
                $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries();
1352
1353 77
                if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) {
1354 9
                    $plotStyle = $plotGroup->getPlotStyle();
1355 9
                    if (is_numeric($plotStyle)) {
1356 2
                        $objWriter->startElement('c:explosion');
1357 2
                        $objWriter->writeAttribute('val', $plotStyle);
1358 2
                        $objWriter->endElement();
1359
                    }
1360
                }
1361
1362 77
                if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
1363 28
                    $objWriter->startElement('c:xVal');
1364
                } else {
1365 55
                    $objWriter->startElement('c:cat');
1366
                }
1367
1368
                // xVals (Categories) are not always 'str'
1369
                // Test X-axis Label's Datatype to decide 'str' vs 'num'
1370 77
                $CategoryDatatype = $plotSeriesCategory->getDataType();
1371 77
                if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) {
1372 21
                    $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num');
1373
                } else {
1374 57
                    $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str');
1375
                }
1376 77
                $objWriter->endElement();
1377
            }
1378
1379
            //    Values
1380 86
            if ($plotSeriesValues) {
1381 86
                $valIsMultiLevelSeries = $valIsMultiLevelSeries || $plotSeriesValues->isMultiLevelSeries();
1382
1383 86
                if (($groupType === DataSeries::TYPE_BUBBLECHART) || ($groupType === DataSeries::TYPE_SCATTERCHART)) {
1384 28
                    $objWriter->startElement('c:yVal');
1385
                } else {
1386 64
                    $objWriter->startElement('c:val');
1387
                }
1388
1389 86
                $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num');
1390 86
                $objWriter->endElement();
1391 86
                if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') {
1392 11
                    $objWriter->startElement('c:smooth');
1393 11
                    $objWriter->writeAttribute('val', $plotSeriesValues->getSmoothLine() ? '1' : '0');
1394 11
                    $objWriter->endElement();
1395
                }
1396
            }
1397
1398 86
            if ($groupType === DataSeries::TYPE_BUBBLECHART) {
1399 2
                if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) {
1400 2
                    $objWriter->startElement('c:bubbleSize');
1401 2
                    $this->writePlotSeriesValues(
1402 2
                        $plotGroup->getPlotBubbleSizes()[$plotSeriesIdx],
1403 2
                        $objWriter,
1404 2
                        $groupType,
1405 2
                        'num'
1406 2
                    );
1407 2
                    $objWriter->endElement();
1408 2
                    if ($plotSeriesValues !== false) {
1409 2
                        $objWriter->startElement('c:bubble3D');
1410 2
                        $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0');
1411 2
                        $objWriter->endElement();
1412
                    }
1413 1
                } elseif ($plotSeriesValues !== false) {
1414 1
                    $this->writeBubbles($plotSeriesValues, $objWriter);
1415
                }
1416
            }
1417
1418 86
            $objWriter->endElement();
1419
        }
1420
1421 86
        $this->seriesIndex += $plotSeriesIdx + 1;
1422
    }
1423
1424
    /**
1425
     * Write Plot Series Label.
1426
     */
1427 84
    private function writePlotSeriesLabel(?DataSeriesValues $plotSeriesLabel, XMLWriter $objWriter): void
1428
    {
1429 84
        if ($plotSeriesLabel === null) {
1430
            return;
1431
        }
1432
1433 84
        $objWriter->startElement('c:f');
1434 84
        $objWriter->writeRawData($plotSeriesLabel->getDataSource());
1435 84
        $objWriter->endElement();
1436
1437 84
        $objWriter->startElement('c:strCache');
1438 84
        $objWriter->startElement('c:ptCount');
1439 84
        $objWriter->writeAttribute('val', (string) $plotSeriesLabel->getPointCount());
1440 84
        $objWriter->endElement();
1441
1442 84
        foreach (($plotSeriesLabel->getDataValues() ?? []) as $plotLabelKey => $plotLabelValue) {
1443 84
            $objWriter->startElement('c:pt');
1444 84
            $objWriter->writeAttribute('idx', $plotLabelKey);
1445
1446 84
            $objWriter->startElement('c:v');
1447 84
            $objWriter->writeRawData($plotLabelValue);
1448 84
            $objWriter->endElement();
1449 84
            $objWriter->endElement();
1450
        }
1451 84
        $objWriter->endElement();
1452
    }
1453
1454
    /**
1455
     * Write Plot Series Values.
1456
     *
1457
     * @param string $groupType Type of plot for dataseries
1458
     * @param string $dataType Datatype of series values
1459
     */
1460 86
    private function writePlotSeriesValues(?DataSeriesValues $plotSeriesValues, XMLWriter $objWriter, string $groupType, string $dataType = 'str'): void
1461
    {
1462 86
        if ($plotSeriesValues === null) {
1463
            return;
1464
        }
1465
1466 86
        if ($plotSeriesValues->isMultiLevelSeries()) {
1467 18
            $levelCount = $plotSeriesValues->multiLevelCount();
1468
1469 18
            $objWriter->startElement('c:multiLvlStrRef');
1470
1471 18
            $objWriter->startElement('c:f');
1472 18
            $objWriter->writeRawData($plotSeriesValues->getDataSource());
1473 18
            $objWriter->endElement();
1474
1475 18
            $objWriter->startElement('c:multiLvlStrCache');
1476
1477 18
            $objWriter->startElement('c:ptCount');
1478 18
            $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
1479 18
            $objWriter->endElement();
1480
1481 18
            for ($level = 0; $level < $levelCount; ++$level) {
1482 18
                $objWriter->startElement('c:lvl');
1483
1484 18
                foreach (($plotSeriesValues->getDataValues() ?? []) as $plotSeriesKey => $plotSeriesValue) {
1485 18
                    if (isset($plotSeriesValue[$level])) {
1486 18
                        $objWriter->startElement('c:pt');
1487 18
                        $objWriter->writeAttribute('idx', $plotSeriesKey);
1488
1489 18
                        $objWriter->startElement('c:v');
1490 18
                        $objWriter->writeRawData($plotSeriesValue[$level]);
1491 18
                        $objWriter->endElement();
1492 18
                        $objWriter->endElement();
1493
                    }
1494
                }
1495
1496 18
                $objWriter->endElement();
1497
            }
1498
1499 18
            $objWriter->endElement();
1500
1501 18
            $objWriter->endElement();
1502
        } else {
1503 86
            $objWriter->startElement('c:' . $dataType . 'Ref');
1504
1505 86
            $objWriter->startElement('c:f');
1506 86
            $objWriter->writeRawData($plotSeriesValues->getDataSource());
1507 86
            $objWriter->endElement();
1508
1509 86
            $count = $plotSeriesValues->getPointCount();
1510 86
            $source = $plotSeriesValues->getDataSource();
1511 86
            $values = $plotSeriesValues->getDataValues();
1512 86
            if ($count > 1 || ($count === 1 && is_array($values) && array_key_exists(0, $values) && "=$source" !== (string) $values[0])) {
1513 86
                $objWriter->startElement('c:' . $dataType . 'Cache');
1514
1515 86
                if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) {
1516 81
                    if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) {
1517 46
                        $objWriter->startElement('c:formatCode');
1518 46
                        $objWriter->writeRawData($plotSeriesValues->getFormatCode());
1519 46
                        $objWriter->endElement();
1520
                    }
1521
                }
1522
1523 86
                $objWriter->startElement('c:ptCount');
1524 86
                $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
1525 86
                $objWriter->endElement();
1526
1527 86
                $dataValues = $plotSeriesValues->getDataValues();
1528 86
                if (!empty($dataValues)) {
1529 86
                    foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
1530 86
                        $objWriter->startElement('c:pt');
1531 86
                        $objWriter->writeAttribute('idx', $plotSeriesKey);
1532
1533 86
                        $objWriter->startElement('c:v');
1534 86
                        $objWriter->writeRawData($plotSeriesValue);
1535 86
                        $objWriter->endElement();
1536 86
                        $objWriter->endElement();
1537
                    }
1538
                }
1539
1540 86
                $objWriter->endElement(); // *Cache
1541
            }
1542
1543 86
            $objWriter->endElement(); // *Ref
1544
        }
1545
    }
1546
1547
    private const CUSTOM_COLOR_TYPES = [
1548
        DataSeries::TYPE_BARCHART,
1549
        DataSeries::TYPE_BARCHART_3D,
1550
        DataSeries::TYPE_PIECHART,
1551
        DataSeries::TYPE_PIECHART_3D,
1552
        DataSeries::TYPE_DONUTCHART,
1553
    ];
1554
1555
    /**
1556
     * Write Bubble Chart Details.
1557
     */
1558 1
    private function writeBubbles(?DataSeriesValues $plotSeriesValues, XMLWriter $objWriter): void
1559
    {
1560 1
        if ($plotSeriesValues === null) {
1561
            return;
1562
        }
1563
1564 1
        $objWriter->startElement('c:bubbleSize');
1565 1
        $objWriter->startElement('c:numLit');
1566
1567 1
        $objWriter->startElement('c:formatCode');
1568 1
        $objWriter->writeRawData('General');
1569 1
        $objWriter->endElement();
1570
1571 1
        $objWriter->startElement('c:ptCount');
1572 1
        $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount());
1573 1
        $objWriter->endElement();
1574
1575 1
        $dataValues = $plotSeriesValues->getDataValues();
1576 1
        if (!empty($dataValues)) {
1577 1
            foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) {
1578 1
                $objWriter->startElement('c:pt');
1579 1
                $objWriter->writeAttribute('idx', $plotSeriesKey);
1580 1
                $objWriter->startElement('c:v');
1581 1
                $objWriter->writeRawData('1');
1582 1
                $objWriter->endElement();
1583 1
                $objWriter->endElement();
1584
            }
1585
        }
1586
1587 1
        $objWriter->endElement();
1588 1
        $objWriter->endElement();
1589
1590 1
        $objWriter->startElement('c:bubble3D');
1591 1
        $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0');
1592 1
        $objWriter->endElement();
1593
    }
1594
1595
    /**
1596
     * Write Layout.
1597
     */
1598 86
    private function writeLayout(XMLWriter $objWriter, ?Layout $layout = null): void
1599
    {
1600 86
        $objWriter->startElement('c:layout');
1601
1602 86
        if ($layout !== null) {
1603 47
            $objWriter->startElement('c:manualLayout');
1604
1605 47
            $layoutTarget = $layout->getLayoutTarget();
1606 47
            if ($layoutTarget !== null) {
1607 19
                $objWriter->startElement('c:layoutTarget');
1608 19
                $objWriter->writeAttribute('val', $layoutTarget);
1609 19
                $objWriter->endElement();
1610
            }
1611
1612 47
            $xMode = $layout->getXMode();
1613 47
            if ($xMode !== null) {
1614 19
                $objWriter->startElement('c:xMode');
1615 19
                $objWriter->writeAttribute('val', $xMode);
1616 19
                $objWriter->endElement();
1617
            }
1618
1619 47
            $yMode = $layout->getYMode();
1620 47
            if ($yMode !== null) {
1621 19
                $objWriter->startElement('c:yMode');
1622 19
                $objWriter->writeAttribute('val', $yMode);
1623 19
                $objWriter->endElement();
1624
            }
1625
1626 47
            $x = $layout->getXPosition();
1627 47
            if ($x !== null) {
1628 19
                $objWriter->startElement('c:x');
1629 19
                $objWriter->writeAttribute('val', "$x");
1630 19
                $objWriter->endElement();
1631
            }
1632
1633 47
            $y = $layout->getYPosition();
1634 47
            if ($y !== null) {
1635 19
                $objWriter->startElement('c:y');
1636 19
                $objWriter->writeAttribute('val', "$y");
1637 19
                $objWriter->endElement();
1638
            }
1639
1640 47
            $w = $layout->getWidth();
1641 47
            if ($w !== null) {
1642 19
                $objWriter->startElement('c:w');
1643 19
                $objWriter->writeAttribute('val', "$w");
1644 19
                $objWriter->endElement();
1645
            }
1646
1647 47
            $h = $layout->getHeight();
1648 47
            if ($h !== null) {
1649 19
                $objWriter->startElement('c:h');
1650 19
                $objWriter->writeAttribute('val', "$h");
1651 19
                $objWriter->endElement();
1652
            }
1653
1654 47
            $objWriter->endElement();
1655
        }
1656
1657 86
        $objWriter->endElement();
1658
    }
1659
1660
    /**
1661
     * Write Alternate Content block.
1662
     */
1663 86
    private function writeAlternateContent(XMLWriter $objWriter): void
1664
    {
1665 86
        $objWriter->startElement('mc:AlternateContent');
1666 86
        $objWriter->writeAttribute('xmlns:mc', Namespaces::COMPATIBILITY);
1667
1668 86
        $objWriter->startElement('mc:Choice');
1669 86
        $objWriter->writeAttribute('Requires', 'c14');
1670 86
        $objWriter->writeAttribute('xmlns:c14', Namespaces::CHART_ALTERNATE);
1671
1672 86
        $objWriter->startElement('c14:style');
1673 86
        $objWriter->writeAttribute('val', '102');
1674 86
        $objWriter->endElement();
1675 86
        $objWriter->endElement();
1676
1677 86
        $objWriter->startElement('mc:Fallback');
1678 86
        $objWriter->startElement('c:style');
1679 86
        $objWriter->writeAttribute('val', '2');
1680 86
        $objWriter->endElement();
1681 86
        $objWriter->endElement();
1682
1683 86
        $objWriter->endElement();
1684
    }
1685
1686
    /**
1687
     * Write Printer Settings.
1688
     */
1689 86
    private function writePrintSettings(XMLWriter $objWriter): void
1690
    {
1691 86
        $objWriter->startElement('c:printSettings');
1692
1693 86
        $objWriter->startElement('c:headerFooter');
1694 86
        $objWriter->endElement();
1695
1696 86
        $objWriter->startElement('c:pageMargins');
1697 86
        $objWriter->writeAttribute('footer', '0.3');
1698 86
        $objWriter->writeAttribute('header', '0.3');
1699 86
        $objWriter->writeAttribute('r', '0.7');
1700 86
        $objWriter->writeAttribute('l', '0.7');
1701 86
        $objWriter->writeAttribute('t', '0.75');
1702 86
        $objWriter->writeAttribute('b', '0.75');
1703 86
        $objWriter->endElement();
1704
1705 86
        $objWriter->startElement('c:pageSetup');
1706 86
        $objWriter->writeAttribute('orientation', 'portrait');
1707 86
        $objWriter->endElement();
1708
1709 86
        $objWriter->endElement();
1710
    }
1711
1712 86
    private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void
1713
    {
1714
        if (
1715 86
            !empty($yAxis->getSoftEdgesSize())
1716 86
            || !empty($yAxis->getShadowProperty('effect'))
1717 86
            || !empty($yAxis->getGlowProperty('size'))
1718
        ) {
1719 13
            $objWriter->startElement('a:effectLst');
1720 13
            $this->writeGlow($objWriter, $yAxis);
1721 13
            $this->writeShadow($objWriter, $yAxis);
1722 13
            $this->writeSoftEdge($objWriter, $yAxis);
1723 13
            $objWriter->endElement(); // effectLst
1724
        }
1725
    }
1726
1727 13
    private function writeShadow(XMLWriter $objWriter, Properties $xAxis): void
1728
    {
1729 13
        if (empty($xAxis->getShadowProperty('effect'))) {
1730 8
            return;
1731
        }
1732
        /** @var string $effect */
1733 8
        $effect = $xAxis->getShadowProperty('effect');
1734 8
        $objWriter->startElement("a:$effect");
1735
1736 8
        if (is_numeric($xAxis->getShadowProperty('blur'))) {
1737 8
            $objWriter->writeAttribute('blurRad', Properties::pointsToXml((float) $xAxis->getShadowProperty('blur')));
1738
        }
1739 8
        if (is_numeric($xAxis->getShadowProperty('distance'))) {
1740 8
            $objWriter->writeAttribute('dist', Properties::pointsToXml((float) $xAxis->getShadowProperty('distance')));
1741
        }
1742 8
        if (is_numeric($xAxis->getShadowProperty('direction'))) {
1743 8
            $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction')));
1744
        }
1745 8
        $algn = $xAxis->getShadowProperty('algn');
1746 8
        if (is_string($algn) && $algn !== '') {
1747 8
            $objWriter->writeAttribute('algn', $algn);
1748
        }
1749 8
        foreach (['sx', 'sy'] as $sizeType) {
1750 8
            $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]);
1751 8
            if (is_numeric($sizeValue)) {
1752 2
                $objWriter->writeAttribute($sizeType, Properties::tenthOfPercentToXml((float) $sizeValue));
1753
            }
1754
        }
1755 8
        foreach (['kx', 'ky'] as $sizeType) {
1756 8
            $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]);
1757 8
            if (is_numeric($sizeValue)) {
1758 2
                $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue));
1759
            }
1760
        }
1761 8
        $rotWithShape = $xAxis->getShadowProperty('rotWithShape');
1762 8
        if (is_numeric($rotWithShape)) {
1763 8
            $objWriter->writeAttribute('rotWithShape', (string) (int) $rotWithShape);
1764
        }
1765
1766 8
        $this->writeColor($objWriter, $xAxis->getShadowColorObject(), false);
1767
1768 8
        $objWriter->endElement();
1769
    }
1770
1771 13
    private function writeGlow(XMLWriter $objWriter, Properties $yAxis): void
1772
    {
1773 13
        $size = $yAxis->getGlowProperty('size');
1774 13
        if (empty($size)) {
1775 9
            return;
1776
        }
1777 8
        $objWriter->startElement('a:glow');
1778 8
        $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size));
1779 8
        $this->writeColor($objWriter, $yAxis->getGlowColorObject(), false);
1780 8
        $objWriter->endElement(); // glow
1781
    }
1782
1783 13
    private function writeSoftEdge(XMLWriter $objWriter, Properties $yAxis): void
1784
    {
1785 13
        $softEdgeSize = $yAxis->getSoftEdgesSize();
1786 13
        if (empty($softEdgeSize)) {
1787 12
            return;
1788
        }
1789 2
        $objWriter->startElement('a:softEdge');
1790 2
        $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize));
1791 2
        $objWriter->endElement(); //end softEdge
1792
    }
1793
1794 86
    private function writeLineStyles(XMLWriter $objWriter, Properties $gridlines, bool $noFill = false): void
1795
    {
1796 86
        $objWriter->startElement('a:ln');
1797 86
        $widthTemp = $gridlines->getLineStyleProperty('width');
1798 86
        if (is_numeric($widthTemp)) {
1799 39
            $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp));
1800
        }
1801 86
        $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap'));
1802 86
        $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound'));
1803 86
        if ($noFill) {
1804 27
            $objWriter->startElement('a:noFill');
1805 27
            $objWriter->endElement();
1806
        } else {
1807 86
            $this->writeColor($objWriter, $gridlines->getLineColor());
1808
        }
1809
1810 86
        $dash = $gridlines->getLineStyleProperty('dash');
1811 86
        if (!empty($dash)) {
1812 33
            $objWriter->startElement('a:prstDash');
1813 33
            $this->writeNotEmpty($objWriter, 'val', $dash);
1814 33
            $objWriter->endElement();
1815
        }
1816
1817 86
        if ($gridlines->getLineStyleProperty('join') === 'miter') {
1818 13
            $objWriter->startElement('a:miter');
1819 13
            $objWriter->writeAttribute('lim', '800000');
1820 13
            $objWriter->endElement();
1821 86
        } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') {
1822 6
            $objWriter->startElement('a:bevel');
1823 6
            $objWriter->endElement();
1824
        }
1825
1826 86
        if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) {
1827 13
            $objWriter->startElement('a:headEnd');
1828 13
            $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type']));
1829 13
            $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('head'));
1830 13
            $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('head'));
1831 13
            $objWriter->endElement();
1832
        }
1833
1834 86
        if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) {
1835 13
            $objWriter->startElement('a:tailEnd');
1836 13
            $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type']));
1837 13
            $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('end'));
1838 13
            $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('end'));
1839 13
            $objWriter->endElement();
1840
        }
1841 86
        $objWriter->endElement(); //end ln
1842
    }
1843
1844 86
    private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void
1845
    {
1846 86
        if ($value !== null && $value !== '') {
1847 35
            $objWriter->writeAttribute($name, $value);
1848
        }
1849
    }
1850
1851 86
    private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $solidFill = true): void
1852
    {
1853 86
        $type = $chartColor->getType();
1854 86
        $value = $chartColor->getValue();
1855 86
        if (!empty($type) && !empty($value)) {
1856 62
            if ($solidFill) {
1857 56
                $objWriter->startElement('a:solidFill');
1858
            }
1859 62
            $objWriter->startElement("a:$type");
1860 62
            $objWriter->writeAttribute('val', $value);
1861 62
            $alpha = $chartColor->getAlpha();
1862 62
            if (is_numeric($alpha)) {
1863 24
                $objWriter->startElement('a:alpha');
1864 24
                $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha));
1865 24
                $objWriter->endElement(); // a:alpha
1866
            }
1867 62
            $brightness = $chartColor->getBrightness();
1868 62
            if (is_numeric($brightness)) {
1869 9
                $brightness = (int) $brightness;
1870 9
                $lumOff = 100 - $brightness;
1871 9
                $objWriter->startElement('a:lumMod');
1872 9
                $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness));
1873 9
                $objWriter->endElement(); // a:lumMod
1874 9
                $objWriter->startElement('a:lumOff');
1875 9
                $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff));
1876 9
                $objWriter->endElement(); // a:lumOff
1877
            }
1878 62
            $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr
1879 62
            if ($solidFill) {
1880 56
                $objWriter->endElement(); //a:solidFill
1881
            }
1882
        }
1883
    }
1884
1885 15
    private function writeLabelFont(XMLWriter $objWriter, ?Font $labelFont, ?Properties $axisText): void
1886
    {
1887 15
        $objWriter->startElement('a:p');
1888 15
        $objWriter->startElement('a:pPr');
1889 15
        $objWriter->startElement('a:defRPr');
1890 15
        if ($labelFont !== null) {
1891 13
            $fontSize = $labelFont->getSize();
1892 13
            if (is_numeric($fontSize)) {
1893 6
                $fontSize *= (($fontSize < 100) ? 100 : 1);
1894 6
                $objWriter->writeAttribute('sz', (string) $fontSize);
1895
            }
1896 13
            if ($labelFont->getBold() === true) {
1897
                $objWriter->writeAttribute('b', '1');
1898
            }
1899 13
            if ($labelFont->getItalic() === true) {
1900
                $objWriter->writeAttribute('i', '1');
1901
            }
1902 13
            $cap = $labelFont->getCap();
1903 13
            if ($cap !== null) {
1904 2
                $objWriter->writeAttribute('cap', $cap);
1905
            }
1906 13
            $fontColor = $labelFont->getChartColor();
1907 13
            if ($fontColor !== null) {
1908 8
                $this->writeColor($objWriter, $fontColor);
1909
            }
1910
        }
1911 15
        if ($axisText !== null) {
1912 9
            $this->writeEffects($objWriter, $axisText);
1913
        }
1914 15
        if ($labelFont !== null) {
1915 13
            if (!empty($labelFont->getLatin())) {
1916 5
                $objWriter->startElement('a:latin');
1917 5
                $objWriter->writeAttribute('typeface', $labelFont->getLatin());
1918 5
                $objWriter->endElement();
1919
            }
1920 13
            if (!empty($labelFont->getEastAsian())) {
1921 1
                $objWriter->startElement('a:eastAsian');
1922 1
                $objWriter->writeAttribute('typeface', $labelFont->getEastAsian());
1923 1
                $objWriter->endElement();
1924
            }
1925 13
            if (!empty($labelFont->getComplexScript())) {
1926 1
                $objWriter->startElement('a:complexScript');
1927 1
                $objWriter->writeAttribute('typeface', $labelFont->getComplexScript());
1928 1
                $objWriter->endElement();
1929
            }
1930
        }
1931 15
        $objWriter->endElement(); // a:defRPr
1932 15
        $objWriter->endElement(); // a:pPr
1933 15
        $objWriter->endElement(); // a:p
1934
    }
1935
}
1936