budget-view-chart
Version:
A React chart component specialising in display budget for personal finance.
371 lines (370 loc) • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChartRenders = void 0;
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
const Constants_1 = require("./Constants");
class ChartRenders {
budgetNames;
totalBudget;
fullWidth;
fullHeight;
lowestY;
numberFormatter;
monthLabelGetter;
budgetColorGetter;
constructor(budgetNames, totalBudget, fullWidth, fullHeight, lowestY, locale, currency) {
this.budgetNames = budgetNames;
this.totalBudget = totalBudget;
this.fullWidth = fullWidth;
this.fullHeight = fullHeight;
this.lowestY = lowestY;
this.numberFormatter = (amount) => {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency
});
return formatter.format(amount);
};
this.monthLabelGetter = (month) => {
return Constants_1.monthLabels[month];
};
this.budgetColorGetter = (budgetName) => {
return Constants_1.defaultColorPalettes[this.budgetNames.indexOf(budgetName) % Constants_1.defaultColorPalettes.length];
};
}
/* Budget */
renderBudgetLabel = (params, api) => {
return {
type: 'group',
children: [
this.renderBudgetBlock(params, api),
this.renderBudgetText(params, api)
],
focus: 'self',
blurScope: 'series'
};
};
renderBudgetText = (params, api) => {
const start = api.coord([api.value('xStart'), 0]);
const size = api.size([api.value('xLength'), 0]);
const baseStyle = {
text: `${api.value('name')}`,
textAlign: 'left',
textVerticalAlign: 'middle',
opacity: 1
};
return {
type: 'text',
x: start[0] + size[0] / 2,
y: api.coord([0, Math.floor(this.lowestY / 25) * 25])[1] + 30,
rotation: -Math.PI / 2,
style: baseStyle,
blur: {
style: {
opacity: 1
}
},
emphasis: {
style: {
fontWeight: 600
}
}
};
};
renderBudgetBlock = (params, api) => {
const fill = this.budgetColorGetter(api.value('name'));
const yValue = api.value('yLength');
const y = yValue < 0 ? api.value('yStart') : yValue + api.value('yStart');
const start = api.coord([api.value('xStart'), y]);
const size = api.size([api.value('xLength'), Math.abs(yValue)]);
const baseStyle = {
fill,
opacity: 0.3
};
if (yValue < 0) {
baseStyle.fill = 'rgba(0, 0, 0, 0)';
baseStyle.decal = {
symbol: 'rect',
dashArrayX: [2, 0],
dashArrayY: [3, 5],
rotation: -Math.PI / 4,
color: fill
};
}
return {
type: 'rect',
shape: {
x: start[0],
y: start[1],
width: size[0],
height: size[1]
},
style: baseStyle,
emphasis: {
style: {
opacity: 0.4
}
},
blur: {
style: {
opacity: 0.2
}
}
};
};
/* Spending - Breakdown */
renderMonthlyBreakdown = (param, api) => {
if (api.value('type') === 'aggregate') {
return this.renderMonthLegendWithFocus(param, api);
}
return this.renderBreakdownBlock(param, api);
};
/* Spending - Aggregate */
renderMonthlyAggregate = (param, api) => {
return {
type: 'group',
ignore: api.value('type') !== 'aggregate',
children: [
this.renderMonthLegendWithFocus(param, api),
this.renderMonthlyAggregateBlock(param, api)
],
focus: 'self',
blurScope: 'series'
};
};
renderMonthLegend = (params, api) => {
const month = api.value('month');
const boxWidthPx = 30;
const boxHeightVal = this.fullHeight / Constants_1.MONTH_PER_YEAR;
const monthSize = api.size([0, boxHeightVal]);
const monthStart = api.coord([0, boxHeightVal * (month + 1)]);
const baseBoxStyle = {
fill: '#444444',
opacity: 0.8
};
const baseTextStyle = {
fill: '#c7c7c7',
text: `${this.monthLabelGetter(month)}`
};
return {
type: 'rect',
id: `month-legend-${month}`,
shape: {
x: monthStart[0] - boxWidthPx,
y: monthStart[1],
width: boxWidthPx,
height: monthSize[1]
},
style: baseBoxStyle,
emphasis: {
style: {
opacity: 1
}
},
blur: {
style: {
opacity: 0.8
}
},
textConfig: {
position: 'inside',
inside: true
},
textContent: {
style: baseTextStyle,
emphasisDisabled: false,
emphasis: {
style: {
fontWeight: '600',
fill: '#ffffff'
}
},
blur: {
style: {
fontWeight: '600',
fill: '#ffffff',
opacity: 1
}
}
},
morph: false
};
};
renderMonthLegendWithFocus = (params, api) => {
return {
...this.renderMonthLegend(params, api),
focus: 'series',
blurScope: 'global'
};
};
renderBreakdownBlock = (params, api) => {
const fill = this.budgetColorGetter(api.value('name'));
const yValue = api.value('yLength');
const y = yValue < 0 ? api.value('yStart') : yValue + api.value('yStart');
const start = api.coord([api.value('xStart'), y]);
const size = api.size([api.value('xLength'), Math.abs(yValue)]);
const baseBoxStyle = {
fill
};
if (yValue < 0) {
baseBoxStyle.fill = 'rgba(0, 0, 0, 0)';
baseBoxStyle.decal = {
symbol: 'rect',
dashArrayX: [2, 0],
dashArrayY: [3, 5],
rotation: -Math.PI / 4,
color: fill
};
}
return {
type: 'rect',
shape: {
x: start[0],
y: start[1],
width: size[0],
height: size[1]
},
style: baseBoxStyle,
emphasis: {
style: {
stroke: '#000',
lineWidth: 1
}
},
blur: {
style: {
opacity: 0.3
}
},
focus: 'self',
blurScope: 'global'
};
};
renderMonthlyAggregateBlock = (param, api) => {
const month = api.value('month');
const yValue = api.value('yLength');
const y = yValue < 0 ? api.value('yStart') : yValue + api.value('yStart');
const start = api.coord([api.value('xStart'), y]);
const size = api.size([api.value('xLength'), Math.abs(yValue)]);
const baseBoxStyle = {
fill: '#444444',
opacity: 0.8
};
const baseTextStyle = {
fill: '#c7c7c7',
text: `${this.monthLabelGetter(month)}`
};
if (yValue < 0) {
baseBoxStyle.fill = 'rgba(0, 0, 0, 0)';
baseBoxStyle.decal = {
symbol: 'rect',
dashArrayX: [2, 0],
dashArrayY: [3, 5],
rotation: -Math.PI / 4,
color: '#444444'
};
}
return {
type: 'rect',
shape: {
x: start[0],
y: start[1],
width: size[0],
height: size[1]
},
style: baseBoxStyle,
emphasis: {
style: {
opacity: 1
}
},
blur: {
style: {
opacity: 0.8
}
},
textConfig: {
position: 'inside',
inside: true
},
textContent: {
style: baseTextStyle,
emphasis: {
style: {
fontWeight: '600',
fill: '#ffffff'
}
}
},
ignore: yValue === 0
};
};
renderHorizontalLine = (valueFunction, param, api) => {
const h = valueFunction(api) / this.totalBudget * this.fullHeight;
const start = api.coord([0, h]);
const end = api.coord([this.fullWidth, h]);
return {
type: 'line',
transition: ['shape'],
shape: {
x1: start[0],
x2: end[0],
y1: start[1],
y2: end[1]
},
style: {
fill: null,
stroke: '#e43',
lineWidth: 2
}
};
};
budgetDataTooltipFormatter = (data) => {
return `
<b>${data.name}</b>
<p>${data.description}</p>
<hr/>
<div style="display: block">Annual Budget: <b style="float: right; margin-left:10px">${this.numberFormatter(data.monthlyBudget * Constants_1.MONTH_PER_YEAR)}</b></div>
<div style="display: block">Annual Amount: <b style="float: right; margin-left:10px">${this.numberFormatter(data.amount)}</b></div>
<div style="display: block">Left to Spend: <b style="float: right; margin-left:10px">${this.numberFormatter(data.monthlyBudget * Constants_1.MONTH_PER_YEAR - data.amount)}</b></div>
`;
};
chartDataTooltipFormatter = (data) => {
if (data.type === 'aggregate') {
return `
<b>${this.monthLabelGetter(data.month)}</b> <hr/>
<div style="display: block">Monthly Total Budget: <b style="float: right; margin-left:10px">${this.numberFormatter(data.monthlyBudget)}</b></div>
<div style="display: block">Monthly Total Amount: <b style="float: right; margin-left:10px">${this.numberFormatter(data.amount)}</b></div>
`;
}
else if (data.type === 'breakdown') {
return `
<b>${data.name}</b> <br/>
<b>${this.monthLabelGetter(data.month)}</b> <hr/>
<div style="display: block">Monthly Budget: <b style="float: right; margin-left:10px">${this.numberFormatter(data.monthlyBudget)}</b></div>
<div style="display: block">Monthly Amount: <b style="float: right; margin-left:10px">${this.numberFormatter(data.amount)}</b></div>
`;
}
else {
throw new Error('Unknown type');
}
};
totalLineTooltipFormatter = (totalBudget, totalAmount) => {
return `
<b>Total</b> <hr/>
<div style="display: block">Annual Budget: <b style="float: right; margin-left:10px">${this.numberFormatter(totalBudget)}</b></div>
<div style="display: block">Annual Amount: <b style="float: right; margin-left:10px">${this.numberFormatter(totalAmount)}</b></div>
<div style="display: block">Left to Spend: <b style="float: right; margin-left:10px">${this.numberFormatter(totalBudget - totalAmount)}</b></div>
`;
};
currentMonthEndLineTooltipFormatter = (budgetToMonthEnd, amountToMonthEnd) => {
return `
<b>Current</b> <hr/>
<div style="display: block">Current Budget: <b style="float: right; margin-left:10px">${this.numberFormatter(budgetToMonthEnd)}</b></div>
<div style="display: block">Current Amount: <b style="float: right; margin-left:10px">${this.numberFormatter(amountToMonthEnd)}</b></div>
<div style="display: block">Left to Spend: <b style="float: right; margin-left:10px">${this.numberFormatter(budgetToMonthEnd - amountToMonthEnd)}</b></div>
`;
};
}
exports.ChartRenders = ChartRenders;