mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Expand layout
This commit is contained in:
parent
9bb62c865a
commit
dffddfda18
9
package-lock.json
generated
9
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"alpinejs": "^3.12.3",
|
"alpinejs": "^3.12.3",
|
||||||
"bootstrap": "^5.3.0",
|
"bootstrap": "^5.3.0",
|
||||||
"chart.js": "^4.3.3",
|
"chart.js": "^4.3.3",
|
||||||
|
"chartjs-chart-sankey": "^0.12.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"store": "^2.0.12"
|
"store": "^2.0.12"
|
||||||
},
|
},
|
||||||
@ -507,6 +508,14 @@
|
|||||||
"pnpm": ">=7"
|
"pnpm": ">=7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chartjs-chart-sankey": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-chart-sankey/-/chartjs-chart-sankey-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-2f0YfDWNTTDqztVALlD2YMdSbpmjxdxHpcpKgBi9cUq3IPWBvHb58h4gIa7GjsYVjMLwX6gusDXgxlh9PMKkkA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"alpinejs": "^3.12.3",
|
"alpinejs": "^3.12.3",
|
||||||
"bootstrap": "^5.3.0",
|
"bootstrap": "^5.3.0",
|
||||||
"chart.js": "^4.3.3",
|
"chart.js": "^4.3.3",
|
||||||
|
"chartjs-chart-sankey": "^0.12.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"store": "^2.0.12"
|
"store": "^2.0.12"
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {api} from "../../../boot/axios";
|
import {api} from "../../../boot/axios";
|
||||||
|
import format from "date-fns/format";
|
||||||
|
|
||||||
export default class Get {
|
export default class Get {
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ export default class Get {
|
|||||||
* @returns {Promise<AxiosResponse<any>>}
|
* @returns {Promise<AxiosResponse<any>>}
|
||||||
*/
|
*/
|
||||||
get(identifier, date) {
|
get(identifier, date) {
|
||||||
let params = {date: date};
|
let params = {date: format(date, 'y-MM-d').slice(0, 10)};
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return api.get('/api/v1/accounts/' + identifier);
|
return api.get('/api/v1/accounts/' + identifier);
|
||||||
}
|
}
|
||||||
|
34
resources/assets/v2/api/v2/model/transaction/get.js
Normal file
34
resources/assets/v2/api/v2/model/transaction/get.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* get.js
|
||||||
|
* Copyright (c) 2023 james@firefly-iii.org
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {api} from "../../../../boot/axios";
|
||||||
|
|
||||||
|
export default class Get {
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @returns {Promise<AxiosResponse<any>>}
|
||||||
|
*/
|
||||||
|
get(params) {
|
||||||
|
return api.get('/api/v1/transactions', {params: params});
|
||||||
|
}
|
||||||
|
}
|
@ -24,12 +24,13 @@ import boxes from './pages/dashboard/boxes.js';
|
|||||||
import accounts from './pages/dashboard/accounts.js';
|
import accounts from './pages/dashboard/accounts.js';
|
||||||
import budgets from './pages/dashboard/budgets.js';
|
import budgets from './pages/dashboard/budgets.js';
|
||||||
import categories from './pages/dashboard/categories.js';
|
import categories from './pages/dashboard/categories.js';
|
||||||
|
import sankey from './pages/dashboard/sankey.js';
|
||||||
|
|
||||||
const comps = {dates, boxes, accounts, budgets, categories};
|
const comps = {dates, boxes, accounts, budgets, categories, sankey};
|
||||||
|
|
||||||
function loadPage(comps) {
|
function loadPage(comps) {
|
||||||
Object.keys(comps).forEach(comp => {
|
Object.keys(comps).forEach(comp => {
|
||||||
console.log(`Loading ${comp}`);
|
console.log(`Loading page component "${comp}"`);
|
||||||
let data = comps[comp]();
|
let data = comps[comp]();
|
||||||
Alpine.data(comp, () => data);
|
Alpine.data(comp, () => data);
|
||||||
});
|
});
|
||||||
|
@ -22,129 +22,97 @@
|
|||||||
import {getVariable} from "../../store/get-variable.js";
|
import {getVariable} from "../../store/get-variable.js";
|
||||||
import {setVariable} from "../../store/set-variable.js";
|
import {setVariable} from "../../store/set-variable.js";
|
||||||
import Dashboard from "../../api/v2/chart/account/dashboard.js";
|
import Dashboard from "../../api/v2/chart/account/dashboard.js";
|
||||||
import formatLocal from "../../util/format.js";
|
|
||||||
import {format} from "date-fns";
|
|
||||||
import formatMoney from "../../util/format-money.js";
|
import formatMoney from "../../util/format-money.js";
|
||||||
import Get from "../../api/v1/accounts/get.js";
|
import Get from "../../api/v1/accounts/get.js";
|
||||||
|
import Chart from "chart.js/auto";
|
||||||
|
|
||||||
// this is very ugly, but I have no better ideas at the moment to save the currency info
|
// this is very ugly, but I have no better ideas at the moment to save the currency info
|
||||||
// for each series.
|
// for each series.
|
||||||
window.currencies = [];
|
let currencies = [];
|
||||||
|
let chart = null;
|
||||||
|
let chartData = null;
|
||||||
|
|
||||||
export default () => ({
|
export default () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
loadingAccounts: false,
|
loadingAccounts: false,
|
||||||
accountList: [],
|
accountList: [],
|
||||||
autoConversion: false,
|
autoConversion: false,
|
||||||
chart: null,
|
|
||||||
chartData: null,
|
|
||||||
chartOptions: null,
|
chartOptions: null,
|
||||||
switchAutoConversion() {
|
switchAutoConversion() {
|
||||||
this.autoConversion = !this.autoConversion;
|
this.autoConversion = !this.autoConversion;
|
||||||
setVariable('autoConversion', this.autoConversion);
|
setVariable('autoConversion', this.autoConversion);
|
||||||
this.loadChart();
|
|
||||||
},
|
},
|
||||||
getFreshData() {
|
getFreshData() {
|
||||||
const dashboard = new Dashboard();
|
const dashboard = new Dashboard();
|
||||||
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
||||||
this.chartData = response.data;
|
this.chartData = response.data;
|
||||||
this.generateOptions(this.chartData);
|
this.drawChart(this.generateOptions(this.chartData));
|
||||||
this.drawChart();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
generateOptions(data) {
|
generateOptions(data) {
|
||||||
window.currencies = [];
|
currencies = [];
|
||||||
let options = {
|
let options = {
|
||||||
legend: {show: false},
|
type: 'line',
|
||||||
chart: {
|
data: {
|
||||||
height: 400,
|
labels: [],
|
||||||
type: 'line'
|
datasets: []
|
||||||
},
|
|
||||||
series: [],
|
|
||||||
settings: [],
|
|
||||||
xaxis: {
|
|
||||||
categories: [],
|
|
||||||
labels: {
|
|
||||||
formatter: function (value) {
|
|
||||||
if (undefined === value) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const date = new Date(value);
|
|
||||||
if (date instanceof Date && !isNaN(date)) {
|
|
||||||
return formatLocal(date, 'PP');
|
|
||||||
}
|
|
||||||
console.error('Could not parse "' + value + '", return "".');
|
|
||||||
return ':(';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value, index) {
|
|
||||||
if (undefined === value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (undefined === index) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof index === 'object') {
|
|
||||||
index = index.seriesIndex;
|
|
||||||
}
|
|
||||||
//console.log(index);
|
|
||||||
let currencyCode = window.currencies[index] ?? 'EUR';
|
|
||||||
return formatMoney(value, currencyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// render data:
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
if (data.hasOwnProperty(i)) {
|
if (data.hasOwnProperty(i)) {
|
||||||
let current = data[i];
|
let current = data[i];
|
||||||
let entry = [];
|
let dataset = {};
|
||||||
let collection = [];
|
let collection = [];
|
||||||
|
|
||||||
|
// if index = 0, push all keys as labels:
|
||||||
|
if (0 === i) {
|
||||||
|
options.data.labels = Object.keys(current.entries);
|
||||||
|
}
|
||||||
|
dataset.label = current.label;
|
||||||
|
|
||||||
// use the "native" currency code and use the "native_entries" as array
|
// use the "native" currency code and use the "native_entries" as array
|
||||||
if (this.autoConversion) {
|
if (this.autoConversion) {
|
||||||
window.currencies.push(current.native_code);
|
currencies.push(current.native_code);
|
||||||
collection = current.native_entries;
|
collection = Object.values(current.native_entries);
|
||||||
}
|
}
|
||||||
if (!this.autoConversion) {
|
if (!this.autoConversion) {
|
||||||
window.currencies.push(current.currency_code);
|
currencies.push(current.currency_code);
|
||||||
collection = current.entries;
|
collection = Object.values(current.entries);
|
||||||
}
|
}
|
||||||
|
dataset.data = collection;
|
||||||
|
|
||||||
for (const [ii, value] of Object.entries(collection)) {
|
for (const [ii, value] of Object.entries(collection)) {
|
||||||
entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
|
//entry.push({x: format(new Date(ii), 'yyyy-MM-dd'), y: parseFloat(value)});
|
||||||
}
|
}
|
||||||
options.series.push({name: current.label, data: entry});
|
options.data.datasets.push(dataset);
|
||||||
|
//options.series.push({name: current.label, data: entry});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.chartOptions = options;
|
|
||||||
|
return options;
|
||||||
},
|
},
|
||||||
loadChart() {
|
loadChart() {
|
||||||
if (true === this.loading) {
|
if (true === this.loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (null === this.chartData) {
|
if (null === chartData) {
|
||||||
this.getFreshData();
|
this.getFreshData();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (null !== this.chartData) {
|
this.drawChart(this.generateOptions(chartData));
|
||||||
this.generateOptions(this.chartData);
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
||||||
},
|
},
|
||||||
drawChart() {
|
drawChart(options) {
|
||||||
if (null !== this.chart) {
|
if (null !== chart) {
|
||||||
// chart already in place, refresh:
|
// chart already in place, refresh:
|
||||||
this.chart.updateOptions(this.chartOptions);
|
chart.data.datasets = options.data.datasets;
|
||||||
}
|
chart.update();
|
||||||
if (null === this.chart) {
|
return;
|
||||||
//this.chart = new ApexCharts(document.querySelector("#account-chart"), this.chartOptions);
|
|
||||||
//this.chart.render();
|
|
||||||
}
|
}
|
||||||
|
chart = new Chart(document.querySelector("#account-chart"), options);
|
||||||
},
|
},
|
||||||
loadAccounts() {
|
loadAccounts() {
|
||||||
if (true === this.loadingAccounts) {
|
if (true === this.loadingAccounts) {
|
||||||
@ -211,13 +179,12 @@ export default () => ({
|
|||||||
this.autoConversion = values[1];
|
this.autoConversion = values[1];
|
||||||
// main dashboard chart:
|
// main dashboard chart:
|
||||||
this.loadChart();
|
this.loadChart();
|
||||||
// this.loadAccounts();
|
this.loadAccounts();
|
||||||
});
|
});
|
||||||
window.store.observe('end', () => {
|
window.store.observe('end', () => {
|
||||||
this.chartData = null;
|
chartData = null;
|
||||||
this.expenseAccountChart = null;
|
|
||||||
// main dashboard chart:
|
// main dashboard chart:
|
||||||
// this.loadChart();
|
this.loadChart();
|
||||||
this.loadAccounts();
|
this.loadAccounts();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -22,60 +22,51 @@ import Dashboard from "../../api/v2/chart/budget/dashboard.js";
|
|||||||
// todo optimize
|
// todo optimize
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from 'chart.js/auto';
|
||||||
import {getDefaultChartSettings} from "../../support/default-chart-settings.js";
|
import {getDefaultChartSettings} from "../../support/default-chart-settings.js";
|
||||||
|
import formatMoney from "../../util/format-money.js";
|
||||||
|
|
||||||
|
let currencies = [];
|
||||||
|
let chart = null;
|
||||||
|
let chartData = null;
|
||||||
|
|
||||||
window.budgetCurrencies = [];
|
|
||||||
export default () => ({
|
export default () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
chart: null,
|
|
||||||
autoConversion: false,
|
autoConversion: false,
|
||||||
chartData: null,
|
|
||||||
chartOptions: null,
|
|
||||||
loadChart() {
|
loadChart() {
|
||||||
if (true === this.loading) {
|
if (true === this.loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (null === this.chartData) {
|
|
||||||
this.getFreshData();
|
if (null !== chartData) {
|
||||||
}
|
this.drawChart(this.generateOptions(chartData));
|
||||||
if (null !== this.chartData) {
|
|
||||||
this.generateOptions(this.chartData);
|
|
||||||
this.drawChart();
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.getFreshData();
|
||||||
},
|
},
|
||||||
drawChart() {
|
drawChart(options) {
|
||||||
if (null !== this.chart) {
|
if (null !== chart) {
|
||||||
// chart already in place, refresh:
|
chart.data.datasets = options.data.datasets;
|
||||||
console.log('refresh');
|
chart.update();
|
||||||
this.chart.data = this.chartOptions.data;
|
return;
|
||||||
//this.chart.updateOptions(this.chartOptions);
|
|
||||||
}
|
|
||||||
if (null === this.chart) {
|
|
||||||
//this.chart = new ApexCharts(document.querySelector("#budget-chart"), this.chartOptions);
|
|
||||||
this.chart = new Chart(document.querySelector("#budget-chart"), this.chartOptions);
|
|
||||||
//this.chart.render();
|
|
||||||
}
|
}
|
||||||
|
chart = new Chart(document.querySelector("#budget-chart"), options);
|
||||||
},
|
},
|
||||||
getFreshData() {
|
getFreshData() {
|
||||||
const dashboard = new Dashboard();
|
const dashboard = new Dashboard();
|
||||||
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
||||||
this.chartData = response.data;
|
chartData = response.data; // save chart data for later.
|
||||||
this.generateOptions(this.chartData);
|
this.drawChart(this.generateOptions(response.data));
|
||||||
this.drawChart();
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
generateOptions(data) {
|
generateOptions(data) {
|
||||||
window.budgetCurrencies = [];
|
currencies = [];
|
||||||
let options = getDefaultChartSettings('column');
|
let options = getDefaultChartSettings('column');
|
||||||
options.options.locale = window.store.get('locale').replace('_', '-');
|
options.options.locale = window.store.get('locale').replace('_', '-');
|
||||||
options.options.plugins = {
|
options.options.plugins = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
// tooltip: function (context) {
|
|
||||||
// //console.log(context);
|
|
||||||
// },
|
|
||||||
title: function (context) {
|
title: function (context) {
|
||||||
return context.label;
|
return context.label;
|
||||||
},
|
},
|
||||||
@ -85,12 +76,7 @@ export default () => ({
|
|||||||
if (label) {
|
if (label) {
|
||||||
label += ': ';
|
label += ': ';
|
||||||
}
|
}
|
||||||
//console.log('label');
|
return label + ' ' + formatMoney(context.parsed.y, currencies[context.parsed.x] ?? 'EUR');
|
||||||
//console.log(context.label + ' X');
|
|
||||||
//return context.label + ' X';
|
|
||||||
// console.log(context);
|
|
||||||
return label + ' ' + context.parsed.x;
|
|
||||||
// return label + ' ' + formatMoney(context.parsed.y, window.budgetCurrencies[context.parsed.x] ?? 'EUR');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +112,7 @@ export default () => ({
|
|||||||
let label = current.label + ' (' + current.currency_code + ')';
|
let label = current.label + ' (' + current.currency_code + ')';
|
||||||
options.data.labels.push(label);
|
options.data.labels.push(label);
|
||||||
if (this.autoConversion) {
|
if (this.autoConversion) {
|
||||||
window.budgetCurrencies.push(current.native_code);
|
currencies.push(current.native_code);
|
||||||
// series 0: spent
|
// series 0: spent
|
||||||
options.data.datasets[0].data.push(parseFloat(current.native_entries.spent) * -1);
|
options.data.datasets[0].data.push(parseFloat(current.native_entries.spent) * -1);
|
||||||
// series 1: left
|
// series 1: left
|
||||||
@ -135,7 +121,7 @@ export default () => ({
|
|||||||
options.data.datasets[2].data.push(parseFloat(current.native_entries.overspent));
|
options.data.datasets[2].data.push(parseFloat(current.native_entries.overspent));
|
||||||
}
|
}
|
||||||
if (!this.autoConversion) {
|
if (!this.autoConversion) {
|
||||||
window.budgetCurrencies.push(current.currency_code);
|
currencies.push(current.currency_code);
|
||||||
// series 0: spent
|
// series 0: spent
|
||||||
options.data.datasets[0].data.push(parseFloat(current.entries.spent) * -1);
|
options.data.datasets[0].data.push(parseFloat(current.entries.spent) * -1);
|
||||||
// series 1: left
|
// series 1: left
|
||||||
@ -143,147 +129,30 @@ export default () => ({
|
|||||||
// series 2: overspent
|
// series 2: overspent
|
||||||
options.data.datasets[2].data.push(parseFloat(current.entries.overspent));
|
options.data.datasets[2].data.push(parseFloat(current.entries.overspent));
|
||||||
}
|
}
|
||||||
// console.log('Currencies');
|
|
||||||
// console.log(window.budgetCurrencies);
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return options;
|
||||||
// options = {
|
|
||||||
// legend: {show: false},
|
|
||||||
// series: [{
|
|
||||||
// name: 'Spent',
|
|
||||||
// data: []
|
|
||||||
// }, {
|
|
||||||
// name: 'Left',
|
|
||||||
// data: []
|
|
||||||
// }, {
|
|
||||||
// name: 'Overspent',
|
|
||||||
// data: []
|
|
||||||
// }],
|
|
||||||
// chart: {
|
|
||||||
// type: 'bar',
|
|
||||||
// height: 400,
|
|
||||||
// stacked: true,
|
|
||||||
// toolbar: {tools: {zoom: false, download: false, pan: false}},
|
|
||||||
// zoom: {
|
|
||||||
// enabled: true
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// responsive: [{
|
|
||||||
// breakpoint: 480,
|
|
||||||
// options: {
|
|
||||||
// legend: {
|
|
||||||
// position: 'bottom',
|
|
||||||
// offsetX: -10,
|
|
||||||
// offsetY: 0
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }],
|
|
||||||
// plotOptions: {
|
|
||||||
// bar: {
|
|
||||||
// horizontal: false,
|
|
||||||
// borderRadius: 10,
|
|
||||||
// dataLabels: {
|
|
||||||
// total: {
|
|
||||||
// enabled: true,
|
|
||||||
// // style: {
|
|
||||||
// // fontSize: '13px',
|
|
||||||
// // fontWeight: 900
|
|
||||||
// // },
|
|
||||||
// formatter: function (val, opt) {
|
|
||||||
// let index = 0;
|
|
||||||
// if (typeof opt === 'object') {
|
|
||||||
// index = opt.dataPointIndex; // this is the "category name + currency" index
|
|
||||||
// }
|
|
||||||
// let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
|
|
||||||
// return formatMoney(val, currencyCode);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// yaxis: {
|
|
||||||
// labels: {
|
|
||||||
// formatter: function (value, index) {
|
|
||||||
// if (undefined === value) {
|
|
||||||
// return value;
|
|
||||||
// }
|
|
||||||
// if (undefined === index) {
|
|
||||||
// return value;
|
|
||||||
// }
|
|
||||||
// if (typeof index === 'object') {
|
|
||||||
// index = index.dataPointIndex; // this is the "category name + currency" index
|
|
||||||
// }
|
|
||||||
// let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
|
|
||||||
// return formatMoney(value, currencyCode);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// xaxis: {
|
|
||||||
// categories: []
|
|
||||||
// },
|
|
||||||
// fill: {
|
|
||||||
// opacity: 0.8
|
|
||||||
// },
|
|
||||||
// dataLabels: {
|
|
||||||
// formatter: function (val, opt) {
|
|
||||||
// let index = 0;
|
|
||||||
// if (typeof opt === 'object') {
|
|
||||||
// index = opt.dataPointIndex; // this is the "category name + currency" index
|
|
||||||
// }
|
|
||||||
// let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
|
|
||||||
// return formatMoney(val, currencyCode);
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
// for (const i in data) {
|
|
||||||
// if (data.hasOwnProperty(i)) {
|
|
||||||
// let current = data[i];
|
|
||||||
// // convert to EUR yes no?
|
|
||||||
// let label = current.label + ' (' + current.currency_code + ')';
|
|
||||||
// options.xaxis.categories.push(label);
|
|
||||||
// if (this.autoConversion) {
|
|
||||||
// window.budgetCurrencies.push(current.native_code);
|
|
||||||
//
|
|
||||||
// // series 0: spent
|
|
||||||
// options.series[0].data.push(parseFloat(current.native_entries.spent) * -1);
|
|
||||||
// // series 1: left
|
|
||||||
// options.series[1].data.push(parseFloat(current.native_entries.left));
|
|
||||||
// // series 2: overspent
|
|
||||||
// options.series[2].data.push(parseFloat(current.native_entries.overspent));
|
|
||||||
// }
|
|
||||||
// if (!this.autoConversion) {
|
|
||||||
// window.budgetCurrencies.push(current.currency_code);
|
|
||||||
// // series 0: spent
|
|
||||||
// options.series[0].data.push(parseFloat(current.entries.spent) * -1);
|
|
||||||
// // series 1: left
|
|
||||||
// options.series[1].data.push(parseFloat(current.entries.left));
|
|
||||||
// // series 2: overspent
|
|
||||||
// options.series[2].data.push(parseFloat(current.entries.overspent));
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
this.chartOptions = options;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
Promise.all([getVariable('autoConversion', false),]).then((values) => {
|
Promise.all([getVariable('autoConversion', false),]).then((values) => {
|
||||||
this.autoConversion = values[0];
|
this.autoConversion = values[0];
|
||||||
this.loadChart();
|
if (false === this.loading) {
|
||||||
|
this.loadChart();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// todo the charts don't need to reload from server if the autoConversion value changes.
|
|
||||||
window.store.observe('end', () => {
|
window.store.observe('end', () => {
|
||||||
this.chartData = null;
|
if (false === this.loading) {
|
||||||
this.loadChart();
|
this.chartData = null;
|
||||||
|
this.loadChart();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.store.observe('autoConversion', (newValue) => {
|
window.store.observe('autoConversion', (newValue) => {
|
||||||
this.autoConversion = newValue;
|
this.autoConversion = newValue;
|
||||||
this.loadChart();
|
if (false === this.loading) {
|
||||||
|
this.loadChart();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -21,79 +21,22 @@ import {getVariable} from "../../store/get-variable.js";
|
|||||||
import Dashboard from "../../api/v2/chart/category/dashboard.js";
|
import Dashboard from "../../api/v2/chart/category/dashboard.js";
|
||||||
//import ApexCharts from "apexcharts";
|
//import ApexCharts from "apexcharts";
|
||||||
import formatMoney from "../../util/format-money.js";
|
import formatMoney from "../../util/format-money.js";
|
||||||
|
import {getDefaultChartSettings} from "../../support/default-chart-settings.js";
|
||||||
|
import Chart from "chart.js/auto";
|
||||||
|
|
||||||
|
let currencies = [];
|
||||||
|
|
||||||
|
let chart = null;
|
||||||
|
let chartData = null;
|
||||||
|
|
||||||
window.categoryCurrencies = [];
|
|
||||||
export default () => ({
|
export default () => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
chart: null,
|
|
||||||
autoConversion: false,
|
autoConversion: false,
|
||||||
chartData: null,
|
|
||||||
chartOptions: null,
|
|
||||||
generateOptions(data) {
|
generateOptions(data) {
|
||||||
window.categoryCurrencies = [];
|
currencies = [];
|
||||||
let options = {
|
let options = getDefaultChartSettings('column');
|
||||||
series: [],
|
|
||||||
chart: {
|
// first, create "series" per currency.
|
||||||
type: 'bar',
|
|
||||||
height: 350
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
horizontal: false,
|
|
||||||
columnWidth: '55%',
|
|
||||||
endingShape: 'rounded'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
stroke: {
|
|
||||||
show: true,
|
|
||||||
width: 2,
|
|
||||||
colors: ['transparent']
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories: [],
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value, index) {
|
|
||||||
if (undefined === value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (undefined === index) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof index === 'object') {
|
|
||||||
index = index.dataPointIndex; // this is the "category name + currency" index
|
|
||||||
}
|
|
||||||
let currencyCode = window.budgetCurrencies[index] ?? 'EUR';
|
|
||||||
return formatMoney(value, currencyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fill: {
|
|
||||||
opacity: 1
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
y: {
|
|
||||||
formatter: function (value, index) {
|
|
||||||
if (undefined === value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (undefined === index) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof index === 'object') {
|
|
||||||
index = index.seriesIndex; // this is the currency index.
|
|
||||||
}
|
|
||||||
let currencyCode = window.categoryCurrencies[index] ?? 'EUR';
|
|
||||||
return formatMoney(value, currencyCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// first, collect all currencies and use them as series.
|
|
||||||
let series = {};
|
let series = {};
|
||||||
for (const i in data) {
|
for (const i in data) {
|
||||||
if (data.hasOwnProperty(i)) {
|
if (data.hasOwnProperty(i)) {
|
||||||
@ -109,11 +52,12 @@ export default () => ({
|
|||||||
name: code,
|
name: code,
|
||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
window.categoryCurrencies.push(code);
|
currencies.push(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// loop data again to add amounts.
|
|
||||||
|
// loop data again to add amounts to each series.
|
||||||
for (const i in data) {
|
for (const i in data) {
|
||||||
if (data.hasOwnProperty(i)) {
|
if (data.hasOwnProperty(i)) {
|
||||||
let current = data[i];
|
let current = data[i];
|
||||||
@ -147,42 +91,40 @@ export default () => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// add label to x-axis, not unimportant.
|
// add label to x-axis, not unimportant.
|
||||||
if (!options.xaxis.categories.includes(current.label)) {
|
if (!options.data.labels.includes(current.label)) {
|
||||||
options.xaxis.categories.push(current.label);
|
options.data.labels.push(current.label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// loop the series and create Apex-compatible data sets.
|
// loop the series and create ChartJS-compatible data sets.
|
||||||
for (const i in series) {
|
for (const i in series) {
|
||||||
let current = {
|
let dataset = {
|
||||||
name: i,
|
label: i,
|
||||||
data: [],
|
data: [],
|
||||||
}
|
}
|
||||||
for (const ii in series[i].data) {
|
for (const ii in series[i].data) {
|
||||||
current.data.push(series[i].data[ii]);
|
dataset.data.push(series[i].data[ii]);
|
||||||
}
|
}
|
||||||
options.series.push(current);
|
options.data.datasets.push(dataset);
|
||||||
}
|
}
|
||||||
this.chartOptions = options;
|
|
||||||
|
return options;
|
||||||
},
|
},
|
||||||
drawChart() {
|
drawChart(options) {
|
||||||
if (null !== this.chart) {
|
if (null !== chart) {
|
||||||
// chart already in place, refresh:
|
chart.data.datasets = options.data.datasets;
|
||||||
this.chart.updateOptions(this.chartOptions);
|
chart.update();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (null === this.chart) {
|
chart = new Chart(document.querySelector("#category-chart"), options);
|
||||||
this.chart = new ApexCharts(document.querySelector("#category-chart"), this.chartOptions);
|
|
||||||
this.chart.render();
|
|
||||||
}
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
},
|
},
|
||||||
getFreshData() {
|
getFreshData() {
|
||||||
const dashboard = new Dashboard();
|
const dashboard = new Dashboard();
|
||||||
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
dashboard.dashboard(new Date(window.store.get('start')), new Date(window.store.get('end')), null).then((response) => {
|
||||||
this.chartData = response.data;
|
chartData = response.data; // save chart data for later.
|
||||||
this.generateOptions(this.chartData);
|
this.drawChart(this.generateOptions(response.data));
|
||||||
this.drawChart();
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -191,28 +133,26 @@ export default () => ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
if (null === this.chartData) {
|
|
||||||
this.getFreshData();
|
|
||||||
}
|
|
||||||
if (null !== this.chartData) {
|
|
||||||
this.generateOptions(this.chartData);
|
|
||||||
this.drawChart();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
if (null !== chartData) {
|
||||||
|
this.drawChart(this.generateOptions(chartData));
|
||||||
|
this.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.getFreshData();
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
Promise.all([getVariable('autoConversion', false),]).then((values) => {
|
Promise.all([getVariable('autoConversion', false),]).then((values) => {
|
||||||
// this.autoConversion = values[0];
|
this.autoConversion = values[0];
|
||||||
// this.loadChart();
|
this.loadChart();
|
||||||
});
|
});
|
||||||
window.store.observe('end', () => {
|
window.store.observe('end', () => {
|
||||||
// this.chartData = null;
|
this.chartData = null;
|
||||||
// this.loadChart();
|
this.loadChart();
|
||||||
});
|
});
|
||||||
window.store.observe('autoConversion', (newValue) => {
|
window.store.observe('autoConversion', (newValue) => {
|
||||||
// this.autoConversion = newValue;
|
this.autoConversion = newValue;
|
||||||
// this.loadChart();
|
this.loadChart();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
155
resources/assets/v2/pages/dashboard/sankey.js
Normal file
155
resources/assets/v2/pages/dashboard/sankey.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* budgets.js
|
||||||
|
* Copyright (c) 2023 james@firefly-iii.org
|
||||||
|
*
|
||||||
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {getVariable} from "../../store/get-variable.js";
|
||||||
|
import Get from "../../api/v2/model/transaction/get.js";
|
||||||
|
import {getDefaultChartSettings} from "../../support/default-chart-settings.js";
|
||||||
|
import Chart from "chart.js/auto";
|
||||||
|
import {Flow, SankeyController} from 'chartjs-chart-sankey';
|
||||||
|
|
||||||
|
Chart.register(SankeyController, Flow);
|
||||||
|
|
||||||
|
let currencies = [];
|
||||||
|
|
||||||
|
let chart = null;
|
||||||
|
let transactions = [];
|
||||||
|
|
||||||
|
export default () => ({
|
||||||
|
loading: false,
|
||||||
|
autoConversion: false,
|
||||||
|
generateOptions(data) {
|
||||||
|
currencies = [];
|
||||||
|
console.log('generate options');
|
||||||
|
let options = getDefaultChartSettings('sankey');
|
||||||
|
|
||||||
|
// temp code for first sankey
|
||||||
|
const colors = {
|
||||||
|
a: 'red',
|
||||||
|
b: 'green',
|
||||||
|
c: 'blue',
|
||||||
|
d: 'gray'
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColor = (key) => colors[key];
|
||||||
|
// end of temp code for first sankey
|
||||||
|
let dataSet =
|
||||||
|
// sankey chart has one data set.
|
||||||
|
{
|
||||||
|
label: 'My sankey',
|
||||||
|
data: [
|
||||||
|
{from: 'a', to: 'b', flow: 10},
|
||||||
|
{from: 'a', to: 'c', flow: 5},
|
||||||
|
{from: 'b', to: 'c', flow: 10},
|
||||||
|
{from: 'd', to: 'c', flow: 7}
|
||||||
|
],
|
||||||
|
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from),
|
||||||
|
colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to),
|
||||||
|
colorMode: 'gradient', // or 'from' or 'to'
|
||||||
|
/* optional labels */
|
||||||
|
labels: {
|
||||||
|
a: 'Label A',
|
||||||
|
b: 'Label B',
|
||||||
|
c: 'Label C',
|
||||||
|
d: 'Label D'
|
||||||
|
},
|
||||||
|
/* optional priority */
|
||||||
|
priority: {
|
||||||
|
b: 1,
|
||||||
|
d: 0
|
||||||
|
},
|
||||||
|
/* optional column overrides */
|
||||||
|
column: {
|
||||||
|
d: 1
|
||||||
|
},
|
||||||
|
size: 'max', // or 'min' if flow overlap is preferred
|
||||||
|
};
|
||||||
|
options.data.datasets.push(dataSet);
|
||||||
|
|
||||||
|
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
drawChart(options) {
|
||||||
|
if (null !== chart) {
|
||||||
|
chart.data.datasets = options.data.datasets;
|
||||||
|
chart.update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chart = new Chart(document.querySelector("#sankey-chart"), options);
|
||||||
|
|
||||||
|
},
|
||||||
|
getFreshData() {
|
||||||
|
let params = {
|
||||||
|
start: window.store.get('start').slice(0, 10),
|
||||||
|
end: window.store.get('end').slice(0, 10),
|
||||||
|
type: 'withdrawal,deposit',
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
this.downloadTransactions(params);
|
||||||
|
},
|
||||||
|
downloadTransactions(params) {
|
||||||
|
console.log('Downloading page ' + params.page + '...');
|
||||||
|
const getter = new Get();
|
||||||
|
getter.get(params).then((response) => {
|
||||||
|
transactions = [...transactions, ...response.data.data];
|
||||||
|
//this.drawChart(this.generateOptions(response.data));
|
||||||
|
//this.loading = false;
|
||||||
|
if (parseInt(response.data.meta.pagination.total_pages) > params.page) {
|
||||||
|
// continue to next page.
|
||||||
|
params.page++;
|
||||||
|
this.downloadTransactions(params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// continue to next step.
|
||||||
|
console.log('Final page!');
|
||||||
|
console.log(transactions);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
loadChart() {
|
||||||
|
if (true === this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
if (0 !== transactions.length) {
|
||||||
|
this.drawChart(this.generateOptions());
|
||||||
|
this.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.getFreshData();
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
transactions = [];
|
||||||
|
Promise.all([getVariable('autoConversion', false),]).then((values) => {
|
||||||
|
this.autoConversion = values[0];
|
||||||
|
this.loadChart();
|
||||||
|
});
|
||||||
|
window.store.observe('end', () => {
|
||||||
|
this.transactions = [];
|
||||||
|
this.loadChart();
|
||||||
|
});
|
||||||
|
window.store.observe('autoConversion', (newValue) => {
|
||||||
|
this.autoConversion = newValue;
|
||||||
|
this.loadChart();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -33,7 +33,6 @@ const Basic = () => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const init = () => {
|
const init = () => {
|
||||||
console.log('Basic store init')
|
|
||||||
this.loadVariable('viewRange')
|
this.loadVariable('viewRange')
|
||||||
this.loadVariable('darkMode')
|
this.loadVariable('darkMode')
|
||||||
this.loadVariable('language')
|
this.loadVariable('language')
|
||||||
@ -89,7 +88,6 @@ const Basic = () => {
|
|||||||
const triggerReady = () => {
|
const triggerReady = () => {
|
||||||
this.count++;
|
this.count++;
|
||||||
if (this.count === this.readyCount) {
|
if (this.count === this.readyCount) {
|
||||||
console.log('Basic store is ready!')
|
|
||||||
// trigger event:
|
// trigger event:
|
||||||
const event = new Event("BasicStoreReady");
|
const event = new Event("BasicStoreReady");
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
@ -36,13 +36,11 @@ export function setVariable(name, value = null) {
|
|||||||
// post to user preferences (because why not):
|
// post to user preferences (because why not):
|
||||||
let putter = new Put();
|
let putter = new Put();
|
||||||
putter.put(name, value).then((response) => {
|
putter.put(name, value).then((response) => {
|
||||||
// console.log('Put in API');
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// preference does not exist (yet).
|
// preference does not exist (yet).
|
||||||
// POST it
|
// POST it
|
||||||
let poster = (new Post);
|
let poster = (new Post);
|
||||||
poster.post(name, value).then((response) => {
|
poster.post(name, value).then((response) => {
|
||||||
// console.log('Post in API');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function getDefaultChartSettings(type) {
|
function getDefaultChartSettings(type) {
|
||||||
|
if ('sankey' === type) {
|
||||||
|
return {
|
||||||
|
type: 'sankey',
|
||||||
|
data: {
|
||||||
|
datasets: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if ('column' === type) {
|
if ('column' === type) {
|
||||||
return {
|
return {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {},
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
<!--begin::Container-->
|
<!--begin::Container-->
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@include('partials.dashboard.boxes')
|
@include('partials.dashboard.boxes')
|
||||||
|
|
||||||
<!-- row with account data -->
|
<!-- row with account data -->
|
||||||
<div class="row mb-2" x-data="accounts">
|
<div class="row mb-2" x-data="accounts">
|
||||||
<div class="col-xl-8 col-lg-12 col-sm-12 col-xs-12">
|
<div class="col-xl-8 col-lg-12 col-sm-12 col-xs-12">
|
||||||
@ -20,7 +21,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="account-chart"></div>
|
<canvas id="account-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-end">
|
<div class="card-footer text-end">
|
||||||
<template x-if="autoConversion">
|
<template x-if="autoConversion">
|
||||||
@ -68,7 +69,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="category-chart"></div>
|
<canvas id="category-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -150,8 +151,8 @@
|
|||||||
title="{{ route('reports.index') }}">{{ __('firefly.income_and_expense') }}</a>
|
title="{{ route('reports.index') }}">{{ __('firefly.income_and_expense') }}</a>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body" x-data="sankey">
|
||||||
<div id="sankey-chart"></div>
|
<canvas id="sankey-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user