Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
198 / 198 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
Chart | |
100.00% |
198 / 198 |
|
100.00% |
9 / 9 |
46 | |
100.00% |
1 / 1 |
setElement | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
write | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
writeChart | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
writePlotArea | |
100.00% |
48 / 48 |
|
100.00% |
1 / 1 |
14 | |||
writeSeries | |
100.00% |
50 / 50 |
|
100.00% |
1 / 1 |
10 | |||
writeSeriesItem | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
3 | |||
writeAxis | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
13 | |||
writeShape | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
writeAxisTitle | |
100.00% |
20 / 20 |
|
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 | |
19 | namespace PhpOffice\PhpWord\Writer\Word2007\Part; |
20 | |
21 | use PhpOffice\PhpWord\Element\Chart as ChartElement; |
22 | use 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 | */ |
30 | class 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 | } |