Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
198 / 198
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
Chart
100.00% covered (success)
100.00%
198 / 198
100.00% covered (success)
100.00%
9 / 9
46
100.00% covered (success)
100.00%
1 / 1
 setElement
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 write
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 writeChart
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 writePlotArea
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
14
 writeSeries
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
10
 writeSeriesItem
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 writeAxis
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
13
 writeShape
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 writeAxisTitle
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * This file is part of PHPWord - A pure PHP library for reading and writing
5 * word processing documents.
6 *
7 * PHPWord is free software distributed under the terms of the GNU Lesser
8 * General Public License version 3 as published by the Free Software Foundation.
9 *
10 * For the full copyright and license information, please read the LICENSE
11 * file that was distributed with this source code. For the full list of
12 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
13 *
14 * @see         https://github.com/PHPOffice/PHPWord
15 *
16 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17 */
18
19namespace PhpOffice\PhpWord\Writer\Word2007\Part;
20
21use PhpOffice\PhpWord\Element\Chart as ChartElement;
22use PhpOffice\PhpWord\Shared\XMLWriter;
23
24/**
25 * Word2007 chart part writer: word/charts/chartx.xml.
26 *
27 * @since 0.12.0
28 * @see  http://www.datypic.com/sc/ooxml/e-draw-chart_chartSpace.html
29 */
30class Chart extends AbstractPart
31{
32    /**
33     * Chart element.
34     *
35     * @var ChartElement
36     */
37    private $element;
38
39    /**
40     * Type definition.
41     *
42     * @var array
43     */
44    private $types = [
45        'pie' => ['type' => 'pie', 'colors' => 1],
46        'doughnut' => ['type' => 'doughnut', 'colors' => 1, 'hole' => 75, 'no3d' => true],
47        'bar' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'clustered'],
48        'stacked_bar' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'stacked'],
49        'percent_stacked_bar' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'percentStacked'],
50        'column' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'clustered'],
51        'stacked_column' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'stacked'],
52        'percent_stacked_column' => ['type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'percentStacked'],
53        'line' => ['type' => 'line', 'colors' => 0, 'axes' => true],
54        'area' => ['type' => 'area', 'colors' => 0, 'axes' => true],
55        'radar' => ['type' => 'radar', 'colors' => 0, 'axes' => true, 'radar' => 'standard', 'no3d' => true],
56        'scatter' => ['type' => 'scatter', 'colors' => 0, 'axes' => true, 'scatter' => 'marker', 'no3d' => true],
57    ];
58
59    /**
60     * Chart options.
61     *
62     * @var array
63     */
64    private $options = [];
65
66    /**
67     * Set chart element.
68     */
69    public function setElement(ChartElement $element): void
70    {
71        $this->element = $element;
72    }
73
74    /**
75     * Write part.
76     *
77     * @return string
78     */
79    public function write()
80    {
81        $xmlWriter = $this->getXmlWriter();
82
83        $xmlWriter->startDocument('1.0', 'UTF-8', 'yes');
84        $xmlWriter->startElement('c:chartSpace');
85        $xmlWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart');
86        $xmlWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
87        $xmlWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
88
89        $this->writeChart($xmlWriter);
90        $this->writeShape($xmlWriter);
91
92        $xmlWriter->endElement(); // c:chartSpace
93
94        return $xmlWriter->getData();
95    }
96
97    /**
98     * Write chart.
99     *
100     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_Chart.html
101     */
102    private function writeChart(XMLWriter $xmlWriter): void
103    {
104        $xmlWriter->startElement('c:chart');
105
106        $this->writePlotArea($xmlWriter);
107
108        $xmlWriter->endElement(); // c:chart
109    }
110
111    /**
112     * Write plot area.
113     *
114     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_PlotArea.html
115     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_PieChart.html
116     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_DoughnutChart.html
117     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_BarChart.html
118     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_LineChart.html
119     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_AreaChart.html
120     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_RadarChart.html
121     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_ScatterChart.html
122     */
123    private function writePlotArea(XMLWriter $xmlWriter): void
124    {
125        $type = $this->element->getType();
126        $style = $this->element->getStyle();
127        $this->options = $this->types[$type];
128
129        $title = $style->getTitle();
130        $showLegend = $style->isShowLegend();
131        $legendPosition = $style->getLegendPosition();
132
133        //Chart title
134        if ($title) {
135            $xmlWriter->startElement('c:title');
136            $xmlWriter->startElement('c:tx');
137            $xmlWriter->startElement('c:rich');
138            $xmlWriter->writeRaw('
139                <a:bodyPr/>
140                <a:lstStyle/>
141                <a:p>
142                <a:pPr>
143                <a:defRPr/></a:pPr><a:r><a:rPr/><a:t>' . $title . '</a:t></a:r>
144                <a:endParaRPr/>
145                </a:p>');
146            $xmlWriter->endElement(); // c:rich
147            $xmlWriter->endElement(); // c:tx
148            $xmlWriter->endElement(); // c:title
149        } else {
150            $xmlWriter->writeElementBlock('c:autoTitleDeleted', 'val', 1);
151        }
152
153        //Chart legend
154        if ($showLegend) {
155            $xmlWriter->writeRaw('<c:legend><c:legendPos val="' . $legendPosition . '"/></c:legend>');
156        }
157
158        $xmlWriter->startElement('c:plotArea');
159        $xmlWriter->writeElement('c:layout');
160
161        // Chart
162        $chartType = $this->options['type'];
163        $chartType .= $style->is3d() && !isset($this->options['no3d']) ? '3D' : '';
164        $chartType .= 'Chart';
165        $xmlWriter->startElement("c:{$chartType}");
166
167        $xmlWriter->writeElementBlock('c:varyColors', 'val', $this->options['colors']);
168        if ($type == 'area') {
169            $xmlWriter->writeElementBlock('c:grouping', 'val', 'standard');
170        }
171        if (isset($this->options['hole'])) {
172            $xmlWriter->writeElementBlock('c:holeSize', 'val', $this->options['hole']);
173        }
174        if (isset($this->options['bar'])) {
175            $xmlWriter->writeElementBlock('c:barDir', 'val', $this->options['bar']); // bar|col
176            $xmlWriter->writeElementBlock('c:grouping', 'val', $this->options['grouping']); // 3d; standard = percentStacked
177        }
178        if (isset($this->options['radar'])) {
179            $xmlWriter->writeElementBlock('c:radarStyle', 'val', $this->options['radar']);
180        }
181        if (isset($this->options['scatter'])) {
182            $xmlWriter->writeElementBlock('c:scatterStyle', 'val', $this->options['scatter']);
183        }
184
185        // Series
186        $this->writeSeries($xmlWriter, isset($this->options['scatter']));
187
188        // don't overlap if grouping is 'clustered'
189        if (!isset($this->options['grouping']) || $this->options['grouping'] != 'clustered') {
190            $xmlWriter->writeElementBlock('c:overlap', 'val', '100');
191        }
192
193        // Axes
194        if (isset($this->options['axes'])) {
195            $xmlWriter->writeElementBlock('c:axId', 'val', 1);
196            $xmlWriter->writeElementBlock('c:axId', 'val', 2);
197        }
198
199        $xmlWriter->endElement(); // chart type
200
201        // Axes
202        if (isset($this->options['axes'])) {
203            $this->writeAxis($xmlWriter, 'cat');
204            $this->writeAxis($xmlWriter, 'val');
205        }
206
207        $xmlWriter->endElement(); // c:plotArea
208    }
209
210    /**
211     * Write series.
212     *
213     * @param bool $scatter
214     */
215    private function writeSeries(XMLWriter $xmlWriter, $scatter = false): void
216    {
217        $series = $this->element->getSeries();
218        $style = $this->element->getStyle();
219        $colors = $style->getColors();
220
221        $index = 0;
222        $colorIndex = 0;
223        foreach ($series as $seriesItem) {
224            $categories = $seriesItem['categories'];
225            $values = $seriesItem['values'];
226
227            $xmlWriter->startElement('c:ser');
228
229            $xmlWriter->writeElementBlock('c:idx', 'val', $index);
230            $xmlWriter->writeElementBlock('c:order', 'val', $index);
231
232            if (null !== $seriesItem['name'] && $seriesItem['name'] != '') {
233                $xmlWriter->startElement('c:tx');
234                $xmlWriter->startElement('c:strRef');
235                $xmlWriter->startElement('c:strCache');
236                $xmlWriter->writeElementBlock('c:ptCount', 'val', 1);
237                $xmlWriter->startElement('c:pt');
238                $xmlWriter->writeAttribute('idx', 0);
239                $xmlWriter->startElement('c:v');
240                $xmlWriter->writeRaw($seriesItem['name']);
241                $xmlWriter->endElement(); // c:v
242                $xmlWriter->endElement(); // c:pt
243                $xmlWriter->endElement(); // c:strCache
244                $xmlWriter->endElement(); // c:strRef
245                $xmlWriter->endElement(); // c:tx
246            }
247
248            // The c:dLbls was added to make word charts look more like the reports in SurveyGizmo
249            // This section needs to be made configurable before a pull request is made
250            $xmlWriter->startElement('c:dLbls');
251
252            foreach ($style->getDataLabelOptions() as $option => $val) {
253                $xmlWriter->writeElementBlock("c:{$option}", 'val', (int) $val);
254            }
255
256            $xmlWriter->endElement(); // c:dLbls
257
258            if (isset($this->options['scatter'])) {
259                $this->writeShape($xmlWriter);
260            }
261
262            if ($scatter === true) {
263                $this->writeSeriesItem($xmlWriter, 'xVal', $categories);
264                $this->writeSeriesItem($xmlWriter, 'yVal', $values);
265            } else {
266                $this->writeSeriesItem($xmlWriter, 'cat', $categories);
267                $this->writeSeriesItem($xmlWriter, 'val', $values);
268
269                // check that there are colors
270                if (is_array($colors) && count($colors) > 0) {
271                    // assign a color to each value
272                    $valueIndex = 0;
273                    for ($i = 0; $i < count($values); ++$i) {
274                        // check that there are still enought colors
275                        $xmlWriter->startElement('c:dPt');
276                        $xmlWriter->writeElementBlock('c:idx', 'val', $valueIndex);
277                        $xmlWriter->startElement('c:spPr');
278                        $xmlWriter->startElement('a:solidFill');
279                        $xmlWriter->writeElementBlock('a:srgbClr', 'val', $colors[$colorIndex++ % count($colors)]);
280                        $xmlWriter->endElement(); // a:solidFill
281                        $xmlWriter->endElement(); // c:spPr
282                        $xmlWriter->endElement(); // c:dPt
283                        ++$valueIndex;
284                    }
285                }
286            }
287
288            $xmlWriter->endElement(); // c:ser
289            ++$index;
290        }
291    }
292
293    /**
294     * Write series items.
295     *
296     * @param string $type
297     * @param array $values
298     */
299    private function writeSeriesItem(XMLWriter $xmlWriter, $type, $values): void
300    {
301        $types = [
302            'cat' => ['c:cat', 'c:strLit'],
303            'val' => ['c:val', 'c:numLit'],
304            'xVal' => ['c:xVal', 'c:strLit'],
305            'yVal' => ['c:yVal', 'c:numLit'],
306        ];
307        [$itemType, $itemLit] = $types[$type];
308
309        $xmlWriter->startElement($itemType);
310        $xmlWriter->startElement($itemLit);
311        $xmlWriter->writeElementBlock('c:ptCount', 'val', count($values));
312
313        $index = 0;
314        foreach ($values as $value) {
315            $xmlWriter->startElement('c:pt');
316            $xmlWriter->writeAttribute('idx', $index);
317            if (\PhpOffice\PhpWord\Settings::isOutputEscapingEnabled()) {
318                $xmlWriter->writeElement('c:v', $value);
319            } else {
320                $xmlWriter->startElement('c:v');
321                $xmlWriter->writeRaw($value);
322                $xmlWriter->endElement(); // c:v
323            }
324            $xmlWriter->endElement(); // c:pt
325            ++$index;
326        }
327
328        $xmlWriter->endElement(); // $itemLit
329        $xmlWriter->endElement(); // $itemType
330    }
331
332    /**
333     * Write axis.
334     *
335     * @see  http://www.datypic.com/sc/ooxml/t-draw-chart_CT_CatAx.html
336     *
337     * @param string $type
338     */
339    private function writeAxis(XMLWriter $xmlWriter, $type): void
340    {
341        $style = $this->element->getStyle();
342        $types = [
343            'cat' => ['c:catAx', 1, 'b', 2],
344            'val' => ['c:valAx', 2, 'l', 1],
345        ];
346        [$axisType, $axisId, $axisPos, $axisCross] = $types[$type];
347
348        $xmlWriter->startElement($axisType);
349
350        $xmlWriter->writeElementBlock('c:axId', 'val', $axisId);
351        $xmlWriter->writeElementBlock('c:axPos', 'val', $axisPos);
352
353        $categoryAxisTitle = $style->getCategoryAxisTitle();
354        $valueAxisTitle = $style->getValueAxisTitle();
355
356        if ($axisType == 'c:catAx') {
357            if (null !== $categoryAxisTitle) {
358                $this->writeAxisTitle($xmlWriter, $categoryAxisTitle);
359            }
360        } elseif ($axisType == 'c:valAx') {
361            if (null !== $valueAxisTitle) {
362                $this->writeAxisTitle($xmlWriter, $valueAxisTitle);
363            }
364        }
365
366        $xmlWriter->writeElementBlock('c:crossAx', 'val', $axisCross);
367        $xmlWriter->writeElementBlock('c:auto', 'val', 1);
368
369        if (isset($this->options['axes'])) {
370            $xmlWriter->writeElementBlock('c:delete', 'val', 0);
371            $xmlWriter->writeElementBlock('c:majorTickMark', 'val', $style->getMajorTickPosition());
372            $xmlWriter->writeElementBlock('c:minorTickMark', 'val', 'none');
373            if ($style->showAxisLabels()) {
374                if ($axisType == 'c:catAx') {
375                    $xmlWriter->writeElementBlock('c:tickLblPos', 'val', $style->getCategoryLabelPosition());
376                } else {
377                    $xmlWriter->writeElementBlock('c:tickLblPos', 'val', $style->getValueLabelPosition());
378                }
379            } else {
380                $xmlWriter->writeElementBlock('c:tickLblPos', 'val', 'none');
381            }
382            $xmlWriter->writeElementBlock('c:crosses', 'val', 'autoZero');
383        }
384        if (isset($this->options['radar']) || ($type == 'cat' && $style->showGridX()) || ($type == 'val' && $style->showGridY())) {
385            $xmlWriter->writeElement('c:majorGridlines');
386        }
387
388        $xmlWriter->startElement('c:scaling');
389        $xmlWriter->writeElementBlock('c:orientation', 'val', 'minMax');
390        $xmlWriter->endElement(); // c:scaling
391
392        $this->writeShape($xmlWriter, true);
393
394        $xmlWriter->endElement(); // $axisType
395    }
396
397    /**
398     * Write shape.
399     *
400     * @see  http://www.datypic.com/sc/ooxml/t-a_CT_ShapeProperties.html
401     *
402     * @param bool $line
403     */
404    private function writeShape(XMLWriter $xmlWriter, $line = false): void
405    {
406        $xmlWriter->startElement('c:spPr');
407        $xmlWriter->startElement('a:ln');
408        if ($line === true) {
409            $xmlWriter->writeElement('a:solidFill');
410        } else {
411            $xmlWriter->writeElement('a:noFill');
412        }
413        $xmlWriter->endElement(); // a:ln
414        $xmlWriter->endElement(); // c:spPr
415    }
416
417    private function writeAxisTitle(XMLWriter $xmlWriter, $title): void
418    {
419        $xmlWriter->startElement('c:title'); //start c:title
420        $xmlWriter->startElement('c:tx'); //start c:tx
421        $xmlWriter->startElement('c:rich'); // start c:rich
422        $xmlWriter->writeElement('a:bodyPr');
423        $xmlWriter->writeElement('a:lstStyle');
424        $xmlWriter->startElement('a:p');
425        $xmlWriter->startElement('a:pPr');
426        $xmlWriter->writeElement('a:defRPr');
427        $xmlWriter->endElement(); // end a:pPr
428        $xmlWriter->startElement('a:r');
429        $xmlWriter->writeElementBlock('a:rPr', 'lang', 'en-US');
430
431        $xmlWriter->startElement('a:t');
432        $xmlWriter->writeRaw($title);
433        $xmlWriter->endElement(); //end a:t
434
435        $xmlWriter->endElement(); // end a:r
436        $xmlWriter->endElement(); //end a:p
437        $xmlWriter->endElement(); //end c:rich
438        $xmlWriter->endElement(); // end c:tx
439        $xmlWriter->writeElementBlock('c:overlay', 'val', '0');
440        $xmlWriter->endElement(); // end c:title
441    }
442}