Map view for results
This commit is contained in:
+9
-2
@@ -7,12 +7,19 @@ import { FileManager } from './fileManager/FileManager';
|
||||
console.log("env:", import.meta.env.MODE);
|
||||
console.log("api host:", import.meta.env.VITE_API_HOST);
|
||||
|
||||
type Section = "aggregator" | "fileManager";
|
||||
type Section = "aggregator" | "mapView" | "fileManager";
|
||||
|
||||
const App: Component = () => {
|
||||
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 (
|
||||
<div class={styles.App}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import moment from "moment";
|
||||
import { createSignal } from "solid-js";
|
||||
import { QueryResult } from "./result/QueryResult";
|
||||
|
||||
import { Result } from "./Result";
|
||||
import { SelectCity } from "./SelectCity";
|
||||
import { SelectField } from "./SelectField";
|
||||
import { SelectGranularity } from "./SelectGranularity";
|
||||
@@ -16,7 +16,7 @@ export function Aggregator() {
|
||||
const [getStart, setStart] = createSignal(dayAgo);
|
||||
const [getEnd, setEnd] = createSignal(nowRounded);
|
||||
const [getField, setField] = createSignal("tempMax");
|
||||
const [getKey, setKey] = createSignal("list");
|
||||
const [getKey, setKey] = createSignal("max");
|
||||
const [getGranularity, setGranularity] = createSignal("hour");
|
||||
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ export function Aggregator() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<Result
|
||||
<QueryResult
|
||||
getCities={getCities}
|
||||
getStart={getStart}
|
||||
getEnd={getEnd}
|
||||
|
||||
@@ -14,6 +14,11 @@ export function formatDateString(str: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryResult {
|
||||
query: DataQuery;
|
||||
result: {[key: string]: any};
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
cities: string[];
|
||||
field: string;
|
||||
@@ -21,6 +26,18 @@ export interface DataQuery {
|
||||
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 {
|
||||
return variable &&
|
||||
Array.isArray(variable.cities) &&
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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 { Accessor, Component, createResource, createSignal } from "solid-js";
|
||||
|
||||
import "../css/Result.css"
|
||||
import { apiHost, ResultKeyVal, resultOrder, ResultOrderKeys } from "../consts";
|
||||
import { SelectOrder } from "./SelectOrder";
|
||||
import { CityResult } from "./chart/CityResult";
|
||||
import { isDataQuery } from "./helpers";
|
||||
import "../../css/Result.css"
|
||||
import { apiHost } from "../../consts";
|
||||
import { isQueryResult } from "../helpers";
|
||||
import { Result } from "./Result";
|
||||
|
||||
export const Result: Component<{
|
||||
export const QueryResult: Component<{
|
||||
getCities: Accessor<Set<string>>,
|
||||
getStart: Accessor<Date>,
|
||||
getEnd: Accessor<Date>,
|
||||
@@ -29,7 +28,6 @@ export const Result: Component<{
|
||||
}
|
||||
|
||||
const [queryResource] = createResource(getTimestamp, fetchQuery);
|
||||
const [getOrderKey, setOrderKey] = createSignal<ResultOrderKeys>("A -> Z");
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -39,10 +37,6 @@ export const Result: Component<{
|
||||
value="Query Data"
|
||||
onClick={() => setTimestamp(Date.now())}
|
||||
/>
|
||||
<div class="resultTitle">
|
||||
<h3>Result:</h3>
|
||||
<SelectOrder getter={getOrderKey} setter={setOrderKey} />
|
||||
</div>
|
||||
{ queryResource.loading && (
|
||||
<div>
|
||||
<span class="spinner"></span>
|
||||
@@ -55,19 +49,9 @@ export const Result: Component<{
|
||||
{ queryResource() && queryResource() instanceof Error &&
|
||||
<div>{ queryResource().message }</div>
|
||||
}
|
||||
{ queryResource() && queryResource().result && isDataQuery(queryResource().query) && (
|
||||
<div class="result-container">
|
||||
{ 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>
|
||||
)}
|
||||
{ queryResource() && isQueryResult( queryResource() ) &&
|
||||
<Result result={queryResource} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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")}
|
||||
/>
|
||||
|
|
||||
<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 +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 |
Reference in New Issue
Block a user