diff options
| author | Joe Carstairs <me@joeac.net> | 2024-09-05 22:44:37 +0100 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2024-09-05 22:44:37 +0100 |
| commit | 6e827308ce332a842c87f19d7adedf827544317e (patch) | |
| tree | 9509622166b56537469ec48bbe980efee75b6974 /gui/src/components | |
| parent | 95d4db4da725b74999524a1307e5e17b434dbfeb (diff) | |
Adds GUI with basic histogram
Diffstat (limited to 'gui/src/components')
| -rw-r--r-- | gui/src/components/app/App.css | 1 | ||||
| -rw-r--r-- | gui/src/components/app/App.tsx | 53 | ||||
| -rw-r--r-- | gui/src/components/histogram/Histogram.scss | 84 | ||||
| -rw-r--r-- | gui/src/components/histogram/Histogram.tsx | 70 |
4 files changed, 208 insertions, 0 deletions
diff --git a/gui/src/components/app/App.css b/gui/src/components/app/App.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/gui/src/components/app/App.css @@ -0,0 +1 @@ + diff --git a/gui/src/components/app/App.tsx b/gui/src/components/app/App.tsx new file mode 100644 index 0000000..cd9f9ee --- /dev/null +++ b/gui/src/components/app/App.tsx @@ -0,0 +1,53 @@ +import Category from '../../types/category'; +import Gbp from '../../types/currency/gbp'; +import Histogram from '../histogram/Histogram' +import './App.css' + +function App() { + return ( + <> + <Histogram categories={categories} /> + </> + ) +} + +const categories: Category[] = [ + { + id: 0, + name: 'Groceries', + currentBalance: new Gbp(71, 37, 1), + currentBudget: { + quantity: new Gbp(65, 0, 1), + period: 7, + }, + }, + { + id: 1, + name: 'Stationery', + currentBalance: new Gbp(10, 12, -1), + currentBudget: { + quantity: new Gbp(12, 0, 1), + period: 30, + }, + }, + { + id: 2, + name: 'Holidays', + currentBalance: new Gbp(325, 0, 1), + currentBudget: { + quantity: new Gbp(1000, 0, 1), + period: 365, + }, + }, + { + id: 3, + name: 'Wages', + currentBalance: new Gbp(1025, 60, -1), + currentBudget: { + quantity: new Gbp(2167, 7, -1), + period: 30.25, + }, + }, +].map(c => ({ ...c, balanceToBudgetRatio: c.currentBalance.valueOf() / c.currentBudget.quantity.valueOf() })); + +export default App diff --git a/gui/src/components/histogram/Histogram.scss b/gui/src/components/histogram/Histogram.scss new file mode 100644 index 0000000..e3f4604 --- /dev/null +++ b/gui/src/components/histogram/Histogram.scss @@ -0,0 +1,84 @@ +table { + border-collapse: collapse; +} + +:is(thead, .ratio__text) { + opacity: 0; + height: 0; + width: 0; +} + +tr { + display: grid; + gap: 0.3rem; + grid-template-columns: 10rem 1fr min-content; + justify-content: start; +} + +td.name { + text-align: right; +} + +td.ratio { + width: 10rem; + display: grid; + grid-template-columns: 5rem 5rem; + grid-template-rows: 1fr; + align-items: center; + + &::before { + content: ''; + display: block; + height: 100%; + grid-row: 1 / 2; + z-index: -1; + } + + &:is(.ratio--save, .ratio--spend)::before { + grid-column: 1 / 2; + border-right: 1px dashed white; + position: relative; + right: -1px; + } + + &.ratio--arrears::before { + grid-column: 2 / 3; + border-left: 1px dashed white; + left: -1px; + } +} + +.ratio__bar { + display: block; + width: calc(3rem * var(--normal-abs-ratio) + 1px); + height: 33%; + background-color: var(--category-highlight-color); + grid-row: 1 / 2; + + :is(.ratio--save, .ratio--spend) & { + grid-column: 2 / 3; + } + + .ratio--arrears & { + grid-column: 1 / 2; + position: relative; + right: -1px; + justify-self: end; + } +} + +td.balance { + color: var(--category-highlight-color); +} + +.ratio--arrears { + --category-highlight-color: crimson; +} + +.ratio--save { + --category-highlight-color: white; +} + +.ratio--spend { + --category-highlight-color: dodgerblue; +} diff --git a/gui/src/components/histogram/Histogram.tsx b/gui/src/components/histogram/Histogram.tsx new file mode 100644 index 0000000..ee4b7b0 --- /dev/null +++ b/gui/src/components/histogram/Histogram.tsx @@ -0,0 +1,70 @@ +import Category from "../../types/category"; +import './Histogram.scss'; + +function Histogram({ categories }: Props) { + const maxAbsBalanceToBudgetRatio = Math.max( + ...categories.map((c) => Math.abs(c.balanceToBudgetRatio)) + ); + + return ( + <table> + <thead> + <tr> + <th>Category</th> + <th>Balance-to-Budget Ratio</th> + <th>Balance</th> + </tr> + </thead> + + <tbody> + { + categories + .sort((c1, c2) => c1.balanceToBudgetRatio - c2.balanceToBudgetRatio) + .map((category) => Row({ category, maxAbsBalanceToBudgetRatio })) + } + </tbody> + </table> + ); +} + +function Row({ category, maxAbsBalanceToBudgetRatio }: RowProps) { + const normalisedRatio = category.balanceToBudgetRatio / maxAbsBalanceToBudgetRatio; + const variant = getRowVariant(new Number(normalisedRatio).valueOf()); + + return ( + <tr> + <td class="name">{category.name}</td> + <td + class={`ratio ratio--${variant}`} + style={`--normal-abs-ratio: ${Math.abs(normalisedRatio)};`} + > + <span class="ratio__text"> + {category.balanceToBudgetRatio.toFixed(2)} + </span> + <span class="ratio__bar"></span> + </td> + <td class="balance">{category.currentBalance.toString()}</td> + </tr> + ); +} + +function getRowVariant(balanceToBudgetRatio: number) { + if (balanceToBudgetRatio < 0) { + return 'arrears'; + } + if (balanceToBudgetRatio >= 1) { + return 'spend'; + } + return 'save'; +} + +type Props = { + categories: Category[], +}; + +type RowProps = { + category: Category, + maxAbsBalanceToBudgetRatio: number, +}; + +export default Histogram; |
