diff --git a/samples/Sample_32_Chart.php b/samples/Sample_32_Chart.php new file mode 100644 index 00000000..52c19f3c --- /dev/null +++ b/samples/Sample_32_Chart.php @@ -0,0 +1,26 @@ +addSection(array('colsNum' => 2)); +$phpWord->addTitleStyle(1, array('size' => 14, 'bold' => true), array('keepNext' => true, 'spaceBefore' => 240)); + +$charts = array('pie', 'doughnut', 'line', 'area', 'scatter', 'bar', 'radar'); +$labels = array('A', 'B', 'C', 'D', 'E'); +$data = array(1, 3, 2, 5, 4); + +foreach ($charts as $chart) { + $section->addTitle(ucfirst($chart), 1); + $section->addChart($chart, $labels, $data); + $section->addTextBreak(); +} + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/Collection/Charts.php b/src/PhpWord/Collection/Charts.php new file mode 100644 index 00000000..cfccee2e --- /dev/null +++ b/src/PhpWord/Collection/Charts.php @@ -0,0 +1,27 @@ + array('Section'), 'TOC' => array('Section'), 'PageBreak' => array('Section'), + 'Chart' => array('Section'), ); // Special condition, e.g. preservetext can only exists in cell when // the cell is located in header or footer diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index fe133fdd..3fd2e4d0 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -109,7 +109,7 @@ abstract class AbstractElement protected $mediaRelation = false; /** - * Is part of collection; true for Title, Footnote, and Endnote + * Is part of collection; true for Title, Footnote, Endnote, and Chart * * @var bool */ diff --git a/src/PhpWord/Element/Chart.php b/src/PhpWord/Element/Chart.php new file mode 100644 index 00000000..46b7b784 --- /dev/null +++ b/src/PhpWord/Element/Chart.php @@ -0,0 +1,129 @@ +setType($type); + $this->setLabels($labels); + $this->setData($data); + } + + /** + * Get type + * + * @return array + */ + public function getType() + { + return $this->type; + } + + /** + * Set type + * + * @param array $value + */ + public function setType($value) + { + $enum = array('pie', 'doughnut', 'line', 'bar', 'area', 'radar', 'scatter'); + $this->type = $this->setEnumVal($value, $enum, 'pie'); + } + + /** + * Get labels + * + * @return array + */ + public function getLabels() + { + return $this->labels; + } + + /** + * Set labels + * + * @param array $value + */ + public function setLabels($value) + { + $this->labels = $value; + } + + /** + * Get data + * + * @return array + */ + public function getData() + { + return $this->data; + } + + /** + * Set data + * + * @param array $value + */ + public function setData($value) + { + $this->data = $value; + } +} diff --git a/src/PhpWord/Element/Footnote.php b/src/PhpWord/Element/Footnote.php index e263933c..162a703e 100644 --- a/src/PhpWord/Element/Footnote.php +++ b/src/PhpWord/Element/Footnote.php @@ -37,7 +37,7 @@ class Footnote extends AbstractContainer protected $paragraphStyle; /** - * Is part of collection; true for Title, Footnote, and Endnote + * Is part of collection * * @var bool */ diff --git a/src/PhpWord/Element/Title.php b/src/PhpWord/Element/Title.php index a4faeb70..8d385845 100644 --- a/src/PhpWord/Element/Title.php +++ b/src/PhpWord/Element/Title.php @@ -47,7 +47,7 @@ class Title extends AbstractElement private $style; /** - * Is part of collection; true for Title, Footnote, and Endnote + * Is part of collection * * @var bool */ diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index de9d8433..d4c8df9c 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -26,9 +26,11 @@ use PhpOffice\PhpWord\Exception\Exception; * @method Collection\Titles getTitles() * @method Collection\Footnotes getFootnotes() * @method Collection\Endnotes getEndnotes() + * @method Collection\Charts getCharts() * @method int addTitle(Element\Title $title) * @method int addFootnote(Element\Footnote $footnote) * @method int addEndnote(Element\Endnote $endnote) + * @method int addChart(Element\Chart $chart) * * @method Style\Paragraph addParagraphStyle(string $styleName, array $styles) * @method Style\Font addFontStyle(string $styleName, mixed $fontStyle, mixed $paragraphStyle = null) @@ -80,7 +82,7 @@ class PhpWord { $this->documentProperties = new DocumentProperties(); - $collections = array('Titles', 'Footnotes', 'Endnotes'); + $collections = array('Titles', 'Footnotes', 'Endnotes', 'Charts'); foreach ($collections as $collection) { $class = 'PhpOffice\\PhpWord\\Collection\\' . $collection; $this->collections[$collection] = new $class(); @@ -108,7 +110,7 @@ class PhpWord $addCollection = array(); $addStyle = array(); - $collections = array('Title', 'Footnote', 'Endnote'); + $collections = array('Title', 'Footnote', 'Endnote', 'Chart'); foreach ($collections as $collection) { $getCollection[] = strtolower("get{$collection}s"); $addCollection[] = strtolower("add{$collection}"); diff --git a/src/PhpWord/Shared/XMLWriter.php b/src/PhpWord/Shared/XMLWriter.php index cb00c70b..b2057bb9 100644 --- a/src/PhpWord/Shared/XMLWriter.php +++ b/src/PhpWord/Shared/XMLWriter.php @@ -139,6 +139,29 @@ class XMLWriter } } + /** + * Write simple element and attribute(s) block + * + * There are two options: + * 1. If the `$attributes` is an array, then it's an associative array of attributes + * 2. If not, then it's a simple attribute-value pair + * + * @param string $element + * @param string|array $attributes + * @param string $value + */ + public function writeBlock($element, $attributes, $value = null) + { + $this->xmlWriter->startElement($element); + if (!is_array($attributes)) { + $attributes = array($attributes => $value); + } + foreach ($attributes as $attribute => $value) { + $this->xmlWriter->writeAttribute($attribute, $value); + } + $this->xmlWriter->endElement(); + } + /** * Write element if ... * diff --git a/src/PhpWord/Writer/Word2007.php b/src/PhpWord/Writer/Word2007.php index 751b58b6..d15f40ac 100644 --- a/src/PhpWord/Writer/Word2007.php +++ b/src/PhpWord/Writer/Word2007.php @@ -71,6 +71,7 @@ class Word2007 extends AbstractWriter implements WriterInterface 'Footer' => '', 'Footnotes' => '', 'Endnotes' => '', + 'Chart' => '', ); foreach (array_keys($this->parts) as $partName) { $partClass = get_class($this) . '\\Part\\' . $partName; @@ -127,6 +128,7 @@ class Word2007 extends AbstractWriter implements WriterInterface $this->addNotes($zip, $rId, 'footnote'); $this->addNotes($zip, $rId, 'endnote'); + $this->addChart($zip, $rId); // Write parts foreach ($this->parts as $partName => $fileName) { @@ -249,6 +251,40 @@ class Word2007 extends AbstractWriter implements WriterInterface } } + /** + * Add chart + * + * @param \PhpOffice\PhpWord\Shared\ZipArchive $zip + * @param integer $rId + */ + private function addChart(ZipArchive $zip, &$rId) + { + $phpWord = $this->getPhpWord(); + + $collection = $phpWord->getCharts(); + $index = 0; + if ($collection->countItems() > 0) { + foreach ($collection->getItems() as $chart) { + $index++; + $rId++; + $filename = "charts/chart{$index}.xml"; + + // ContentTypes.xml + $this->contentTypes['override']["/word/{$filename}"] = 'chart'; + + // word/_rels/document.xml.rel + $this->relationships[] = array('target' => $filename, 'type' => 'chart', 'rID' => $rId); + + // word/charts/chartN.xml + /** @var \PhpOffice\PhpWord\Element\Chart $chart */ + $chart->setRelationId($rId); + $writerPart = $this->getWriterPart('Chart'); + $writerPart->setElement($chart); + $zip->addFromString("word/{$filename}", $writerPart->write()); + } + } + } + /** * Register content types for each media * diff --git a/src/PhpWord/Writer/Word2007/Element/Chart.php b/src/PhpWord/Writer/Word2007/Element/Chart.php new file mode 100644 index 00000000..3c4e43f2 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Element/Chart.php @@ -0,0 +1,79 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof ChartElement) { + return; + } + + if (!$this->withoutP) { + $xmlWriter->startElement('w:p'); + } + + $xmlWriter->startElement('w:r'); + $xmlWriter->startElement('w:drawing'); + $xmlWriter->startElement('wp:inline'); + + // EMU + $xmlWriter->startElement('wp:extent'); + $xmlWriter->writeAttribute('cx', '2000000'); + $xmlWriter->writeAttribute('cy', '2000000'); + $xmlWriter->endElement(); // wp:extent + + $xmlWriter->startElement('wp:docPr'); + $xmlWriter->writeAttribute('id', $element->getRelationId()); + $xmlWriter->writeAttribute('name', 'Chart'. $element->getRelationId()); + $xmlWriter->endElement(); // wp:docPr + + $xmlWriter->startElement('a:graphic'); + $xmlWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $xmlWriter->startElement('a:graphicData'); + $xmlWriter->writeAttribute('uri', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + + $xmlWriter->startElement('c:chart'); + $xmlWriter->writeAttribute('r:id', 'rId' . $element->getRelationId()); + $xmlWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $xmlWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $xmlWriter->endElement(); // c:chart + + $xmlWriter->endElement(); // a:graphicData + $xmlWriter->endElement(); // a:graphic + + $xmlWriter->endElement(); // wp:inline + $xmlWriter->endElement(); // w:drawing + $xmlWriter->endElement(); // w:r + + $this->endElementP(); // w:p + } +} diff --git a/src/PhpWord/Writer/Word2007/Part/Chart.php b/src/PhpWord/Writer/Word2007/Part/Chart.php new file mode 100644 index 00000000..13525fe7 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Part/Chart.php @@ -0,0 +1,331 @@ +element = $element; + } + + /** + * Write part + * + * @return string + */ + public function write() + { + $xmlWriter = $this->getXmlWriter(); + + $xmlWriter->startDocument('1.0', 'UTF-8', 'yes'); + $xmlWriter->startElement('c:chartSpace'); + $xmlWriter->writeAttribute('xmlns:c', 'http://schemas.openxmlformats.org/drawingml/2006/chart'); + $xmlWriter->writeAttribute('xmlns:a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $xmlWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + + $xmlWriter->writeBlock('c:roundedCorners', 'val', '0'); + + $xmlWriter->startElement('c:chart'); + $this->writePlotArea($xmlWriter); + $xmlWriter->endElement(); // c:chart + + $xmlWriter->endElement(); // c:chartSpace + + return $xmlWriter->getData(); + } + + /** + * Write plot area + */ + private function writePlotArea(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:plotArea'); + + $method = "write{$this->element->getType()}Chart"; + $this->$method($xmlWriter); + + $xmlWriter->endElement(); // c:plotArea + } + + /** + * Write pie chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_PieChart.html + */ + private function writePieChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:pieChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '1'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:pie3DChart + } + + /** + * Write doughnut chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_DoughnutChart.html + */ + private function writeDoughnutChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:doughnutChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '1'); + $xmlWriter->writeBlock('c:holeSize', 'val', '75'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:doughnutChart + } + + /** + * Write bar chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_BarChart.html + */ + private function writeBarChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:barChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '0'); + $xmlWriter->writeBlock('c:barDir', 'val', 'col'); // bar|col + $xmlWriter->writeBlock('c:axId', 'val', '1'); + $xmlWriter->writeBlock('c:axId', 'val', '2'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:barChart + + // Axes + $this->writeAxis($xmlWriter, 'cat'); + $this->writeAxis($xmlWriter, 'val'); + } + + /** + * Write line chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_LineChart.html + */ + private function writeLineChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:lineChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '0'); + $xmlWriter->writeBlock('c:axId', 'val', '1'); + $xmlWriter->writeBlock('c:axId', 'val', '2'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:lineChart + + // Axes + $this->writeAxis($xmlWriter, 'cat'); + $this->writeAxis($xmlWriter, 'val'); + } + + /** + * Write area chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_AreaChart.html + */ + private function writeAreaChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:areaChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '0'); + $xmlWriter->writeBlock('c:axId', 'val', '1'); + $xmlWriter->writeBlock('c:axId', 'val', '2'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:areaChart + + // Axes + $this->writeAxis($xmlWriter, 'cat'); + $this->writeAxis($xmlWriter, 'val'); + } + + /** + * Write radar chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_RadarChart.html + */ + private function writeRadarChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:radarChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '0'); + $xmlWriter->writeBlock('c:radarStyle', 'val', 'standard'); + $xmlWriter->writeBlock('c:axId', 'val', '1'); + $xmlWriter->writeBlock('c:axId', 'val', '2'); + + $this->writeSeries($xmlWriter); + + $xmlWriter->endElement(); // c:radarChart + + // Axes + $this->writeAxis($xmlWriter, 'cat'); + $this->writeAxis($xmlWriter, 'val'); + } + + /** + * Write scatter chart + * + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_ScatterChart.html + */ + private function writeScatterChart(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('c:scatterChart'); + + $xmlWriter->writeBlock('c:varyColors', 'val', '0'); + $xmlWriter->writeBlock('c:scatterStyle', 'val', 'lineMarker'); + $xmlWriter->writeBlock('c:axId', 'val', '1'); + $xmlWriter->writeBlock('c:axId', 'val', '2'); + + $this->writeSeries($xmlWriter, true); + + $xmlWriter->endElement(); // c:scatterChart + + // Axes + $this->writeAxis($xmlWriter, 'cat'); + $this->writeAxis($xmlWriter, 'val'); + } + + /** + * Write series + * + * @param \PhpOffice\PhpWord\Shared\XMLWriter $xmlWriter + * @param bool $scatter + */ + private function writeSeries(XMLWriter $xmlWriter, $scatter = false) + { + $xmlWriter->startElement('c:ser'); + + $xmlWriter->writeBlock('c:idx', 'val', '0'); + $xmlWriter->writeBlock('c:order', 'val', '0'); + + if ($scatter === true) { + $this->writeSeriesItems($xmlWriter, 'xVal', $this->element->getLabels()); + $this->writeSeriesItems($xmlWriter, 'yVal', $this->element->getData()); + } else { + $this->writeSeriesItems($xmlWriter, 'cat', $this->element->getLabels()); + $this->writeSeriesItems($xmlWriter, 'val', $this->element->getData()); + } + + $xmlWriter->endElement(); // c:ser + } + + /** + * Write series items + * + * @param \PhpOffice\PhpWord\Shared\XMLWriter $xmlWriter + * @param string $type + * @param array $values + */ + private function writeSeriesItems(XMLWriter $xmlWriter, $type, $values) + { + $types = array( + 'cat' => array('c:cat', 'c:strRef', 'c:strCache'), + 'val' => array('c:val', 'c:numRef', 'c:numCache'), + 'xVal' => array('c:xVal', 'c:strRef', 'c:strCache'), + 'yVal' => array('c:yVal', 'c:numRef', 'c:numCache'), + ); + list($itemType, $itemRef, $itemCache) = $types[$type]; + + $xmlWriter->startElement($itemType); + $xmlWriter->startElement($itemRef); + $xmlWriter->startElement($itemCache); + + $index = 0; + foreach ($values as $value) { + $xmlWriter->startElement('c:pt'); + $xmlWriter->writeAttribute('idx', $index); + + $xmlWriter->startElement('c:v'); + $xmlWriter->writeRaw($value); + $xmlWriter->endElement(); // c:v + + $xmlWriter->endElement(); // c:pt + $index++; + } + + $xmlWriter->endElement(); // $itemCache + + $xmlWriter->endElement(); // $itemRef + $xmlWriter->endElement(); // $itemType + } + + /** + * Write axis + * + * @param \PhpOffice\PhpWord\Shared\XMLWriter $xmlWriter + * @param string $type + * @link http://www.datypic.com/sc/ooxml/t-draw-chart_CT_CatAx.html + */ + private function writeAxis(XMLWriter $xmlWriter, $type) + { + $types = array( + 'cat' => array('c:catAx', '1', 'b', '2'), + 'val' => array('c:valAx', '2', 'l', '1'), + ); + list($axisType, $axisId, $axisPos, $axisCross) = $types[$type]; + + $xmlWriter->startElement($axisType); + + $xmlWriter->writeBlock('c:axId', 'val', $axisId); + $xmlWriter->writeBlock('c:axPos', 'val', $axisPos); + $xmlWriter->writeBlock('c:crossAx', 'val', $axisCross); + + $xmlWriter->startElement('c:scaling'); + $xmlWriter->writeBlock('c:orientation', 'val', 'minMax'); + $xmlWriter->endElement(); // c:scaling + + $xmlWriter->startElement('c:spPr'); + $xmlWriter->writeElement('a:noFill'); + $xmlWriter->startElement('a:ln'); + $xmlWriter->startElement('a:solidFill'); + $xmlWriter->writeBlock('a:srgbClr', 'val', '0FB7'); + $xmlWriter->endElement(); // a:solidFill + $xmlWriter->endElement(); // a:ln + $xmlWriter->endElement(); // c:crossAx + + $xmlWriter->endElement(); // $type + } +} diff --git a/src/PhpWord/Writer/Word2007/Part/ContentTypes.php b/src/PhpWord/Writer/Word2007/Part/ContentTypes.php index b6f23f47..ac636d3a 100644 --- a/src/PhpWord/Writer/Word2007/Part/ContentTypes.php +++ b/src/PhpWord/Writer/Word2007/Part/ContentTypes.php @@ -37,6 +37,7 @@ class ContentTypes extends AbstractPart $openXMLPrefix = 'application/vnd.openxmlformats-'; $wordMLPrefix = $openXMLPrefix . 'officedocument.wordprocessingml.'; + $drawingMLPrefix = $openXMLPrefix . 'officedocument.drawingml.'; $overrides = array( '/docProps/core.xml' => $openXMLPrefix . 'package.core-properties+xml', '/docProps/app.xml' => $openXMLPrefix . 'officedocument.extended-properties+xml', @@ -53,7 +54,11 @@ class ContentTypes extends AbstractPart $defaults = $contentTypes['default']; if (!empty($contentTypes['override'])) { foreach ($contentTypes['override'] as $key => $val) { - $overrides[$key] = $wordMLPrefix . $val . '+xml'; + if ($val == 'chart') { + $overrides[$key] = $drawingMLPrefix . $val . '+xml'; + } else { + $overrides[$key] = $wordMLPrefix . $val . '+xml'; + } } } diff --git a/tests/PhpWord/Tests/Writer/Word2007/ElementTest.php b/tests/PhpWord/Tests/Writer/Word2007/ElementTest.php index c774e9f3..2e76df12 100644 --- a/tests/PhpWord/Tests/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Tests/Writer/Word2007/ElementTest.php @@ -41,7 +41,7 @@ class ElementTest extends \PHPUnit_Framework_TestCase $elements = array( 'CheckBox', 'Container', 'Footnote', 'Image', 'Link', 'ListItem', 'ListItemRun', 'Object', 'PreserveText', 'Table', 'Text', 'TextBox', 'TextBreak', 'Title', 'TOC', - 'Field', 'Line', 'Shape' + 'Field', 'Line', 'Shape', 'Chart' ); foreach ($elements as $element) { $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $element;