Skip to content

Commit c6e6240

Browse files
committed
added possibility to configure threshold value
1 parent f8cdb67 commit c6e6240

File tree

4 files changed

+69
-37
lines changed

4 files changed

+69
-37
lines changed

.github/workflows/build-lua-script.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
needs: build
7676
runs-on: ubuntu-latest
7777
environment:
78-
name: sample-output
78+
name: github-pages
7979
url: ${{ steps.deployment.outputs.page_url }}
8080

8181
steps:

src/main.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import 'bootstrap/dist/css/bootstrap.min.css';
77

88
import Tree from "./Tree";
99
import './style.css';
10+
import {PointOptionsObject, SeriesSankeyNodesOptionsObject} from "highcharts/highcharts.src";
1011

1112
let categories: Map<number,string>;
1213
let numberOfMonths: number;
1314
let currency: string;
14-
let chart = null;
15-
let divider = 1;
16-
let chartData = null;
15+
let chart: Highcharts.Chart = null;
16+
let scaling_factor: number = 1;
17+
let chartData: Array<PointOptionsObject> = null;
1718
let excludedCategoryIds: number[] = [];
18-
const mainNodeId = 1;
19+
const mainNodeId: number = 1;
1920

2021
export { Tree }
2122

@@ -39,8 +40,14 @@ function setExcludedCategoriesFromSelect(): void {
3940
}
4041

4142
function updateChartData(chartDataTree: Tree): void {
42-
divider = (document.querySelector("form #isShowMonthlyValues") as HTMLInputElement).checked ? numberOfMonths : 1;
43-
console.debug('using divider ' + divider);
43+
console.debug('updating chart data..');
44+
45+
scaling_factor = (document.querySelector("form #isShowMonthlyValues") as HTMLInputElement).checked ? numberOfMonths : 1;
46+
console.debug('scaling: ' + scaling_factor);
47+
48+
let threshold = parseFloat((document.querySelector("form #threshold") as HTMLInputElement).value);
49+
threshold = isNaN(threshold) ? 0 : threshold;
50+
console.debug('threshold: ' + threshold);
4451

4552
// verify that excludedCategoryIds does not contain main node
4653
const index = excludedCategoryIds.indexOf(mainNodeId);
@@ -52,7 +59,7 @@ function updateChartData(chartDataTree: Tree): void {
5259
console.debug(excludedCategoryIds);
5360

5461
// recalculate weight values for each parent node
55-
[...chartDataTree.postOrderTraversal()].filter(x => x.children.length > 0).map(x => x.value = x.children.reduce(function (a: Tree, b: Tree) {
62+
[...chartDataTree.postOrderTraversal()].filter(x => x.children.length > 0).map(x => x.value = x.children.reduce((a: Tree, b: Tree) => {
5663
return excludedCategoryIds.includes(parseInt(b["key"])) ? a : a + b["value"];
5764
}, 0));
5865

@@ -63,42 +70,49 @@ function updateChartData(chartDataTree: Tree): void {
6370
// - using category ids instead of names because these might be the same for income and expense
6471
chartData = [...chartDataTree.preOrderTraversal()].filter(x => x.value >= 0 && x.parent && ! excludedCategoryIds.includes(x.key)).map(x => { return {from: String(x.key), to: String(x.parent.key), weight: x.value, custom: {real: x.value}}})
6572
.concat([...chartDataTree.preOrderTraversal()].filter(x => x.value < 0 && x.parent && ! excludedCategoryIds.includes(x.key)).map(x => { return {from: String(x.parent.key), to: String(x.key), weight: (-1)*x.value, outgoing: !x.hasChildren, custom: {real: x.value}}}));
73+
chartData = chartData.filter((x: PointOptionsObject) => Math.abs(x.weight) > threshold);
74+
6675
console.debug('chart data:');
6776
console.debug(chartData);
77+
6878
(chart.series[0] as Highcharts.Series).setData(chartData);
79+
80+
// add ids for testing
81+
chart.series[0].points.map(point => point.graphic.element).forEach((elem, i) => elem.setAttribute('data-testid', 'chart-node-' + i));
6982
}
7083

7184
function numberFormat(nb: number) {
72-
return '<strong>' + new Intl.NumberFormat(undefined, { style: 'currency', currency: currency }).format(nb/divider) + '</strong>';
85+
return '<strong>' + new Intl.NumberFormat(undefined, { style: 'currency', currency: currency }).format(nb/scaling_factor) + '</strong>';
7386
}
7487

7588
function numberFormatColored(nb: number) {
7689
let color = (nb >= 0) ? '#14c57e' : '#ff6b4a';
7790
return '<strong style="color:' + color + '">' + numberFormat(nb) + '</strong>';
7891
}
7992

80-
function buildChartNodesConfig() {
81-
let nodes = [];
93+
function buildChartNodesConfig(): Array<SeriesSankeyNodesOptionsObject> {
94+
let nodes: Array<SeriesSankeyNodesOptionsObject> = [];
8295
nodes.push({
8396
id: String(mainNodeId),
8497
name: categories.get(mainNodeId),
8598
colorIndex: 1,
86-
className: "main-node",
8799
dataLabels: {
88100
className: "main-node",
89-
nodeFormatter: function() {
90-
const incomingWeight = ('linksTo' in this.point ? (this.point as any).linksTo.map(point => point.weight) : []).reduce((pv, cv) => pv + cv, 0);
91-
const outgoingWeight = ('linksFrom' in this.point ? (this.point as any).linksFrom.map(point => point.weight) : []).reduce((pv, cv) => pv + cv, 0);
101+
nodeFormatter: function(): string {
102+
// @ts-ignore
103+
const point: any = this.point;
104+
const incomingWeight = ('linksTo' in point ? point.linksTo.map((point: PointOptionsObject) => point.weight) : []).reduce((pv, cv) => pv + cv, 0);
105+
const outgoingWeight = ('linksFrom' in point ? point.linksFrom.map(point => point.weight) : []).reduce((pv, cv) => pv + cv, 0);
92106

93-
return this.point.name + ': ' + numberFormatColored(incomingWeight - outgoingWeight);
107+
return point.name + ': ' + numberFormatColored(incomingWeight - outgoingWeight);
94108
}
95109
}
96110
});
97111

98112
new Map([...categories].filter(([categoryId, categoryPath]) => categoryId !== mainNodeId))
99113
.forEach(function(categoryPath, categoryId) {
100114
nodes.push({
101-
id: String(categoryId), // Highcarts need the id to be string
115+
id: String(categoryId), // Highcarts needs the id to be string
102116
name: categoryPath.split("]] .. CATEGORIES_PATH_SEPARATOR .. [[").pop() // remove first separator from path
103117
});
104118
});
@@ -131,7 +145,9 @@ export function createChart(chartDataTree: Tree, initCategories: Map<number,stri
131145
}
132146

133147
document.querySelector("#applySettingsButton").addEventListener('click', (event) => {
134-
event.preventDefault(); setExcludedCategoriesFromSelect(); updateChartData(chartDataTree)
148+
event.preventDefault();
149+
setExcludedCategoriesFromSelect();
150+
updateChartData(chartDataTree);
135151
});
136152

137153
/** @see https://www.highcharts.com/docs/chart-and-series-types/sankey-diagram */
@@ -182,7 +198,7 @@ export function createChart(chartDataTree: Tree, initCategories: Map<number,stri
182198
dataLabels: {
183199
align: 'right',
184200
padding: 30,
185-
nodeFormatter: function() {
201+
nodeFormatter: function(): string {
186202
const point = this as Highcharts.Point;
187203
const sum = 'getSum' in this ? (this as any).getSum() : 0;
188204
const percentage = 'linksTo' in point && point.linksTo[0] ? (sum / point.linksTo[0].fromNode.sum) * 100 : null;
@@ -192,12 +208,12 @@ export function createChart(chartDataTree: Tree, initCategories: Map<number,stri
192208
},
193209
tooltip: {
194210
// tooltip for link
195-
pointFormatter: function() {
211+
pointFormatter: function(): string {
196212
const point = this as any;
197213
return point.fromNode.name + " → " + point.toNode.name + ": " + numberFormat(point.weight) + "<br /><br /><span class='small'>(Klick entfernt die Kategorie aus dem Chart.)</span>";
198214
},
199215
// tooltip for node
200-
nodeFormatter: function() {
216+
nodeFormatter: function(): string {
201217
const point = this as Highcharts.Point;
202218

203219
let totalWeight = 0;
@@ -221,7 +237,7 @@ export function createChart(chartDataTree: Tree, initCategories: Map<number,stri
221237
return point.name + ': ' + (totalWeight != 0 ? numberFormatColored(totalWeight) : '') + '<br />' + weightsDetailTooltip;
222238
}
223239
},
224-
nodes: buildChartNodesConfig(),
240+
nodes: buildChartNodesConfig()
225241
}],
226242
chart: {
227243
height: 700,

src/template.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ <h5 class="offcanvas-title" id="offcanvasConfigLabel">Konfiguration</h5>
6161
</div>
6262
<div class="offcanvas-body">
6363
<p>
64-
Das <a class="icon-link" href="https://de.wikipedia.org/wiki/Sankey-Diagramm" target="_blank" rel="external noopener">Sankey Chart<svg class="bi" aria-hidden="true"><use xlink:href="#arrow-up-right"></use></svg></a>
65-
zeigt die aus <a class="icon-link" href="https://moneymoney-app.com/" target="_blank" rel="external noopener">MoneyMoney<svg class="bi" aria-hidden="true"><use xlink:href="#arrow-up-right"></use></svg></a> exportierten Cashflows des
66-
Kontos <strong>{{ account_name }} ({{ account_number }})</strong> für den Zeitraum <strong>{{ start_date }} bis {{ end_date }}</strong>.
67-
</p>
68-
<p>
69-
Es wurde aus insgesamt <strong id="transactionCount">{{ transaction_count }} Transaktionen</strong> generiert.
64+
<small>
65+
Das <a class="icon-link" href="https://de.wikipedia.org/wiki/Sankey-Diagramm" target="_blank" rel="external noopener">Sankey Chart<svg class="bi" aria-hidden="true"><use xlink:href="#arrow-up-right"></use></svg></a>
66+
zeigt die aus <a class="icon-link" href="https://moneymoney-app.com/" target="_blank" rel="external noopener">MoneyMoney<svg class="bi" aria-hidden="true"><use xlink:href="#arrow-up-right"></use></svg></a> exportierten Cashflows des
67+
Kontos <strong>{{ account_name }} ({{ account_number }})</strong> für den Zeitraum <strong>{{ start_date }} bis {{ end_date }}</strong>.<br>
68+
Es wurde aus insgesamt <strong id="transactionCount">{{ transaction_count }} Transaktionen</strong> generiert.
69+
</small>
7070
</p>
7171
<form>
7272
<div class="row g-3">
@@ -75,6 +75,7 @@ <h5 class="offcanvas-title" id="offcanvasConfigLabel">Konfiguration</h5>
7575
<select id="categories" class="form-select" multiple aria-label="multiple select">
7676
</select>
7777
</div>
78+
7879
<div class="col-12">
7980
<div class="form-check">
8081
<input class="form-check-input" type="checkbox" id="isShowMonthlyValues">
@@ -83,6 +84,14 @@ <h5 class="offcanvas-title" id="offcanvasConfigLabel">Konfiguration</h5>
8384
</div>
8485
</div>
8586

87+
<div class="col-12">
88+
<div class="mb-3">
89+
<label for="threshold" class="form-label">Schwellwert ({{ currency }})</label>
90+
<input type="number" class="form-control" id="threshold" placeholder="100" min="0">
91+
<div class="form-text">Es werden nur Pfade angezeigt, die größer als der konfigurierte Wert sind.</div>
92+
</div>
93+
</div>
94+
8695
<div class="col-12">
8796
<button id="applySettingsButton" type="submit" class="btn btn-primary" data-bs-dismiss="offcanvas">Anwenden</button>
8897
</div>

tests/chart.spec.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@ test('has valid Saldo', async ({ page }) => {
1515
});
1616

1717
test('has correct metadata', async ({ page }) => {
18-
await page.locator('#chart-container').screenshot({ path: 'tmp/sample.png' }); // take a screenshot for readme file
18+
await page.locator('#chart-container').screenshot({ path: 'tmp/sample.png' }); // take a screenshot for README file
1919

2020
await expect(page.locator('#transactionCount')).toHaveText('7 Transaktionen');
2121
});
2222

23-
test('can view options', async ({ page }) => {
24-
const config = page.locator('#offcanvasConfig');
25-
const select = page.getByRole('button', { name: 'Chart anpassen' });
23+
test('threshold can be configured', async ({ page }) => {
24+
const configMenu = page.locator('#offcanvasConfig');
25+
const button = page.getByRole('button', { name: 'Chart anpassen' });
2626

27-
await expect(config).toBeHidden();
28-
await expect(select).toBeVisible();
29-
await expect(select).toBeEnabled();
27+
await expect(configMenu).toBeHidden();
28+
await expect(button).toBeVisible();
29+
await expect(button).toBeEnabled();
3030

31-
await select.click();
32-
await expect(config).toBeVisible();
31+
await button.click();
32+
await expect(configMenu).toBeVisible();
33+
34+
const node = page.getByTestId('chart-node-3');
35+
await expect(node).toBeVisible();
36+
const thresholdInput = page.locator('form #threshold');
37+
await thresholdInput.fill('140');
38+
await page.getByRole('button', { name: 'Anwenden' }).click();
39+
await expect(node).toBeHidden();
3340
});

0 commit comments

Comments
 (0)