New features when creating charts (#1332)

* add stacked bar and column charts
* add chart colors feature
* adding preliminary chart axis title functionality to XMLwriter
* added percent_stacked to available types array
* Make tick mark and tick label positions configurable
* scrutinizer fixes
* update changelog
This commit is contained in:
JAEK-S 2018-05-11 14:50:19 -06:00 committed by troosan
parent 94be56b0ec
commit 65b0f062ad
6 changed files with 531 additions and 21 deletions

View File

@ -22,6 +22,7 @@ v0.15.0 (?? ??? 2018)
- Added the ability to enable gridlines and axislabels on charts @FrankMeyer #576
- Add support for table indent (tblInd) @Trainmaster #1343
- Added parsing of internal links in HTML reader @lalop #1336
- Several improvements to charts @JAEK-S #1332
### Fixed
- Fix reading of docx default style - @troosan #1238

View File

@ -16,8 +16,8 @@ $section = $phpWord->addSection();
$section->addTitle('2D charts', 1);
$section = $phpWord->addSection(array('colsNum' => 2, 'breakType' => 'continuous'));
$chartTypes = array('pie', 'doughnut', 'bar', 'column', 'line', 'area', 'scatter', 'radar');
$twoSeries = array('bar', 'column', 'line', 'area', 'scatter', 'radar');
$chartTypes = array('pie', 'doughnut', 'bar', 'column', 'line', 'area', 'scatter', 'radar', 'stacked_bar', 'percent_stacked_bar', 'stacked_column', 'percent_stacked_column');
$twoSeries = array('bar', 'column', 'line', 'area', 'scatter', 'radar', 'stacked_bar', 'percent_stacked_bar', 'stacked_column', 'percent_stacked_column');
$threeSeries = array('bar', 'line');
$categories = array('A', 'B', 'C', 'D', 'E');
$series1 = array(1, 3, 2, 5, 4);

View File

@ -61,11 +61,12 @@ class Chart extends AbstractElement
* @param array $categories
* @param array $values
* @param array $style
* @param null|mixed $seriesName
*/
public function __construct($type, $categories, $values, $style = null)
public function __construct($type, $categories, $values, $style = null, $seriesName = null)
{
$this->setType($type);
$this->addSeries($categories, $values);
$this->addSeries($categories, $values, $seriesName);
$this->style = $this->setNewStyle(new ChartStyle(), $style, true);
}
@ -86,7 +87,7 @@ class Chart extends AbstractElement
*/
public function setType($value)
{
$enum = array('pie', 'doughnut', 'line', 'bar', 'column', 'area', 'radar', 'scatter');
$enum = array('pie', 'doughnut', 'line', 'bar', 'stacked_bar', 'percent_stacked_bar', 'column', 'stacked_column', 'percent_stacked_column', 'area', 'radar', 'scatter');
$this->type = $this->setEnumVal($value, $enum, 'pie');
}
@ -95,10 +96,15 @@ class Chart extends AbstractElement
*
* @param array $categories
* @param array $values
* @param null|mixed $name
*/
public function addSeries($categories, $values)
public function addSeries($categories, $values, $name = null)
{
$this->series[] = array('categories' => $categories, 'values' => $values);
$this->series[] = array(
'categories' => $categories,
'values' => $values,
'name' => $name,
);
}
/**

View File

@ -46,6 +46,60 @@ class Chart extends AbstractStyle
private $is3d = false;
/**
* A list of colors to use in the chart
*
* @var array
*/
private $colors = array();
/**
* A list of display options for data labels
*
* @var array
*/
private $dataLabelOptions = array(
'showVal' => true, // value
'showCatName' => true, // category name
'showLegendKey' => false, //show the cart legend
'showSerName' => false, // series name
'showPercent' => false,
'showLeaderLines' => false,
'showBubbleSize' => false,
);
/**
* A string that tells the writer where to write chart labels or to skip
* "nextTo" - sets labels next to the axis (bar graphs on the left) (default)
* "low" - labels on the left side of the graph
* "high" - labels on the right side of the graph
*
* @var string
*/
private $categoryLabelPosition = 'nextTo';
/**
* A string that tells the writer where to write chart labels or to skip
* "nextTo" - sets labels next to the axis (bar graphs on the bottom) (default)
* "low" - labels are below the graph
* "high" - labels above the graph
*
* @var string
*/
private $valueLabelPosition = 'nextTo';
/**
* @var string
*/
private $categoryAxisTitle;
/**
* @var string
*/
private $valueAxisTitle;
private $majorTickMarkPos = 'none';
/*
* Show labels for axis
*
* @var bool
@ -146,6 +200,28 @@ class Chart extends AbstractStyle
}
/**
* Get the list of colors to use in a chart.
*
* @return array
*/
public function getColors()
{
return $this->colors;
}
/**
* Set the colors to use in a chart.
*
* @param array $value a list of colors to use in the chart
*/
public function setColors($value = array())
{
$this->colors = $value;
return $this;
}
/*
* Show labels for axis
*
* @return bool
@ -169,6 +245,31 @@ class Chart extends AbstractStyle
}
/**
* get the list of options for data labels
*
* @return array
*/
public function getDataLabelOptions()
{
return $this->dataLabelOptions;
}
/**
* Set values for data label options.
* This will only change values for options defined in $this->dataLabelOptions, and cannot create new ones.
*
* @param array $values [description]
*/
public function setDataLabelOptions($values = array())
{
foreach (array_keys($this->dataLabelOptions) as $option) {
if (isset($values[$option])) {
$this->dataLabelOptions[$option] = $this->setBoolVal($values[$option], $this->dataLabelOptions[$option]);
}
}
}
/*
* Show Gridlines for Y-Axis
*
* @return bool
@ -192,6 +293,117 @@ class Chart extends AbstractStyle
}
/**
* Get the categoryLabelPosition setting
*
* @return string
*/
public function getCategoryLabelPosition()
{
return $this->categoryLabelPosition;
}
/**
* Set the categoryLabelPosition setting
* "none" - skips writing labels
* "nextTo" - sets labels next to the (bar graphs on the left)
* "low" - labels on the left side of the graph
* "high" - labels on the right side of the graph
*
* @param mixed $labelPosition
* @return self
*/
public function setCategoryLabelPosition($labelPosition)
{
$enum = array('nextTo', 'low', 'high');
$this->categoryLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->categoryLabelPosition);
return $this;
}
/**
* Get the valueAxisLabelPosition setting
*
* @return string
*/
public function getValueLabelPosition()
{
return $this->valueLabelPosition;
}
/**
* Set the valueLabelPosition setting
* "none" - skips writing labels
* "nextTo" - sets labels next to the value
* "low" - sets labels are below the graph
* "high" - sets labels above the graph
*
* @param string
* @param mixed $labelPosition
*/
public function setValueLabelPosition($labelPosition)
{
$enum = array('nextTo', 'low', 'high');
$this->valueLabelPosition = $this->setEnumVal($labelPosition, $enum, $this->valueLabelPosition);
return $this;
}
/**
* Get the categoryAxisTitle
* @return string
*/
public function getCategoryAxisTitle()
{
return $this->categoryAxisTitle;
}
/**
* Set the title that appears on the category side of the chart
* @param string $axisTitle
*/
public function setCategoryAxisTitle($axisTitle)
{
$this->categoryAxisTitle = $axisTitle;
return $this;
}
/**
* Get the valueAxisTitle
* @return string
*/
public function getValueAxisTitle()
{
return $this->valueAxisTitle;
}
/**
* Set the title that appears on the value side of the chart
* @param string $axisTitle
*/
public function setValueAxisTitle($axisTitle)
{
$this->valueAxisTitle = $axisTitle;
return $this;
}
public function getMajorTickPosition()
{
return $this->majorTickMarkPos;
}
/**
* set the position for major tick marks
* @param string $position [description]
*/
public function setMajorTickPosition($position)
{
$enum = array('in', 'out', 'cross', 'none');
$this->majorTickMarkPos = $this->setEnumVal($position, $enum, $this->majorTickMarkPos);
}
/*
* Show Gridlines for X-Axis
*
* @return bool

View File

@ -43,8 +43,12 @@ class Chart extends AbstractPart
private $types = array(
'pie' => array('type' => 'pie', 'colors' => 1),
'doughnut' => array('type' => 'doughnut', 'colors' => 1, 'hole' => 75, 'no3d' => true),
'bar' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar'),
'column' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col'),
'bar' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'clustered'),
'stacked_bar' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'stacked'),
'percent_stacked_bar' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'bar', 'grouping' => 'percentStacked'),
'column' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'clustered'),
'stacked_column' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'stacked'),
'percent_stacked_column' => array('type' => 'bar', 'colors' => 0, 'axes' => true, 'bar' => 'col', 'grouping' => 'percentStacked'),
'line' => array('type' => 'line', 'colors' => 0, 'axes' => true),
'area' => array('type' => 'area', 'colors' => 0, 'axes' => true),
'radar' => array('type' => 'radar', 'colors' => 0, 'axes' => true, 'radar' => 'standard', 'no3d' => true),
@ -145,7 +149,7 @@ class Chart extends AbstractPart
}
if (isset($this->options['bar'])) {
$xmlWriter->writeElementBlock('c:barDir', 'val', $this->options['bar']); // bar|col
$xmlWriter->writeElementBlock('c:grouping', 'val', 'clustered'); // 3d; standard = percentStacked
$xmlWriter->writeElementBlock('c:grouping', 'val', $this->options['grouping']); // 3d; standard = percentStacked
}
if (isset($this->options['radar'])) {
$xmlWriter->writeElementBlock('c:radarStyle', 'val', $this->options['radar']);
@ -157,6 +161,8 @@ class Chart extends AbstractPart
// Series
$this->writeSeries($xmlWriter, isset($this->options['scatter']));
$xmlWriter->writeElementBlock('c:overlap', 'val', '100');
// Axes
if (isset($this->options['axes'])) {
$xmlWriter->writeElementBlock('c:axId', 'val', 1);
@ -183,6 +189,8 @@ class Chart extends AbstractPart
private function writeSeries(XMLWriter $xmlWriter, $scatter = false)
{
$series = $this->element->getSeries();
$style = $this->element->getStyle();
$colors = $style->getColors();
$index = 0;
foreach ($series as $seriesItem) {
@ -194,6 +202,32 @@ class Chart extends AbstractPart
$xmlWriter->writeElementBlock('c:idx', 'val', $index);
$xmlWriter->writeElementBlock('c:order', 'val', $index);
if (!is_null($seriesItem['name']) && $seriesItem['name'] != '') {
$xmlWriter->startElement('c:tx');
$xmlWriter->startElement('c:strRef');
$xmlWriter->startElement('c:strCache');
$xmlWriter->writeElementBlock('c:ptCount', 'val', 1);
$xmlWriter->startElement('c:pt');
$xmlWriter->writeAttribute('idx', 0);
$xmlWriter->startElement('c:v');
$xmlWriter->writeRaw($seriesItem['name']);
$xmlWriter->endElement(); // c:v
$xmlWriter->endElement(); // c:pt
$xmlWriter->endElement(); // c:strCache
$xmlWriter->endElement(); // c:strRef
$xmlWriter->endElement(); // c:tx
}
// The c:dLbls was added to make word charts look more like the reports in SurveyGizmo
// This section needs to be made configurable before a pull request is made
$xmlWriter->startElement('c:dLbls');
foreach ($style->getDataLabelOptions() as $option => $val) {
$xmlWriter->writeElementBlock("c:{$option}", 'val', (int) $val);
}
$xmlWriter->endElement(); // c:dLbls
if (isset($this->options['scatter'])) {
$this->writeShape($xmlWriter);
}
@ -204,6 +238,26 @@ class Chart extends AbstractPart
} else {
$this->writeSeriesItem($xmlWriter, 'cat', $categories);
$this->writeSeriesItem($xmlWriter, 'val', $values);
// setting the chart colors was taken from https://github.com/PHPOffice/PHPWord/issues/494
if (is_array($colors) && count($colors)) {
// This is a workaround to make each series in a stack chart use a different color
if ($this->options['type'] == 'bar' && stristr($this->options['grouping'], 'stacked')) {
array_shift($colors);
}
$colorIndex = 0;
foreach ($colors as $color) {
$xmlWriter->startElement('c:dPt');
$xmlWriter->writeElementBlock('c:idx', 'val', $colorIndex);
$xmlWriter->startElement('c:spPr');
$xmlWriter->startElement('a:solidFill');
$xmlWriter->writeElementBlock('a:srgbClr', 'val', $color);
$xmlWriter->endElement(); // a:solidFill
$xmlWriter->endElement(); // c:spPr
$xmlWriter->endElement(); // c:dPt
$colorIndex++;
}
}
}
$xmlWriter->endElement(); // c:ser
@ -230,14 +284,19 @@ class Chart extends AbstractPart
$xmlWriter->startElement($itemType);
$xmlWriter->startElement($itemLit);
$xmlWriter->writeElementBlock('c:ptCount', 'val', count($values));
$index = 0;
foreach ($values as $value) {
$xmlWriter->startElement('c:pt');
$xmlWriter->writeAttribute('idx', $index);
if (\PhpOffice\PhpWord\Settings::isOutputEscapingEnabled()) {
$xmlWriter->writeElement('c:v', $value);
} else {
$xmlWriter->startElement('c:v');
$xmlWriter->text($value);
$xmlWriter->writeRaw($value);
$xmlWriter->endElement(); // c:v
}
$xmlWriter->endElement(); // c:pt
$index++;
}
@ -266,15 +325,33 @@ class Chart extends AbstractPart
$xmlWriter->writeElementBlock('c:axId', 'val', $axisId);
$xmlWriter->writeElementBlock('c:axPos', 'val', $axisPos);
$categoryAxisTitle = $style->getCategoryAxisTitle();
$valueAxisTitle = $style->getValueAxisTitle();
if ($axisType == 'c:catAx') {
if (isset($categoryAxisTitle)) {
$this->writeAxisTitle($xmlWriter, $categoryAxisTitle);
}
} elseif ($axisType == 'c:valAx') {
if (isset($valueAxisTitle)) {
$this->writeAxisTitle($xmlWriter, $valueAxisTitle);
}
}
$xmlWriter->writeElementBlock('c:crossAx', 'val', $axisCross);
$xmlWriter->writeElementBlock('c:auto', 'val', 1);
if (isset($this->options['axes'])) {
$xmlWriter->writeElementBlock('c:delete', 'val', 0);
$xmlWriter->writeElementBlock('c:majorTickMark', 'val', 'none');
$xmlWriter->writeElementBlock('c:majorTickMark', 'val', $style->getMajorTickPosition());
$xmlWriter->writeElementBlock('c:minorTickMark', 'val', 'none');
if ($style->showAxisLabels()) {
$xmlWriter->writeElementBlock('c:tickLblPos', 'val', 'nextTo');
if ($axisType == 'c:catAx') {
$xmlWriter->writeElementBlock('c:tickLblPos', 'val', $style->getCategoryLabelPosition());
} else {
$xmlWriter->writeElementBlock('c:tickLblPos', 'val', $style->getValueLabelPosition());
}
} else {
$xmlWriter->writeElementBlock('c:tickLblPos', 'val', 'none');
}
@ -312,4 +389,30 @@ class Chart extends AbstractPart
$xmlWriter->endElement(); // a:ln
$xmlWriter->endElement(); // c:spPr
}
private function writeAxisTitle(XMLWriter $xmlWriter, $title)
{
$xmlWriter->startElement('c:title'); //start c:title
$xmlWriter->startElement('c:tx'); //start c:tx
$xmlWriter->startElement('c:rich'); // start c:rich
$xmlWriter->writeElement('a:bodyPr');
$xmlWriter->writeElement('a:lstStyle');
$xmlWriter->startElement('a:p');
$xmlWriter->startElement('a:pPr');
$xmlWriter->writeElement('a:defRPr');
$xmlWriter->endElement(); // end a:pPr
$xmlWriter->startElement('a:r');
$xmlWriter->writeElementBlock('a:rPr', 'lang', 'en-US');
$xmlWriter->startElement('a:t');
$xmlWriter->writeRaw($title);
$xmlWriter->endElement(); //end a:t
$xmlWriter->endElement(); // end a:r
$xmlWriter->endElement(); //end a:p
$xmlWriter->endElement(); //end c:rich
$xmlWriter->endElement(); // end c:tx
$xmlWriter->writeElementBlock('c:overlay', 'val', '0');
$xmlWriter->endElement(); // end c:title
}
}

View File

@ -0,0 +1,188 @@
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @see https://github.com/PHPOffice/PHPWord
* @copyright 2010-2017 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Style;
/**
* Test class for PhpOffice\PhpWord\Style\Chart
*
* @coversDefaultClass \PhpOffice\PhpWord\Style\Chart
* @runTestsInSeparateProcesses
*/
class ChartTest extends \PHPUnit\Framework\TestCase
{
/**
* Testing getter and setter for chart width
*/
public function testSetGetWidth()
{
$chart = new Chart();
$this->assertEquals($chart->getWidth(), 1000000);
$chart->setWidth(200);
$this->assertEquals($chart->getWidth(), 200);
}
/**
* Testing getter and setter for chart height
*/
public function testSetGetHeight()
{
$chart = new Chart();
$this->assertEquals($chart->getHeight(), 1000000);
$chart->setHeight(200);
$this->assertEquals($chart->getHeight(), 200);
}
/**
* Testing getter and setter for is3d
*/
public function testSetIs3d()
{
$chart = new Chart();
$this->assertEquals($chart->is3d(), false);
$chart->set3d(true);
$this->assertEquals($chart->is3d(), true);
}
/**
* Testing getter and setter for chart colors
*/
public function testSetGetColors()
{
$chart = new Chart();
$this->assertInternalType('array', $chart->getColors());
$this->assertEquals(count($chart->getColors()), 0);
$chart->setColors(array('FFFFFFFF', 'FF000000', 'FFFF0000'));
$this->assertEquals($chart->getColors(), array('FFFFFFFF', 'FF000000', 'FFFF0000'));
}
/**
* Testing getter and setter for dataLabelOptions
*/
public function testSetGetDataLabelOptions()
{
$chart = new Chart();
$originalDataLabelOptions = array(
'showVal' => true,
'showCatName' => true,
'showLegendKey' => false,
'showSerName' => false,
'showPercent' => false,
'showLeaderLines' => false,
'showBubbleSize' => false,
);
$this->assertEquals($chart->getDataLabelOptions(), $originalDataLabelOptions);
$changedDataLabelOptions = array(
'showVal' => false,
'showCatName' => false,
'showLegendKey' => true,
'showSerName' => true,
'showPercent' => true,
'showLeaderLines' => true,
'showBubbleSize' => true,
);
$chart->setDataLabelOptions(
array(
'showVal' => false,
'showCatName' => false,
'showLegendKey' => true,
'showSerName' => true,
'showPercent' => true,
'showLeaderLines' => true,
'showBubbleSize' => true,
)
);
$this->assertEquals($chart->getDataLabelOptions(), $changedDataLabelOptions);
}
/**
* Testing categoryLabelPosition getter and setter
*/
public function testSetGetCategoryLabelPosition()
{
$chart = new Chart();
$this->assertEquals($chart->getCategoryLabelPosition(), 'nextTo');
$chart->setCategoryLabelPosition('high');
$this->assertEquals($chart->getCategoryLabelPosition(), 'high');
}
/**
* Testing valueLabelPosition getter and setter
*/
public function testSetGetValueLabelPosition()
{
$chart = new Chart();
$this->assertEquals($chart->getValueLabelPosition(), 'nextTo');
$chart->setValueLabelPosition('low');
$this->assertEquals($chart->getValueLabelPosition(), 'low');
}
/**
* Testing categoryAxisTitle getter and setter
*/
public function testSetGetCategoryAxisTitle()
{
$chart = new Chart();
$chart->getCategoryAxisTitle();
$this->assertEquals($chart->getCategoryAxisTitle(), null);
$chart->setCategoryAxisTitle('Test Category Axis Title');
$this->assertEquals($chart->getCategoryAxisTitle(), 'Test Category Axis Title');
}
/**
* Testing valueAxisTitle getter and setter
*/
public function testSetGetValueAxisTitle()
{
$chart = new Chart();
$chart->getValueAxisTitle();
$this->assertEquals($chart->getValueAxisTitle(), null);
$chart->setValueAxisTitle('Test Value Axis Title');
$this->assertEquals($chart->getValueAxisTitle(), 'Test Value Axis Title');
}
}