summaryrefslogtreecommitdiff
path: root/gui/src/components/histogram
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/components/histogram')
-rw-r--r--gui/src/components/histogram/Histogram.scss84
-rw-r--r--gui/src/components/histogram/Histogram.tsx70
2 files changed, 154 insertions, 0 deletions
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;