Map view for results

This commit is contained in:
Guntis Smaukstelis
2023-08-07 23:25:35 +03:00
parent 4c9af88859
commit 5ce297e96a
10 changed files with 198 additions and 42 deletions
+9 -2
View File
@@ -7,12 +7,19 @@ import { FileManager } from './fileManager/FileManager';
console.log("env:", import.meta.env.MODE); console.log("env:", import.meta.env.MODE);
console.log("api host:", import.meta.env.VITE_API_HOST); console.log("api host:", import.meta.env.VITE_API_HOST);
type Section = "aggregator" | "fileManager"; type Section = "aggregator" | "mapView" | "fileManager";
const App: Component = () => { const App: Component = () => {
const [getSection, setSection] = createSignal<Section>("aggregator"); const [getSection, setSection] = createSignal<Section>("aggregator");
const section = () => getSection() === "aggregator" ? <Aggregator /> : <FileManager />; const section = () => {
switch(getSection()) {
case "fileManager": return <FileManager />;
case "aggregator":
default:
return <Aggregator />;
}
}
return ( return (
<div class={styles.App}> <div class={styles.App}>
+3 -3
View File
@@ -1,7 +1,7 @@
import moment from "moment"; import moment from "moment";
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { QueryResult } from "./result/QueryResult";
import { Result } from "./Result";
import { SelectCity } from "./SelectCity"; import { SelectCity } from "./SelectCity";
import { SelectField } from "./SelectField"; import { SelectField } from "./SelectField";
import { SelectGranularity } from "./SelectGranularity"; import { SelectGranularity } from "./SelectGranularity";
@@ -16,7 +16,7 @@ export function Aggregator() {
const [getStart, setStart] = createSignal(dayAgo); const [getStart, setStart] = createSignal(dayAgo);
const [getEnd, setEnd] = createSignal(nowRounded); const [getEnd, setEnd] = createSignal(nowRounded);
const [getField, setField] = createSignal("tempMax"); const [getField, setField] = createSignal("tempMax");
const [getKey, setKey] = createSignal("list"); const [getKey, setKey] = createSignal("max");
const [getGranularity, setGranularity] = createSignal("hour"); const [getGranularity, setGranularity] = createSignal("hour");
return ( return (
@@ -42,7 +42,7 @@ export function Aggregator() {
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<Result <QueryResult
getCities={getCities} getCities={getCities}
getStart={getStart} getStart={getStart}
getEnd={getEnd} getEnd={getEnd}
+17
View File
@@ -14,6 +14,11 @@ export function formatDateString(str: string): string {
} }
} }
export interface QueryResult {
query: DataQuery;
result: {[key: string]: any};
}
export interface DataQuery { export interface DataQuery {
cities: string[]; cities: string[];
field: string; field: string;
@@ -21,6 +26,18 @@ export interface DataQuery {
key: string; key: string;
} }
export function isQueryResult(variable: any): variable is QueryResult {
return variable
&& variable.query
&& isDataQuery(variable.query)
&& variable.result
&& (
typeof variable.result === "object"
&& !Array.isArray(variable.result)
&& variable.result !== null
);
}
export function isDataQuery(variable: any): variable is DataQuery { export function isDataQuery(variable: any): variable is DataQuery {
return variable && return variable &&
Array.isArray(variable.cities) && Array.isArray(variable.cities) &&
+56
View File
@@ -0,0 +1,56 @@
import { Component, createEffect, createSignal, onMount } from "solid-js";
import { ResultKeyVal } from "../../consts";
import { cityCoords } from "./cityCoords";
export const MapView: Component<{ data: () => ResultKeyVal[] }> = ({ data }) => {
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
const [getImg, setImg] = createSignal(new Image());
let lastCoords: [string, {x: number, y: number }, number][] = [];
onMount(() => {
const ctx = getCanvas()!.getContext("2d")!;
const img = new Image();
img.onload = () => {
setImg(img);
drawOnMap(ctx, img, lastCoords);
};
img.src = "src/assets/map_1920x1080.webp";
});
createEffect(() => {
const ctx = getCanvas()!.getContext("2d")!;
const coordsAndData: [string, {x: number, y: number }, number][] = data()
.map(([city, value]) => [
city,
cityCoords[city as keyof typeof cityCoords],
typeof value === "number" ? value : -99,
]);
lastCoords = coordsAndData;
drawOnMap(ctx, getImg(), coordsAndData);
});
return (
<canvas ref={setCanvas} width="1920px" height="1080px" />
);
}
function drawOnMap(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
coords: [string, {x: number, y: number }, number][]
): void {
const size = 80;
ctx.font = "bold 30px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.drawImage(img, 0, 0);
coords.forEach(([, {x, y}, value]) => {
ctx.beginPath();
ctx.fillStyle = "#FFFFFF";
ctx.rect(x, y, size, size);
ctx.fill();
ctx.fillStyle = "#000000";
ctx.fillText(`${value}`, x + size/2, y + size/2);
});
}
+35
View File
@@ -0,0 +1,35 @@
export const cityCoords = {
"Ainaži": { x: 685, y: 146 },
"Alūksne": { x: 1176, y: 294 },
"Bauska": { x: 721, y: 608 },
"Dagda": { x: 1280, y: 680 },
"Daugavgrīva": { x: 640, y: 410 },
"Daugavpils": { x: 1140, y: 739 },
"Dobele": { x: 550, y: 580 },
"Gulbene": { x: 1100, y: 370 },
"Jelgava": { x: 620, y: 570 },
"Kalnciems": { x: 600, y: 500 },
"Kolka": { x: 380, y: 200 },
"Kuldīga": { x: 300, y: 470 },
"Lielpēči": { x: 780, y: 490 },
"Liepāja": { x: 124, y: 638 },
"Madona": { x: 1004, y: 432 },
"Mērsrags": { x: 480, y: 300 },
"Pāvilosta": { x: 160, y: 450 },
"Piedruja": { x: 1290, y: 770 },
"Priekuļi": { x: 900, y: 320 },
"Rēzekne": { x: 1229, y: 544 },
"Rīga": { x: 689, y: 430 },
"Rucava": { x: 140, y: 710 },
"Rūjiena": { x: 870, y: 140 },
"Saldus": { x: 383, y: 561 },
"Sigulda": { x: 810, y: 380 },
"Sīļi": { x: 1050, y: 640 },
"Skrīveri": { x: 880, y: 530 },
"Skulte": { x: 710, y: 320 },
"Stende": { x: 402, y: 400 },
"Ventspils": { x: 210, y: 329 },
"Vičaki": { x: 270, y: 260},
"Zīlāni": { x: 983, y: 581 },
"Zosēni": { x: 970, y: 370 },
};
@@ -1,13 +1,12 @@
import moment from "moment"; import moment from "moment";
import { Accessor, Component, createResource, createSignal } from "solid-js"; import { Accessor, Component, createResource, createSignal } from "solid-js";
import "../css/Result.css" import "../../css/Result.css"
import { apiHost, ResultKeyVal, resultOrder, ResultOrderKeys } from "../consts"; import { apiHost } from "../../consts";
import { SelectOrder } from "./SelectOrder"; import { isQueryResult } from "../helpers";
import { CityResult } from "./chart/CityResult"; import { Result } from "./Result";
import { isDataQuery } from "./helpers";
export const Result: Component<{ export const QueryResult: Component<{
getCities: Accessor<Set<string>>, getCities: Accessor<Set<string>>,
getStart: Accessor<Date>, getStart: Accessor<Date>,
getEnd: Accessor<Date>, getEnd: Accessor<Date>,
@@ -29,7 +28,6 @@ export const Result: Component<{
} }
const [queryResource] = createResource(getTimestamp, fetchQuery); const [queryResource] = createResource(getTimestamp, fetchQuery);
const [getOrderKey, setOrderKey] = createSignal<ResultOrderKeys>("A -> Z");
return ( return (
<div> <div>
@@ -39,10 +37,6 @@ export const Result: Component<{
value="Query Data" value="Query Data"
onClick={() => setTimestamp(Date.now())} onClick={() => setTimestamp(Date.now())}
/> />
<div class="resultTitle">
<h3>Result:</h3>
<SelectOrder getter={getOrderKey} setter={setOrderKey} />
</div>
{ queryResource.loading && ( { queryResource.loading && (
<div> <div>
<span class="spinner"></span> <span class="spinner"></span>
@@ -55,19 +49,9 @@ export const Result: Component<{
{ queryResource() && queryResource() instanceof Error && { queryResource() && queryResource() instanceof Error &&
<div>{ queryResource().message }</div> <div>{ queryResource().message }</div>
} }
{ queryResource() && queryResource().result && isDataQuery(queryResource().query) && ( { queryResource() && isQueryResult( queryResource() ) &&
<div class="result-container"> <Result result={queryResource} />
{ Object.entries(queryResource().result) }
.map(keyVal => [...keyVal] as ResultKeyVal)
.sort((a, b) => resultOrder[getOrderKey()](a, b))
.map(cityData =>
<CityResult
city={cityData[0]}
query={queryResource().query}
result={cityData[1]}
/>)
}</div>
)}
</div> </div>
); );
} }
+58
View File
@@ -0,0 +1,58 @@
import { Component, createSignal, Resource } from "solid-js";
import "../../css/Result.css"
import { ResultKeyVal, resultOrder, ResultOrderKeys } from "../../consts";
import { SelectOrder } from "../SelectOrder";
import { CityResult } from "../chart/CityResult";
import { QueryResult } from "../helpers";
import { MapView } from "../map/MapView";
export const Result: Component<{
result: Resource<QueryResult>
}> = ({ result: resultResource }) => {
const [getOrderKey, setOrderKey] = createSignal<ResultOrderKeys>("A -> Z");
type ResultView = "text" | "map";
const [getResultView, setResultView] = createSignal<ResultView>("text");
const cityData = () =>
Object.entries(resultResource()!.result)
.map(keyVal => [...keyVal] as ResultKeyVal)
.sort((a, b) => resultOrder[getOrderKey()](a, b));
return (
<div>
<div class="resultTitle">
<h3>Result:</h3>
<span>
<input
type="button"
class="secondary"
value="text"
onClick={() => setResultView("text")}
/>
&nbsp; | &nbsp;
<input
type="button"
class="secondary"
value="map"
onClick={() => setResultView("map")}
/>
</span>
<span style={{ visibility: getResultView() === "text" ? "visible" : "hidden" }}>
<SelectOrder getter={getOrderKey} setter={setOrderKey} />
</span>
</div>
<div class="result-container">
{ getResultView() === "text"
? cityData().map(([city, data]) =>
<CityResult
city={city}
query={resultResource()!.query}
result={data}
/>)
: <MapView data={cityData} />
}
</div>
</div>
);
}
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><path d="M52 35l-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><path d="M134 80a45 45 0 00-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient><path d="M114 115a45 45 0 00-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

+8 -8
View File
@@ -1,24 +1,24 @@
.container { .container {
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
} }
.column { .column {
flex: 1; flex: 1;
padding: 10px; padding: 10px;
box-sizing: border-box; box-sizing: border-box;
text-align: left; text-align: left;
} }
.column:nth-child(1), .column:nth-child(1),
.column:nth-child(2) { .column:nth-child(2) {
flex-basis: 15%; flex-basis: 15%;
background-color: #f2f2f2; background-color: #f2f2f2;
min-width: 200px; min-width: 200px;
} }
.column:nth-child(3) { .column:nth-child(3) {
flex-basis: 70%; flex-basis: 70%;
background-color: #e6e6e6; background-color: #e6e6e6;
min-width: 680px; min-width: 680px;
} }