Skip to content

Commit

Permalink
feat-add-footer-row
Browse files Browse the repository at this point in the history
  • Loading branch information
EdLeckert authored and daringer committed Nov 27, 2024
1 parent a84eb63 commit 3fc34e8
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ resources:
* [Formatting with CSS](https://github.com/custom-cards/flex-table-card/tree/master/docs/example-cfg-css.md)
* [Auto Entities](https://github.com/custom-cards/flex-table-card/tree/master/docs/example-cfg-autoentities.md)
* [Loading from Services](https://github.com/custom-cards/flex-table-card/tree/master/docs/example-cfg-services.md)
* [Adding a Summary Footer](https://github.com/custom-cards/flex-table-card/tree/master/docs/example-cfg-footers.md)
* [Configuration Reference](https://github.com/custom-cards/flex-table-card/tree/master/docs/config-ref.md)
* [How to Contribute](https://github.com/custom-cards/flex-table-card/tree/master/docs/contribute.md)
9 changes: 7 additions & 2 deletions docs/config-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Flex Table gives you the possibility to visualize any tabular data within Lovela
| `max_rows` | int | optional | Restrict the number of (shown) rows to this maximum number
| `clickable` | boolean | optional | Activates the entities' on-click popup dialog
| `auto_format` | boolean | optional | Format state and attribute data using display precision and unit of measurement, if applicable (default: `false`)
| `display_footer` | boolean | optional | Display additional summary row at end for column totals, averages, etc. (default: `false`, see column options below)
| `css` | section | optional | Modify the CSS-style of this flex-table instance [(css example)](https://github.com/custom-cards/flex-table-card/blob/master/docs/example-cfg-css.md)
| `- ...` | item(s) | optional |
| `entities` | section | **required** | Section defining the entities, either as the *data sources* or for use by a service (see below). If no entities are required for a service, use [] and omit `include/exclude`
Expand Down Expand Up @@ -84,11 +85,15 @@ definition. Apart from `sort_by` no other option requires referencing of this id
| `multi_delimiter` | string | optional | defaults to ' ', concat multiple selector-data using this string
| `fmt` | string | optional | format using predefined 'formatters'
| `sort_unmodified` | boolean | optional | Sort using original value before `modify` option, if any, is applied (default: `false`)
| `footer_type` | string | optional | Used with `display_footer`, one of `sum`, `average`, `count`, `max`, `min`, or `text`
| `footer_text` | string | optional | Used with `display_footer`, text to be dispayed in this and optionally across several more columns (see `footer_colspan`)
| `footer_colspan` | string | optional | Used with `display_footer` and `footer_text`, displays text across specified number of columns
| `footer_modify` | string | optional* | Used with `display_footer`, performs same function as `modify` but for summary row only

<!--|&nbsp;&lt;content&gt; | | **required** | see in `column contents` below, one of those must exist! -->

*Use `modify` with _caution_ and on your own risk only. This will directly execute code using `eval()`, which is by definition a safety risk. Especially avoid processing any third party APIs / contents with `flex-table-card` using the `modify` parameter, *only apply this parameter, if you are 100% sure about the contents and origin of the data.*
Apart from that `modify` is very powerful, see [advanced cell formatting](https://github.com/custom-cards/flex-table-card/blob/master/docs/example-cfg-advanced-cell-formatting.md).
*Use `modify` and `footer_modify` with _caution_ and at your own risk only. This will directly execute code using `eval()`, which is by definition a safety risk. Especially avoid processing any third party APIs / contents with `flex-table-card` using the `modify` or `footer_modify` parameters, *only apply these parameters if you are 100% sure about the contents and origin of the data.*
Apart from that `modify` and `footer_modify` are very powerful, see [advanced cell formatting](https://github.com/custom-cards/flex-table-card/blob/master/docs/example-cfg-advanced-cell-formatting.md).


### Currently the available *formatters are:
Expand Down
126 changes: 126 additions & 0 deletions docs/example-cfg-footers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Examples - Summary Footer

## Adding a summary footer row to a table
<!-- [full text section] -->

To display a summary footer row below the data rows of a table, add this line to the `custom:flex-table-card` config:

<!-- [listing section] -->
``` yaml
display_footer: true
```
Next, use `footer_type` to identify the columns which will display summary information. A column can be summarized using any of the following values for `footer_type`:

* sum (total of numbers in column)
* average (average of numbers in column)
* count (count of rows)
* max (largest number in column)
* min (smallest number in column)
* text (custom text)

For example, to show totals for a column of numbers:

``` yaml
- name: Value
data: value
footer_type: sum
```

### Cells included in summary

Only cells with values that can be interpreted as numbers will be included in the summary when using `sum`, `average`, `max`, or `min`. (`count` always counts all rows.)
The number can have leading and trailing spaces, and any number of trailing non-numeric characters, but only the column `prefix` plus _one_ leading non-numeric character,
such as a currency sign. Otherwise it will be ignored.

### Formatting values

The formatting options `align`, `prefix`, and `suffix` (but not `modify`) will also apply to the summary row.
Use `footer_modify` to apply any special formatting to any `footer_type`. As summary values are not associated with a single entity or entity attribute,
`auto_format` cannot be used to format them.

A good use of `footer_modify` is to limit the number of decimal places shown by using the `toFixed` method:

``` yaml
footer_modify: x.toFixed(2)
```

### Using CSS

The default formatting of the row displays a solid line above and below the row:

``` yaml
"tfoot *": "border-style: solid none solid none;"
```

This can be overridden using the `css` option in the config. Available CSS selectors under `tfoot` are `tr`, `th`, and `td`. Also available are `ID` selectors of the form `'#tfootcol<n>'`,
where `<n>` is the column number, beginning with 0. Only non-hidden columns are counted.

### Displaying text

To display text in one or more columns, use `footer_type: text` and `footer_text: <the text>`. Use `footer_colspan` to span more than one column. For example:

``` yaml
- name: Country
data: countries
footer_type: text
footer_text: 'Total Value: '
footer_colspan: 2
```

Note that the text "Total Value" has no conceptual relationship to the columns it is under, a list of countries and whatever is in the next column in this example.
This is simply the first of two columns where the text will be displayed, with the expectation that the following column will contain a sum of values.

### Complete example

The following is a complete example card that demonstrates the use of footer options.

``` yaml
type: custom:flex-table-card
title: Bottles per Country
service: wine_cellar.get_countries
entities:
include: sensor.member_wine_inventory
sort_by: Country-
display_footer: true
columns:
- name: Country
data: countries
modify: x.Country
footer_type: text
footer_text: 'Grand Total Value: '
footer_colspan: 2
- name: Percentage
data: countries
modify: x.percent
align: right
suffix: '%'
- name: Total Value
data: countries
modify: (x.value_total + '.00')
align: right
prefix: '$'
footer_type: sum
footer_modify: x.toFixed(0)
- name: Average Value
data: countries
modify: x.value_avg
align: right
prefix: $
footer_type: text
footer_text: 'Total Bottles: '
- name: Bottles
data: countries
modify: x.count
align: right
footer_type: sum
css:
tfoot th: 'text-align: right;background-color: midnightblue;'
'#tfootcol4': 'background-color: red'
tfoot *: 'border-style: dotted none solid none;'
```

<!-- [example image section] -->
<img src="../images/FlexTableFooterExample.png" alt="Summary Footer Example" width="500px">


[Return to main README.md](../README.md)
125 changes: 120 additions & 5 deletions flex-table-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,8 +426,7 @@ class DataRow {
this.data = this.raw_data.map((raw, idx) => {
let x = raw;
let cfg = col_cfgs[idx];

let fmt = new CellFormatters();
let fmt = new CellFormatters();
if (cfg.fmt) {
x = fmt[cfg.fmt](x);
if (fmt.failed)
Expand Down Expand Up @@ -561,7 +560,8 @@ class FlexTableCard extends HTMLElement {
"text-decoration: underline; ",
"tbody tr:nth-child(odd)": "background-color: var(--table-row-background-color); ",
"tbody tr:nth-child(even)": "background-color: var(--table-row-alternative-background-color); ",
"th ha-icon": "height: 1em; vertical-align: top; "
"th ha-icon": "height: 1em; vertical-align: top; ",
"tfoot *": "border-style: solid none solid none;"
}
// apply CSS-styles from configuration
// ("+" suffix to key means "append" instead of replace)
Expand Down Expand Up @@ -590,7 +590,7 @@ class FlexTableCard extends HTMLElement {
}));


// table skeleton, body identified with: 'flextbl'
// table skeleton, body identified with: 'flextbl', footer with 'flexfoot'
content.innerHTML = `
<table>
<thead>
Expand All @@ -600,6 +600,7 @@ class FlexTableCard extends HTMLElement {
</tr>
</thead>
<tbody id='flextbl'></tbody>
<tfoot id='flexfoot'></tfoot>
</table>
`;
// push css-style & table as content into the card's DOM tree
Expand Down Expand Up @@ -658,6 +659,118 @@ class FlexTableCard extends HTMLElement {
});
}

_updateFooter(footer, config, rows) {
var innerHTML = '<tr>';
var colnum = -1;
var raw = "";
var colspan_remainder = 0

config.columns.map((col, idx) => {
if (!col.hidden) {
colnum++;
if (colspan_remainder > 0)
// Skip column if previous colspan would overlap it
colspan_remainder--;
else {
var cfg = config.columns[idx];
if (col.footer_type) {
switch (col.footer_type) {
case 'sum':
raw = this._sumColumn(rows, colnum);
break;
case 'average':
raw = this._avgColumn(rows, colnum);
break;
case 'count':
raw = rows.length;
break;
case 'max':
raw = this._maxColumn(rows, colnum);
break;
case 'min':
raw = this._minColumn(rows, colnum);
break;
case 'text':
raw = col.footer_text;
break;
default:
console.log("Invalid footer_type: ", col.footer_type);
}
let x = raw;
let value = cfg.footer_modify ? eval(cfg.footer_modify) : x;
if (col.footer_type == 'text') {
let colspan = cfg.footer_colspan ? cfg.footer_colspan : 1;
innerHTML += `<th id="tfootcol${colnum}" colspan=${colspan}>${value}</th>`;
colspan_remainder = colspan - 1;
}
else
innerHTML += `<td id="tfootcol${colnum}" class="${cfg.align || ""}">${cfg.prefix || ""}${value}${cfg.suffix || ""}</td>`;
}
else {
innerHTML += '<td></td>'
}
}
}
});

innerHTML += '</tr>';
footer.innerHTML = innerHTML;
}

_sumColumn(rows, colnum) {
var sum = 0;
for (var i = 0; i < rows.length; i++) {
let cellValue = this._findNumber(rows[i].data[colnum].sort_unmodified ? rows[i].data[colnum].raw_content : rows[i].data[colnum].content);
if (!Number.isNaN(cellValue)) sum += cellValue;
}
return sum;
}

_avgColumn(rows, colnum) {
var sum = 0;
var count = 0;
for (var i = 0; i < rows.length; i++) {
let cellValue = this._findNumber(rows[i].data[colnum].sort_unmodified ? rows[i].data[colnum].raw_content : rows[i].data[colnum].content);
if (!Number.isNaN(cellValue)) {
sum += cellValue;
count++;
}
}
return sum / count;
}

_maxColumn(rows, colnum) {
var max = Number.MIN_VALUE;
for (var i = 0; i < rows.length; i++) {
let cellValue = this._findNumber(rows[i].data[colnum].sort_unmodified ? rows[i].data[colnum].raw_content : rows[i].data[colnum].content);
if (!Number.isNaN(cellValue)) {
if (cellValue > max) max = cellValue;
}
}
return max == Number.MIN_VALUE ? Number.NaN : max;
}

_minColumn(rows, colnum) {
var min = Number.MAX_VALUE;
for (var i = 0; i < rows.length; i++) {
let cellValue = this._findNumber(rows[i].data[colnum].sort_unmodified ? rows[i].data[colnum].raw_content : rows[i].data[colnum].content);
if (!Number.isNaN(cellValue)) {
if (cellValue < min) min = cellValue;
}
}
return min == Number.MAX_VALUE ? Number.NaN : min;
}

// Trim whitespace and leading non-numeric, but not minus sign
_findNumber(val) {
if (typeof val === "number") {
return val;
}
else {
let value = val.trim();
return (Number.isNaN(parseFloat(value[0])) && value[0] !== '-') ? parseFloat(value.substring(1)) : parseFloat(value);
}
}
set hass(hass) {
const config = this._config;
const root = this.shadowRoot;
Expand Down Expand Up @@ -738,7 +851,9 @@ class FlexTableCard extends HTMLElement {
// finally set card height and insert card
this._setCardSize(this.tbl.rows.length);
// all preprocessing / rendering will be done here inside DataTable::get_rows()
this._updateContent(root.getElementById('flextbl'), this.tbl.get_rows());
let data_rows = this.tbl.get_rows();
this._updateContent(root.getElementById('flextbl'), data_rows);
if (config.display_footer) this._updateFooter(root.getElementById("flexfoot"), config, data_rows);
}

_setCardSize(num_rows) {
Expand Down
Binary file added images/FlexTableFooterExample.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3fc34e8

Please sign in to comment.