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