frontend/src/routes/heatmaps/+page.svelte
2024-11-07 21:09:34 +01:00

627 lines
20 KiB
Svelte

<script lang="ts">
import { screenWidth, numberOfUnreadNotification } from "$lib/store";
import { goto } from "$app/navigation";
import { Chart } from "svelte-echarts";
import { init, use } from "echarts/core";
import { TreemapChart } from "echarts/charts";
import { GridComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
use([TreemapChart, GridComponent, CanvasRenderer]);
export let data;
let cloudFrontUrl = import.meta.env.VITE_IMAGE_URL;
let displayIndex = "S&P500";
let lowestAvg = Infinity;
let lowestAvgCategory = "";
let highestAvg = -Infinity;
let highestAvgCategory = "";
let rawData;
let isLoaded = false;
let options;
const visualMin = -100;
const visualMax = 100;
/*
To-do: Add export to save as png or jpg.
async function exportTreemap() {
// Specify the desired width and height for the canvas
const width = 1000 ;
const height = 1000/;
// Create the canvas element with the specified width and height
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const chart = echarts.init(canvas);
chart.setOption(options);
const link = document.createElement('a');
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
link.href = url;
link.download = 'treemap.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, 'image/png');
}
*/
function getLevelOption() {
return [
{
itemStyle: {
borderColor: "#2D313C",
borderWidth: 0,
gapWidth: 1,
},
upperLabel: {
show: false,
},
},
{
itemStyle: {
borderColor: "#2D313C",
borderWidth: 5,
gapWidth: 1,
},
emphasis: {
itemStyle: {
borderColor: "#2D313C",
},
},
},
{
color: ["#942e38", "#aaa", "#269f3c"],
colorMappingBy: "value",
itemStyle: {
gapWidth: 1,
},
},
{
colorSaturation: [0.5, 0.9],
itemStyle: {
borderWidth: 5,
gapWidth: 1,
borderColorSaturation: 0.6,
},
},
];
}
function plotData(fontSize) {
rawData.forEach((category) => {
if (category?.children && category.children.length > 0) {
let sum = category.children.reduce(
(acc, stock) => acc + (stock?.changesPercentage || 0),
0,
);
let avg = sum / category.children.length;
if (avg < lowestAvg) {
lowestAvg = avg;
lowestAvgCategory = category.name;
}
if (avg > highestAvg) {
highestAvg = avg;
highestAvgCategory = category.name;
}
}
});
options = {
silent: true,
tooltip: {},
grid: {
left: "0%",
right: "0%",
top: "0%",
bottom: "0%",
containLabel: true,
},
series: [
{
breadcrumb: { show: false },
type: "treemap",
roam: false,
width: "100%",
height: "100%",
visualMin: visualMin,
visualMax: visualMax,
visualDimension: 2,
label: {
show: true,
textStyle: {
color: "#fff",
fontSize: fontSize,
fontWeight: "bold",
},
formatter: function (params) {
var changesPercentage = params.data.changesPercentage || 0; // Access changesPercentage from params.data
return params.name + "\n\n" + changesPercentage + "%";
},
},
upperLabel: {
show: true,
textStyle: {
color: "#fff",
fontSize: fontSize,
},
formatter: function (params) {
var sumChangesPercentage = 0;
if (params.data.children && params.data.children.length > 0) {
sumChangesPercentage = params.data.children.reduce(
(acc, child) => acc + (child.changesPercentage || 0),
0,
);
}
return params.name;
},
height: 40,
},
itemStyle: {
borderColor: "#2D313C",
color: function (params) {
return params.data.changesPercentage >= 0 ? "#30954F" : "#FF281E"; // Access changesPercentage from params.data
},
},
levels: getLevelOption(),
data: rawData.map((item) => ({
...item,
children: item.children.map((child) => ({
...child,
itemStyle: {
...child.itemStyle,
color: child.changesPercentage < 0 ? "#7F4650" : "#396550",
},
})),
})),
},
],
};
}
function changeIndex(indexName) {
displayIndex = indexName;
}
function sectorSelector(sector) {
let path;
switch (sector) {
case "Financials":
path = "financial-sector";
break;
case "Healthcare":
path = "healthcare-sector";
break;
case "Information Technology":
path = "technology-sector";
break;
case "Technology":
path = "technology-sector";
break;
case "Financial Services":
path = "financial-sector";
break;
case "Industrials":
path = "industrials-sector";
break;
case "Energy":
path = "energy-sector";
break;
case "Utilities":
path = "utilities-sector";
break;
case "Consumer Cyclical":
path = "consumer-cyclical-sector";
break;
case "Real Estate":
path = "real-estate-sector";
break;
case "Basic Materials":
path = "basic-materials-sector";
break;
case "Communication Services":
path = "communication-services-sector";
break;
case "Consumer Defensive":
path = "consumer-defensive-sector";
break;
default:
// Handle default case if needed
break;
}
goto("list/" + path);
}
$: {
if (typeof window !== "undefined" && displayIndex) {
isLoaded = false;
if (displayIndex === "S&P500") {
rawData = data?.getSP500HeatMap;
} else if (displayIndex === "Dow Jones") {
rawData = data?.getDowJonesHeatMap;
} else if (displayIndex === "Nasdaq") {
rawData = data?.getNasdaqHeatMap;
}
const fontSize = $screenWidth < 640 ? 12 : 16;
plotData(fontSize);
isLoaded = true;
}
}
let charNumber = 40;
$: {
if ($screenWidth < 640) {
charNumber = 20;
} else {
charNumber = 40;
}
}
</script>
<!-- HEADER FOR BETTER SEO -->
<svelte:head>
<title>
{$numberOfUnreadNotification > 0 ? `(${$numberOfUnreadNotification})` : ""} Market
Heatmaps · stocknear</title
>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta
name="description"
content="Stock Heatmaps of S&P500, Nasdaq and Dow Jones to see percentage changes of the return for different time periods"
/>
<!-- Other meta tags -->
<meta property="og:title" content="Market Heatmaps · stocknear" />
<meta
property="og:description"
content="Stock Heatmaps of S&P500, Nasdaq and Dow Jones to see percentage changes of the return for different time periods"
/>
<meta property="og:type" content="website" />
<!-- Add more Open Graph meta tags as needed -->
<!-- Twitter specific meta tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Market Heatmaps · stocknear" />
<meta
name="twitter:description"
content="Stock Heatmaps of S&P500, Nasdaq and Dow Jones to see percentage changes of the return for different time periods"
/>
<!-- Add more Twitter meta tags as needed -->
</svelte:head>
<section
class="w-full max-w-3xl sm:max-w-screen-xl overflow-hidden min-h-screen pt-5 pb-40"
>
<div
class="w-full m-auto sm:bg-[#27272A] sm:rounded-xl h-auto pl-10 pr-10 pt-5 sm:pb-10 sm:pt-10 mt-3 mb-8"
>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-10">
<!-- Start Column -->
<div>
<div class="flex flex-row justify-center items-center">
<h1
class="text-3xl sm:text-4xl text-white text-center font-bold mb-5"
>
Market Heatmaps
</h1>
</div>
<span
class="hidden sm:block text-white text-md font-medium text-center flex justify-center items-center"
>
Latest market changes to never miss another bullish or bearish rally.
</span>
</div>
<!-- End Column -->
<!-- Start Column -->
<div class="hidden sm:block relative m-auto mb-5 mt-5 sm:mb-0 sm:mt-0">
<svg
class="w-40 -my-5"
viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="5" result="glow" />
<feMerge>
<feMergeNode in="glow" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<path
fill="#1E40AF"
d="M57.6,-58.7C72.7,-42.6,81.5,-21.3,82,0.5C82.5,22.3,74.7,44.6,59.7,60.1C44.6,75.6,22.3,84.3,0,84.3C-22.3,84.2,-44.6,75.5,-61.1,60.1C-77.6,44.6,-88.3,22.3,-87.6,0.7C-86.9,-20.8,-74.7,-41.6,-58.2,-57.7C-41.6,-73.8,-20.8,-85.2,0.2,-85.4C21.3,-85.6,42.6,-74.7,57.6,-58.7Z"
transform="translate(100 100)"
filter="url(#glow)"
/>
</svg>
<div class="z-1 absolute top-0 left-8">
<img
class="w-32 h-fit"
src={cloudFrontUrl + "/assets/heatmaps_logo.png"}
alt="logo"
loading="lazy"
/>
</div>
</div>
<!-- End Column -->
</div>
</div>
<body class="w-full overflow-hidden m-auto">
{#if isLoaded}
<section class="w-full overflow-hidden m-auto">
<div class="p-0 flex justify-center w-full m-auto overflow-hidden">
<div
class="relative flex justify-center items-center overflow-hidden w-full"
>
<main class="w-full">
<div
class="w-full text-center sm:text-start sm:flex sm:flex-row sm:items-center m-auto text-gray-100 border border-gray-800 sm:rounded-lg h-auto p-5 mb-4"
>
<svg
class="w-5 h-5 inline-block sm:mr-2 flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
><path
fill="#fff"
d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m-4 48a12 12 0 1 1-12 12a12 12 0 0 1 12-12m12 112a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 0 16"
/></svg
>
<span>
Today, <label
on:click={() => sectorSelector(lowestAvgCategory)}
class="cursor-pointer text-blue-400 sm:hover:text-white"
>{lowestAvgCategory}</label
>
took the lead as the {displayIndex} largest loser, marking a average
return of
<span class="text-white font-medium"
>{lowestAvg?.toFixed(2)}%</span
>, while
<label
on:click={() => sectorSelector(highestAvgCategory)}
class="cursor-pointer text-blue-400 sm:hover:text-white"
>{highestAvgCategory}</label
>
surged ahead as the top performer with an impressive average return
of
<span class="text-white font-medium"
>{highestAvg?.toFixed(2)}%</span
>.
</span>
</div>
<div class="w-full pt-3">
<div class="relative right-0 bg-[#09090B]">
<ul
class="relative w-fit flex flex-wrap px-2 list-none rounded-[3px]"
>
<li
class="px-3 py-1.5 flex-auto text-center bg-[#2E3238] rounded-[3px]"
>
<label
for="indexModal"
class="cursor-pointer border flex items-center justify-center w-full px-0 py-1 mb-0 border-0 rounded-[3px] bg-inherit"
>
<span class="text-sm sm:text-md text-white ml-1">
Market Index:
</span>
<span
class="text-sm sm:text-md font-medium text-white ml-2 mr-2"
>
{displayIndex}
</span>
<svg
class="ml-auto sm:ml-0 mr-5 h-4 w-4 inline-block transform transition-transform mr-2 rotate-180"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
><path
fill="#fff"
d="m488.832 344.32l-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872l319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"
/></svg
>
</label>
</li>
<!--
<li class="pl-3 py-1.5 flex-auto text-center bg-[#2E3238] rounded-[3px]">
<label for="exportDataModal" class="cursor-pointer border flex items-center justify-center w-full px-0 py-1 mb-0 border-0 rounded-[3px] bg-inherit">
<span class="text-sm font-medium text-white ml-3">
Export
</span>
<svg class="ml-auto mr-5 h-4 w-4 inline-block transform transition-transform mr-2 rotate-180" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#fff" d="m488.832 344.32l-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872l319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"/></svg>
</label>
</li>
-->
</ul>
</div>
</div>
<div class="app w-full h-full mt-10">
<Chart
id="treemap"
{init}
{options}
class="chart w-full h-full"
/>
</div>
</main>
</div>
</div>
</section>
{:else}
<div class="flex justify-center items-center h-80">
<div class="relative">
<label
class="bg-[#09090B] rounded-xl h-14 w-14 flex justify-center items-center absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
>
<span class="loading loading-spinner loading-md text-gray-400"
></span>
</label>
</div>
</div>
{/if}
</body>
</section>
<!--Start IndexModal -->
<input type="checkbox" id="indexModal" class="modal-toggle" />
<dialog id="indexModal" class="modal modal-bottom sm:modal-middle">
<label
id="indexModal"
for="indexModal"
class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.02] sm:bg-[#000] sm:bg-opacity-[0.5]"
></label>
<div
class="modal-box w-full bg-[#000] sm:bg-[#09090B] sm:border sm:border-slate-800"
>
<label
for="indexModal"
class="cursor-pointer absolute right-5 top-2 bg-[#000] sm:bg-[#09090B] text-[1.8rem] text-white"
>
</label>
<div class="text-white">
<h3 class="font-medium text-lg sm:text-xl mb-10">Market Index</h3>
<div
class="flex flex-col items-center w-full max-w-3xl bg-[#000] sm:bg-[#09090B]"
>
<label
for="indexModal"
on:click={() => changeIndex("S&P500")}
class="cursor-pointer w-full flex flex-row justify-start items-center mb-5"
>
<div
class="flex flex-row items-center w-full bg-[#09090B] bg-opacity-[0.7] sm:bg-opacity-[1.0] sm:bg-[#303030] p-3 rounded-lg {displayIndex ===
'S&P500'
? 'ring-2 ring-[#04E000]'
: ''}"
>
<span class="ml-1 text-white font-medium mr-auto"> S&P500 </span>
</div>
</label>
<label
for="indexModal"
on:click={() => changeIndex("Dow Jones")}
class="cursor-pointer w-full flex flex-row justify-start items-center mb-5"
>
<div
class="flex flex-row items-center w-full bg-[#09090B] bg-opacity-[0.7] sm:bg-opacity-[1.0] sm:bg-[#303030] p-3 rounded-lg {displayIndex ===
'Dow Jones'
? 'ring-2 ring-[#04E000]'
: ''}"
>
<span class="ml-1 text-white font-medium mr-auto"> Dow Jones </span>
</div>
</label>
<label
for="indexModal"
on:click={() => changeIndex("Nasdaq")}
class="cursor-pointer w-full flex flex-row justify-start items-center mb-5"
>
<div
class="flex flex-row items-center w-full bg-[#09090B] bg-opacity-[0.7] sm:bg-opacity-[1.0] sm:bg-[#303030] p-3 rounded-lg {displayIndex ===
'Nasdaq'
? 'ring-2 ring-[#04E000]'
: ''}"
>
<span class="ml-1 text-white font-medium mr-auto"> Nasdaq </span>
</div>
</label>
</div>
</div>
</div>
</dialog>
<!--End Index Modal-->
<!--Start Export -->
<input type="checkbox" id="exportDataModal" class="modal-toggle" />
<dialog id="exportDataModal" class="modal modal-bottom sm:modal-middle">
<label
id="exportDataModal"
for="exportDataModal"
class="cursor-pointer modal-backdrop bg-[#fff] bg-opacity-[0.02] sm:bg-[#000] sm:bg-opacity-[0.5]"
></label>
<div
class="modal-box w-full bg-[#000] sm:bg-[#09090B] sm:border sm:border-slate-800"
>
<label
for="exportDataModal"
class="cursor-pointer absolute right-5 top-2 bg-[#000] sm:bg-[#09090B] text-[1.8rem] text-white"
>
</label>
<div class="text-white">
<h3 class="font-medium text-lg sm:text-xl mb-10">Export</h3>
<div
class="flex flex-col items-center w-full max-w-3xl bg-[#000] sm:bg-[#09090B]"
>
<label
for="exportDataModal"
on:click={() => exportTreemap()}
class="cursor-pointer w-full flex flex-row justify-start items-center mb-5"
>
<div
class="flex flex-row items-center w-full bg-[#09090B] bg-opacity-[0.7] sm:bg-opacity-[1.0] sm:bg-[#303030] p-3 rounded-lg ring-2 ring-[#04E000]"
>
<span class="ml-1 text-white font-medium mr-auto">
Save as PNG
</span>
</div>
</label>
<label
for="exportDataModal"
class="cursor-pointer w-full flex flex-row justify-start items-center mb-5"
>
<div
class="flex flex-row items-center w-full bg-[#09090B] bg-opacity-[0.7] sm:bg-opacity-[1.0] sm:bg-[#303030] p-3 rounded-lg ring-2 ring-[#04E000]"
>
<span class="ml-1 text-white font-medium mr-auto">
Save as JPG
</span>
</div>
</label>
</div>
</div>
</div>
</dialog>
<!--End Export-->
<style>
.app {
height: 1200px;
width: 100%;
}
.chart {
width: 100%;
}
</style>