adding market heatmap

This commit is contained in:
MuslemRahimi 2025-01-27 13:40:00 +01:00
parent 126c40f806
commit 902b734302
8 changed files with 302 additions and 71 deletions

45
package-lock.json generated
View File

@ -40,6 +40,7 @@
"file-saver": "^2.0.5",
"flowbite-svelte": "^0.46.15",
"got": "^14.4.2",
"html2canvas": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"katex": "^0.16.11",
"layercake": "^8.4.0-beta.1",
@ -3732,6 +3733,16 @@
"integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==",
"license": "MIT"
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
@ -5580,6 +5591,20 @@
"integrity": "sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==",
"license": "MIT"
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -9307,6 +9332,16 @@
"license": "MIT",
"peer": true
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -9628,6 +9663,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",

View File

@ -42,6 +42,7 @@
"file-saver": "^2.0.5",
"flowbite-svelte": "^0.46.15",
"got": "^14.4.2",
"html2canvas": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"katex": "^0.16.11",
"layercake": "^8.4.0-beta.1",

View File

@ -409,19 +409,19 @@
>Market Mover</a
>
</Button>
<!--
<Button
builders={[builder]}
type="submit"
class="w-full bg-[#141417] hover:bg-[#141417]"
>
<a
href="/heatmaps"
href="/heatmap"
class="text-start w-full text-[1rem] text-white ml-4 mt-4"
>Heatmaps</a
>Market Heatmap</a
>
</Button>
-->
<Button
builders={[builder]}
type="submit"
@ -969,12 +969,13 @@
class="text-[1rem] text-white ml-4 mt-4"
>Market Mover</a
>
<!--
<a
href="/heatmaps"
class="text-[1rem] text-white ml-4 mt-4">Heatmaps</a
href="/heatmap"
class="text-[1rem] text-white ml-4 mt-4"
>Market Heatmap</a
>
-->
<a
href="/list"
class="text-[1rem] text-white ml-4 mt-4"

View File

@ -0,0 +1,24 @@
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, locals }) => {
const data = await request.json();
const { apiURL, apiKey } = locals;
const postData = { params: data?.params };
const response = await fetch(apiURL + "/heatmap", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey,
},
body: JSON.stringify(postData),
});
const output = await response.text();
return new Response(JSON.stringify(output));
};

View File

@ -1,21 +0,0 @@
export const load = async ({ locals }) => {
const getData = async () => {
const { apiURL, apiKey } = locals;
const response = await fetch(apiURL + "/heatmap", {
method: "GET",
headers: {
"Content-Type": "text/html",
"X-API-KEY": apiKey,
},
});
const output = await response.text();
return output;
};
// Make sure to return a promise
return {
getData: await getData(),
};
};

View File

@ -1,47 +1,223 @@
<script>
import Plot from "svelte-plotly.js";
<script lang="ts">
import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js";
import { Button } from "$lib/components/shadcn/button/index.js";
import SEO from "$lib/components/SEO.svelte";
// Treemap data configuration
const data = [
{
type: "treemap",
labels: [
"Eve",
"Cain",
"Seth",
"Enos",
"Noam",
"Abel",
"Awan",
"Enoch",
"Azura",
],
parents: ["", "Eve", "Eve", "Seth", "Seth", "Eve", "Eve", "Awan", "Eve"],
textinfo: "label+parent",
marker: { line: { width: 2 } },
},
];
export let data;
let rawData = data?.getData;
let iframe: HTMLIFrameElement;
let iframeLoaded = false;
let selectedFormat: "png" | "jpeg" | "svg" = "png";
let selectedTimePeriod = "1D";
let isLoaded = false;
// Layout configuration with black background
const layout = {
title: "Family Tree Treemap",
margin: { l: 0, r: 0, b: 20, t: 40 },
width: 800,
height: 600,
paper_bgcolor: "#09090b", // Sets the outer background to black
font: { color: "#FFFFFF" }, // Optional: Change text color to white for better contrast
};
async function getHeatMap() {
const postData = { params: selectedTimePeriod };
const response = await fetch("/api/heatmap", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(postData),
});
rawData = await response.json();
}
async function downloadPlot(item) {
if (item === "PNG") {
selectedFormat = "png";
} else if (item === "JPG") {
selectedFormat = "jpeg";
} else {
selectedFormat = "svg";
}
if (!iframe || !iframeLoaded) return;
try {
const iframeWindow = iframe.contentWindow;
if (!iframeWindow) return;
const Plotly = (iframeWindow as any).Plotly;
if (!Plotly) throw new Error("Plotly not found in iframe");
const plotDiv =
iframe.contentDocument?.querySelector(".plotly-graph-div");
if (!plotDiv) throw new Error("Plotly div not found");
// Configure image options based on format
const options = {
format: selectedFormat,
width: 1200,
height: 800,
};
// Get image data URL
const imageData = await Plotly.toImage(plotDiv, options);
// Create download link
const link = document.createElement("a");
link.href = imageData;
link.download = `sp500-heatmap-${selectedTimePeriod}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error("Download failed:", error);
}
}
$: {
if (selectedTimePeriod && typeof window !== "undefined") {
isLoaded = false;
getHeatMap();
isLoaded = true;
}
}
</script>
<div class="w-full min-h-screen">
<div class="treemap-container">
<Plot {data} {layout} />
</div>
</div>
<SEO
title="S&P 500 Stock Market Heatmap"
description="A stock market heatmap showing the performance of the individual stocks, sectors and industries in the S&P500."
/>
<style>
.treemap-container {
margin: 1rem;
padding: 1rem;
}
</style>
<section
class="w-full max-w-3xl sm:max-w-[1400px] overflow-hidden min-h-screen pb-20 pt-5 px-4 lg:px-3"
>
<div class="text-sm sm:text-[1rem] breadcrumbs">
<ul>
<li><a href="/" class="text-gray-300">Home</a></li>
<li class="text-gray-300">Heatmap</li>
</ul>
</div>
<div class="w-full overflow-hidden m-auto mt-5">
<div class="sm:p-0 flex justify-center w-full m-auto overflow-hidden">
<div
class="relative flex justify-center items-start overflow-hidden w-full"
>
<main class="w-full">
<div class="border-b-[2px] flex justify-between items-center gap-4">
<h1 class="mb-1 text-white text-2xl sm:text-3xl font-bold">
S&P 500 - {selectedTimePeriod} Performance
</h1>
</div>
<div class="flex flex-row items-center w-fit">
<div
class="grid grid-cols-2 sm:grid-cols-3 gap-y-3 sm:gap-y-0 gap-x-2.5 lg:grid-cols-3 w-full mt-5"
>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="border-gray-600 border bg-default sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
>
<span class="truncate text-white">Time Period</span>
<svg
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-auto h-fit max-h-72 overflow-y-auto scroller"
>
<div
class="relative sticky z-40 focus:outline-none -top-1"
tabindex="0"
role="menu"
style=""
></div>
<DropdownMenu.Group>
{#each ["1D", "1W", "1M", "3M", "6M", "1Y", "3Y"] as item}
<DropdownMenu.Item class="sm:hover:bg-primary">
<div class="flex items-center">
<button
on:click={() => (selectedTimePeriod = item)}
class="cursor-pointer text-white"
>
<span class="mr-8">{item}</span>
</button>
</div>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
builders={[builder]}
class="border-gray-600 border bg-default sm:hover:bg-primary ease-out flex flex-row justify-between items-center px-3 py-2 text-white rounded-md truncate"
>
<span class="truncate text-white">Download</span>
<svg
class="-mr-1 ml-1 h-5 w-5 xs:ml-2 inline-block"
viewBox="0 0 20 20"
fill="currentColor"
style="max-width:40px"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-auto h-fit max-h-72 overflow-y-auto scroller"
>
<div
class="relative sticky z-40 focus:outline-none -top-1"
tabindex="0"
role="menu"
style=""
></div>
<DropdownMenu.Group>
{#each ["PNG", "JPG", "SVG"] as item}
<DropdownMenu.Item class="sm:hover:bg-primary">
<div class="flex items-center">
<button
on:click={() => downloadPlot(item)}
disabled={!iframeLoaded}
class="cursor-pointer text-white"
>
<span class="mr-8">Download {item}</span>
</button>
</div>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
<div class="w-full min-h-screen bg-[#09090B] overflow-hidden">
{#if rawData}
<iframe
bind:this={iframe}
srcdoc={rawData}
class="w-full h-screen border-none"
on:load={() => (iframeLoaded = true)}
/>
{/if}
</div>
</main>
</div>
</div>
</div>
</section>

View File

@ -65,6 +65,7 @@ const pages = [
{ title: "/politicians/flow-data" },
{ title: "/analysts" },
{ title: "/analysts/top-stocks" },
{ title: "/heatmap" },
];
const website = "https://stocknear.com";

View File

@ -124,6 +124,10 @@
title: "Stock Screener",
link: "/stock-screener",
},
{
title: "Market Heatmap",
link: "/heatmap",
},
{
title: "Stock Lists",
link: "/list",