Skip to content
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

Feature: Add support for footer row #152

Merged
merged 1 commit into from
Nov 27, 2024
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
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
4 changes: 4 additions & 0 deletions docs/example-cfg-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ for the assigned value of `data`:
* `name` = *friendliest name* will be selected
* `object_id` = full entity *path* without domain
* `icon` = renders the entity's current icon (`entity.attributes.icon`) into the cell
* `device` = name of the device that the entity belongs to, if available
* `area` = name of the area that the entity or its device is assigned to, if available
* `_state`= is a *hack* to be able to select `entity.attributes.state` as data
* any `key in this.entity` (e.g., `entity_id`, `state`, ...)
* otherwise a key within `this.entity.attributes` will be assumed
Expand All @@ -28,6 +30,8 @@ for being an `Array.isArray()`).
each one using a comma `,`. If multiple selectors are used the resulting data is concatenated using
`multi_delimiter`, which defaults to a whitespace ' '.

Note that even if a `device` or an `area` is defined for an `entity`, it may not be available for `flex-table` to display.

### Migration from versions < 0.7
Since version 0.7 the old selectors (`attr`, `prop`, `attr_as_list`, `multi`) are all replaced by
`data`, which is a don't care & drop-in replacement for the old selectors. In particular `attr`,
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)
151 changes: 146 additions & 5 deletions flex-table-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ class DataRow {
// 'icon' will show the entity's default icon
let _icon = this.entity.attributes.icon;
raw_content.push(`<ha-icon id="icon" icon="${_icon}"></ha-icon>`);
} else if (col_key === "area") {
// 'area' will show the entity's or its device's assigned area, if any
raw_content.push(this._get_area_name(this.entity.entity_id, hass));
} else if (col_key === "device") {
// 'device' will show the entity's device name, if any
raw_content.push(this._get_device_name(this.entity.entity_id, hass));
} else if (col_key === "state" && config.auto_format && !col.no_auto_format) {
// format entity state
raw_content.push(hass.formatEntityState(this.entity));
Expand Down Expand Up @@ -394,14 +400,33 @@ class DataRow {
return null;
}

_get_device_name(entity_id, hass) {
var device_id;
if (hass.entities[entity_id] !== undefined) {
device_id = hass.entities[entity_id].device_id;
}
return device_id === undefined ? "-" : hass.devices[device_id].name_by_user || hass.devices[device_id].name;
}

_get_area_name(entity_id, hass) {
var area_id;
if (hass.entities[entity_id] !== undefined) {
area_id = hass.entities[entity_id].area_id;
if (area_id === undefined) {
let device_id = hass.entities[entity_id].device_id;
if (device_id !== undefined) area_id = hass.devices[device_id].area_id;
}
}
return area_id === undefined || hass.areas[area_id] === undefined ? "-" : hass.areas[area_id].name;
}

render_data(col_cfgs) {
// apply passed "modify" configuration setting by using eval()
// assuming the data is available inside the function as "x"
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 @@ -535,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 @@ -564,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 @@ -574,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 @@ -632,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 @@ -712,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.