Skip to content

Commit 0ca109d

Browse files
author
Miles Hinchliffe
committed
Merge branch 'table-interactive' into github-private-npm-module-deployment
2 parents 70cacf3 + 7ad3a02 commit 0ca109d

File tree

6 files changed

+778
-1
lines changed

6 files changed

+778
-1
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<script>
2+
import { TabItem } from "flowbite-svelte";
3+
4+
// import { areSameLanguages } from "@maptiler/sdk";
5+
6+
let {
7+
componentNameProp = undefined,
8+
data = undefined,
9+
metaData = undefined,
10+
} = $props();
11+
12+
let localCopyOfData = $state([...data]);
13+
14+
const metrics = Object.keys(localCopyOfData[0]).slice(
15+
1,
16+
localCopyOfData[0].length,
17+
);
18+
19+
let sortState = $state({ column: "sortedColumn", order: "ascending" });
20+
21+
function updateSortState(columnToSort, sortOrder) {
22+
sortState.column = columnToSort;
23+
sortState.order = sortOrder;
24+
}
25+
26+
function sortFunction() {
27+
if (typeof localCopyOfData[0][sortState["column"]] === "number") {
28+
if (sortState.order === "ascending") {
29+
localCopyOfData.sort(
30+
(a, b) => a[sortState.column] - b[sortState.column],
31+
);
32+
} else {
33+
localCopyOfData.sort(
34+
(a, b) => b[sortState.column] - a[sortState.column],
35+
);
36+
}
37+
}
38+
if (typeof localCopyOfData[0][sortState["column"]] === "string") {
39+
if (sortState.order === "ascending") {
40+
localCopyOfData.sort((a, b) => a.areaName.localeCompare(b.name));
41+
} else {
42+
localCopyOfData.sort((a, b) => b.areaName.localeCompare(a.name));
43+
}
44+
}
45+
}
46+
47+
// heat map
48+
49+
// calculate the min and max of each metric
50+
const minAndMaxValues = {}; // create an empty object to store them in
51+
for (const metric of metrics) {
52+
// get the values
53+
const metricValues = localCopyOfData.map((item) => item[metric]);
54+
const min = Math.min(...metricValues);
55+
const max = Math.max(...metricValues);
56+
// store them
57+
minAndMaxValues[metric] = { min, max };
58+
}
59+
60+
localCopyOfData = localCopyOfData.map((row) => {
61+
const rowWithNorms = { ...row };
62+
63+
for (const metric of metrics) {
64+
const { min, max } = minAndMaxValues[metric];
65+
const value = row[metric];
66+
const normalisedValue = (value - min) / (max - min);
67+
68+
rowWithNorms[`${metric}__normalised`] = normalisedValue;
69+
}
70+
71+
return rowWithNorms;
72+
});
73+
74+
function normToColor(norm) {
75+
const hue = 120 * norm;
76+
return `hsl(${hue}, 100%, 80%)`;
77+
}
78+
79+
function normToColorReverse(norm) {
80+
const hue = 120 * (1 - norm);
81+
return `hsl(${hue}, 100%, 80%)`;
82+
}
83+
84+
const colorKey = Object.entries({ Good: 1, Ok: 0.5, Bad: 0 });
85+
</script>
86+
87+
{#snippet propNameAndValue(marginTW, paddingTW, text)}
88+
<span class="bg-slate-100 inline-block italic {marginTW} {paddingTW} rounded"
89+
>{text}</span
90+
>
91+
{/snippet}
92+
93+
<div class="p-4">
94+
<h4>{componentNameProp} component</h4>
95+
96+
<div class="legend">
97+
<div>Colour key:</div>
98+
{#each colorKey as key}
99+
<div class="good" style="background-color: {normToColor(key[1])}">
100+
{key[0]}
101+
</div>
102+
{/each}
103+
</div>
104+
105+
<div class="table-container">
106+
<table class="my-table">
107+
<caption></caption>
108+
<thead
109+
><tr>
110+
<th class="col-one-header">Area</th>
111+
{#each metrics as metric}
112+
<th title={metaData[metric].explainer}>
113+
<div class="header">
114+
<div class="header-top">
115+
<div class="metric">{metaData[metric].label}</div>
116+
<div class="sorting-button">
117+
<button
118+
onclick={() => {
119+
updateSortState(metric, "ascending");
120+
sortFunction();
121+
}}>▲</button
122+
>
123+
<button
124+
onclick={() => {
125+
updateSortState(metric, "descending");
126+
sortFunction();
127+
}}>▼</button
128+
>
129+
</div>
130+
</div>
131+
<div class="metric-explainer">
132+
{metaData[metric].explainer}
133+
</div>
134+
</div>
135+
</th>
136+
{/each}
137+
</tr></thead
138+
>
139+
<tbody>
140+
{#each localCopyOfData as row}
141+
<tr>
142+
<td class="areas">{row["areaName"]}</td>
143+
{#each metrics as metric}
144+
{#if metaData[metric].direction === "Higher is better"}
145+
<td
146+
style="background-color: {normToColor(
147+
row[metric + '__normalised'],
148+
)}">{row[metric]}</td
149+
>
150+
{:else}
151+
<td
152+
style="background-color: {normToColorReverse(
153+
row[metric + '__normalised'],
154+
)}">{row[metric]}</td
155+
>
156+
{/if}
157+
{/each}
158+
</tr>
159+
{/each}
160+
</tbody>
161+
</table>
162+
</div>
163+
</div>
164+
165+
<style>
166+
* {
167+
margin: 0px;
168+
padding: 0px;
169+
}
170+
171+
.table-container {
172+
max-height: 85vh;
173+
overflow-y: auto;
174+
border: 1px solid black;
175+
border-radius: 1%;
176+
}
177+
178+
.ascending {
179+
background-color: #ff7f7f;
180+
}
181+
.descending {
182+
background-color: #add8e6;
183+
}
184+
.buttons-container {
185+
display: flex;
186+
gap: 20px;
187+
}
188+
189+
.metric-explainer {
190+
font-size: 13px;
191+
font-style: italic;
192+
font-weight: 400;
193+
}
194+
.legend {
195+
display: flex;
196+
justify-content: center;
197+
gap: 20px;
198+
margin: 10px;
199+
}
200+
201+
.legend > * {
202+
border-radius: 10%;
203+
padding: 6px;
204+
}
205+
206+
td {
207+
padding: 0.5rem 0.5rem;
208+
}
209+
210+
th {
211+
text-align: left;
212+
font-size: medium;
213+
vertical-align: top;
214+
}
215+
216+
td {
217+
text-align: right;
218+
}
219+
.areas {
220+
font-size: medium;
221+
}
222+
.my-table {
223+
table-layout: fixed;
224+
width: 100%;
225+
}
226+
227+
.my-table th:first-child,
228+
.my-table td:first-child {
229+
width: 25%;
230+
}
231+
232+
.my-table th:nth-child(n + 2),
233+
.my-table td:nth-child(n + 2) {
234+
width: 25%;
235+
}
236+
237+
.header {
238+
display: flex;
239+
flex-direction: column;
240+
padding: 5px;
241+
justify-content: flex-start;
242+
}
243+
244+
.header-top {
245+
display: flex;
246+
gap: 0px;
247+
}
248+
249+
.sorting-button {
250+
display: flex;
251+
flex-direction: column;
252+
font-size: 0.8em;
253+
line-height: 1; /* removes extra space between lines */
254+
gap: 3px;
255+
justify-content: center;
256+
background-color: lightgray;
257+
border-radius: 20%;
258+
}
259+
.col-one-header {
260+
text-align: right;
261+
padding: 5px;
262+
}
263+
</style>

src/routes/+layout.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,47 @@ export const load: LayoutLoad = async (event) => {
8484
),
8585
}));
8686

87+
let dataInFormatForTable = testData.flatMetricData
88+
.map(item => ({
89+
...item,
90+
y: Math.round(parseFloat(item.y)),
91+
}))
92+
.map(d => ({
93+
...d,
94+
areaName: testData.areaCodeLookup[d.areaCode]
95+
}))
96+
.map(({areaCode, xLabel, ...rest }) => rest)
97+
.filter((el) => el.x === 2022)
98+
.map(({ x, ...rest }) => rest)
99+
100+
let groupedTableData = {}
101+
102+
for (let row of dataInFormatForTable) {
103+
if(!groupedTableData[row.areaName]) {
104+
groupedTableData[row.areaName] = {areaName: row.areaName}
105+
}
106+
groupedTableData[row.areaName][row.metric] = row.y
107+
}
108+
109+
let tableData = Object.values(groupedTableData);
110+
111+
let metaData = testData.metaData
112+
87113
return {
88114
metrics,
89115
areas,
90116
years,
91117
dataInFormatForLineChart,
92118
dataInFormatForBarChart,
93119
dataInFormatForMap,
120+
dataInFormatForTable,
121+
tableData,
94122
areaCodeLookup: testData.areaCodeLookup,
95123
svgFontDimensions,
96124
componentSections,
97125
componentDirectories,
98126
uiComponents,
99127
componentTree,
128+
metaData,
100129
};
101130
};

0 commit comments

Comments
 (0)