Skip to content

Conditional Formatting Improvements for Xlsx #3372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions docs/topics/conditional-formatting.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ $conditional->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPERA
$conditional->addCondition(80);
$conditional->getStyle()->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_DARKGREEN);
$conditional->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$conditional->getStyle()->getFill()->getStartColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);
$conditional->getStyle()->getFill()->getEndColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);

$conditionalStyles = $spreadsheet->getActiveSheet()->getStyle('A1:A10')->getConditionalStyles();
$conditionalStyles[] = $conditional;
Expand All @@ -63,7 +63,7 @@ $wizard = $wizardFactory->newRule(\PhpOffice\PhpSpreadsheet\Style\ConditionalFor
$wizard->greaterThan(80);
$wizard->getStyle()->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_DARKGREEN);
$wizard->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$wizard->getStyle()->getFill()->getStartColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);
$wizard->getStyle()->getFill()->getEndColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);

$conditional = $wizard->getConditional();
```
Expand All @@ -84,7 +84,7 @@ $conditional2->setOperatorType(\PhpOffice\PhpSpreadsheet\Style\Conditional::OPER
$conditional2->addCondition(10);
$conditional2->getStyle()->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_DARKRED);
$conditional2->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$conditional2->getStyle()->getFill()->getStartColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED);
$conditional2->getStyle()->getFill()->getEndColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_RED);

$conditionalStyles = $spreadsheet->getActiveSheet()->getStyle('A1:A10')->getConditionalStyles();
$conditionalStyles[] = $conditional2;
Expand All @@ -98,17 +98,17 @@ $wizard = $wizardFactory->newRule(\PhpOffice\PhpSpreadsheet\Style\ConditionalFor
$wizard->lessThan(10);
$wizard->getStyle()->getFont()->getColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_DARKGREEN);
$wizard->getStyle()->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID);
$wizard->getStyle()->getFill()->getStartColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);
$wizard->getStyle()->getFill()->getEndColor()->setARGB(\PhpOffice\PhpSpreadsheet\Style\Color::COLOR_GREEN);

$conditional = $wizard->getConditional();
```


### Order of Evaluating Multiple Rules/Conditions

`$conditionalStyles` is an array, which not only represents multiple conditions that can be applied to a cell (or range of cells), but also the order in which they are checked. MS Excel will check each of those conditions in turn in the order they are defined; and will stop checking once it finds a first matching rule. This means that the order of checking conditions can be important.
`$conditionalStyles` is an array, which not only represents multiple conditions that can be applied to a cell (or range of cells), but also the order in which they are checked. Some spreadsheet programs stop processing conditions once they find a match. On the other hand, MS Excel will check each of those conditions in turn in the order they are defined. It will stop checking only if it finds a matching rule that specifies 'stop if true'; however, if it finds conflicting matches with conflicting formatting (e.g. both set a background fill color but use different choices), the first match wins. In either case, this means that the order of checking conditions can be important.

Consider the following. We have one condition that checks if a cell value is between -10 and 10, styling the cell in yellow if that condition matches; and a second condition that checks if the cell value is equal to 0, styling the cell in red if that matches.
Consider the following. We have one condition that checks if a cell value is between -2 and 2, styling the fill color of the cell in yellow if that condition matches; and a second condition that checks if the cell value is equal to 0, styling the fill color of the cell in red if that matches.
- Yellow if value is between -2 and 2
- Red if value equals 0

Expand All @@ -120,12 +120,22 @@ If the rule order is reversed
- Red if value equals 0
- Yellow if value is between -2 and 2

then the cell containing the value 0 will be rendered in red, because that is the first matching condition; and the between rule will not be assessed for that cell.
then the cell containing the value 0 will be rendered in red, because that is the first matching condition; and the formatting in the other condition conflicts with this, so is discarded.

![11-21-CF-Rule-Order-2.png](./images/11-21-CF-Rule-Order-2.png)

So when you have multiple conditions where the rules might "overlap", the order of these is important.

If the cell matches multiple conditions, Excel (but not most other spreadsheet programs) will apply non-conflicting styles from each match. So, for the example above, if we wanted a match of 0 to have a different *font* color rather than a different *fill* color, Excel can honor both.

![11-21-CF-Rule-Order-2.pic2.png](./images/11-21-CF-Rule-Order-2.pic2.png)

Here is the same spreadsheet opened in LibreOffice - cell A4 has only the first conditional style applied to it. (You would see the same if you checked 'Stop if True' in Excel.) If you want the spreadsheet to appear the same in both Excel and LibreOffice, you would need to use more complicated conditions.

![11-21-CF-Rule-Order-2.pic2.png](./images/11-21-CF-Rule-Order-2.pic3.png)

PhpSpreadsheet supports the setting of [Stop If True](#stop-if-true-and-no-format-set).


### Reader/Writer Support

Expand Down Expand Up @@ -704,8 +714,17 @@ This example can also be found in the [code samples](https://github.com/PHPOffic

## General Notes

### Stop If True
### Stop If True, and No Format Set

Normally, Excel continues to check even after it finds a match. To tell it to stop once a match is found, 'stop if true' should be specified:
```php
$conditional->setStopIfTrue(true);
```

Sometimes you want a matched cell to just show its unconditional format. This is most useful in conjunction with 'stop if true'.
```php
$conditional->setNoFormatSet(true);
```

### Changing the Cell Range

Expand Down
Binary file added docs/topics/images/11-21-CF-Rule-Order-2.pic2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-21-CF-Rule-Order-2.pic3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
$conditionals = [];
foreach ($xmlSheet->conditionalFormatting as $conditional) {
foreach ($conditional->cfRule as $cfRule) {
if (Conditional::isValidConditionType((string) $cfRule['type']) && isset($this->dxfs[(int) ($cfRule['dxfId'])])) {
if (Conditional::isValidConditionType((string) $cfRule['type']) && (!isset($cfRule['dxfId']) || isset($this->dxfs[(int) ($cfRule['dxfId'])]))) {
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
} elseif ((string) $cfRule['type'] == Conditional::CONDITION_DATABAR) {
$conditionals[(string) $conditional['sqref']][(int) ($cfRule['priority'])] = $cfRule;
Expand Down Expand Up @@ -197,6 +197,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
$objConditional = new Conditional();
$objConditional->setConditionType((string) $cfRule['type']);
$objConditional->setOperatorType((string) $cfRule['operator']);
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));

if ((string) $cfRule['text'] != '') {
$objConditional->setText((string) $cfRule['text']);
Expand Down Expand Up @@ -227,7 +228,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
$objConditional->setDataBar(
$this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line
);
} else {
} elseif (isset($cfRule['dxfId'])) {
$objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]);
}

Expand Down
22 changes: 17 additions & 5 deletions src/PhpSpreadsheet/Reader/Xlsx/Styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,21 @@ public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderSt
$borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH);
}

$this->readBorder($borderStyle->getLeft(), $borderStyleXml->left);
$this->readBorder($borderStyle->getRight(), $borderStyleXml->right);
$this->readBorder($borderStyle->getTop(), $borderStyleXml->top);
$this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom);
$this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal);
if (isset($borderStyleXml->left)) {
$this->readBorder($borderStyle->getLeft(), $borderStyleXml->left);
}
if (isset($borderStyleXml->right)) {
$this->readBorder($borderStyle->getRight(), $borderStyleXml->right);
}
if (isset($borderStyleXml->top)) {
$this->readBorder($borderStyle->getTop(), $borderStyleXml->top);
}
if (isset($borderStyleXml->bottom)) {
$this->readBorder($borderStyle->getBottom(), $borderStyleXml->bottom);
}
if (isset($borderStyleXml->diagonal)) {
$this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal);
}
}

private function getAttribute(SimpleXMLElement $xml, string $attribute): string
Expand All @@ -233,6 +243,8 @@ private function readBorder(Border $border, SimpleXMLElement $borderXml): void
$style = $this->getAttribute($borderXml, 'style');
if ($style !== '') {
$border->setBorderStyle((string) $style);
} else {
$border->setBorderStyle(Border::BORDER_NONE);
}
if (isset($borderXml->color)) {
$border->getColor()->setARGB($this->readColor($borderXml->color));
Expand Down
6 changes: 5 additions & 1 deletion src/PhpSpreadsheet/Style/Border.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Border extends Supervisor
const BORDER_SLANTDASHDOT = 'slantDashDot';
const BORDER_THICK = 'thick';
const BORDER_THIN = 'thin';
const BORDER_OMIT = 'omit'; // should be used only for Conditional

/**
* Border style.
Expand Down Expand Up @@ -48,7 +49,7 @@ class Border extends Supervisor
* Leave this value at default unless you understand exactly what
* its ramifications are
*/
public function __construct($isSupervisor = false)
public function __construct($isSupervisor = false, bool $isConditional = false)
{
// Supervisor?
parent::__construct($isSupervisor);
Expand All @@ -60,6 +61,9 @@ public function __construct($isSupervisor = false)
if ($isSupervisor) {
$this->color->bindParent($this, 'color');
}
if ($isConditional) {
$this->borderStyle = self::BORDER_OMIT;
}
}

/**
Expand Down
22 changes: 11 additions & 11 deletions src/PhpSpreadsheet/Style/Borders.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,27 @@ class Borders extends Supervisor
* Leave this value at default unless you understand exactly what
* its ramifications are
*/
public function __construct($isSupervisor = false)
public function __construct($isSupervisor = false, bool $isConditional = false)
{
// Supervisor?
parent::__construct($isSupervisor);

// Initialise values
$this->left = new Border($isSupervisor);
$this->right = new Border($isSupervisor);
$this->top = new Border($isSupervisor);
$this->bottom = new Border($isSupervisor);
$this->diagonal = new Border($isSupervisor);
$this->left = new Border($isSupervisor, $isConditional);
$this->right = new Border($isSupervisor, $isConditional);
$this->top = new Border($isSupervisor, $isConditional);
$this->bottom = new Border($isSupervisor, $isConditional);
$this->diagonal = new Border($isSupervisor, $isConditional);
$this->diagonalDirection = self::DIAGONAL_NONE;

// Specially for supervisor
if ($isSupervisor) {
// Initialize pseudo-borders
$this->allBorders = new Border(true);
$this->outline = new Border(true);
$this->inside = new Border(true);
$this->vertical = new Border(true);
$this->horizontal = new Border(true);
$this->allBorders = new Border(true, $isConditional);
$this->outline = new Border(true, $isConditional);
$this->inside = new Border(true, $isConditional);
$this->vertical = new Border(true, $isConditional);
$this->horizontal = new Border(true, $isConditional);

// bind parent if we are a supervisor
$this->left->bindParent($this, 'left');
Expand Down
15 changes: 15 additions & 0 deletions src/PhpSpreadsheet/Style/Conditional.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class Conditional implements IComparable
*/
private $style;

/** @var bool */
private $noFormatSet = false;

/**
* Create a new Conditional.
*/
Expand All @@ -124,6 +127,18 @@ public function __construct()
$this->style = new Style(false, true);
}

public function getNoFormatSet(): bool
{
return $this->noFormatSet;
}

public function setNoFormatSet(bool $noFormatSet): self
{
$this->noFormatSet = $noFormatSet;

return $this;
}

/**
* Get Condition type.
*
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Style/Style.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function __construct($isSupervisor = false, $isConditional = false)
// Initialise values
$this->font = new Font($isSupervisor, $isConditional);
$this->fill = new Fill($isSupervisor, $isConditional);
$this->borders = new Borders($isSupervisor);
$this->borders = new Borders($isSupervisor, $isConditional);
$this->alignment = new Alignment($isSupervisor, $isConditional);
$this->numberFormat = new NumberFormat($isSupervisor, $isConditional);
$this->protection = new Protection($isSupervisor, $isConditional);
Expand Down
1 change: 1 addition & 0 deletions src/PhpSpreadsheet/Writer/Xls/Style/CellBorder.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CellBorder
Border::BORDER_DASHDOTDOT => 0x0B,
Border::BORDER_MEDIUMDASHDOTDOT => 0x0C,
Border::BORDER_SLANTDASHDOT => 0x0D,
Border::BORDER_OMIT => 0x00,
];

public static function style(Border $border): int
Expand Down
16 changes: 6 additions & 10 deletions src/PhpSpreadsheet/Writer/Xls/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -2888,15 +2888,11 @@ private function writeCFRule(
$bFormatProt = 0;
}
// Border
$bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getColor()->getARGB() == Color::COLOR_BLACK
&& $conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
$bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getColor()->getARGB() == Color::COLOR_BLACK
&& $conditional->getStyle()->getBorders()->getRight()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
$bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getColor()->getARGB() == Color::COLOR_BLACK
&& $conditional->getStyle()->getBorders()->getTop()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
$bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getColor()->getARGB() == Color::COLOR_BLACK
&& $conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() == Border::BORDER_NONE ? 1 : 0);
if ($bBorderLeft == 0 || $bBorderRight == 0 || $bBorderTop == 0 || $bBorderBottom == 0) {
$bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
$bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
$bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
$bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0;
if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1) {
$bFormatBorder = 1;
} else {
$bFormatBorder = 0;
Expand All @@ -2905,7 +2901,7 @@ private function writeCFRule(
$bFillStyle = ($conditional->getStyle()->getFill()->getFillType() === null ? 0 : 1);
$bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() === null ? 0 : 1);
$bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() === null ? 0 : 1);
if ($bFillStyle == 0 || $bFillColor == 0 || $bFillColorBg == 0) {
if ($bFillStyle == 1 || $bFillColor == 1 || $bFillColorBg == 1) {
$bFormatFill = 1;
} else {
$bFormatFill = 0;
Expand Down
Loading