From fa939b926b97957c34792252af0eb4a81f42fb5f Mon Sep 17 00:00:00 2001 From: MuslemRahimi Date: Wed, 25 Dec 2024 12:49:39 +0100 Subject: [PATCH] update dark pool flow page --- src/lib/components/Table/DarkPoolTable.svelte | 263 +++++------ src/lib/utils.ts | 6 +- src/routes/dark-pool-flow/+page.server.ts | 44 +- src/routes/dark-pool-flow/+page.svelte | 416 ++---------------- 4 files changed, 143 insertions(+), 586 deletions(-) diff --git a/src/lib/components/Table/DarkPoolTable.svelte b/src/lib/components/Table/DarkPoolTable.svelte index 6922f602..be87e6b0 100644 --- a/src/lib/components/Table/DarkPoolTable.svelte +++ b/src/lib/components/Table/DarkPoolTable.svelte @@ -4,58 +4,52 @@ import VirtualList from "svelte-tiny-virtual-list"; import HoverStockChart from "$lib/components/HoverStockChart.svelte"; - import toast from "svelte-french-toast"; export let data; - export let optionsWatchlist; export let displayedData = []; export let filteredData = []; export let rawData = []; - let animationClass = ""; - let animationId = ""; + function formatToNewYorkTime(isoString) { + const date = new Date(isoString); - function formatTime(timeString) { - // Split the time string into components - const [hours, minutes, seconds] = timeString?.split(":").map(Number); + // Get the date components in New York time zone + const options = { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZone: "America/New_York", + hour12: false, + }; - // Determine AM or PM - const period = hours >= 12 ? "PM" : "AM"; + // Format date for New York timezone + const formatter = new Intl.DateTimeFormat("en-US", options); + const parts = formatter.formatToParts(date); - // Convert hours from 24-hour to 12-hour format - const formattedHours = hours % 12 || 12; // Converts 0 to 12 for midnight + const year = parts.find((p) => p.type === "year").value; + const day = parts.find((p) => p.type === "day").value; + const hour = parts.find((p) => p.type === "hour").value.padStart(2, "0"); + const minute = parts + .find((p) => p.type === "minute") + .value.padStart(2, "0"); + const second = parts + .find((p) => p.type === "second") + .value.padStart(2, "0"); - // Format the time string - const formattedTimeString = `${formattedHours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")} ${period}`; - - return formattedTimeString; - } - - function reformatDate(dateString) { - return ( - dateString.substring(5, 7) + - "/" + - dateString.substring(8) + - "/" + - dateString.substring(2, 4) - ); + return `${day}/${year} ${hour}:${minute}:${second}`; } let sortOrders = { - time: "none", + date: "none", ticker: "none", - expiry: "none", - dte: "none", - strike: "none", - callPut: "none", - sentiment: "none", - spot: "none", price: "none", premium: "none", - type: "none", - exec: "none", - vol: "none", - oi: "none", + assetType: "none", + volume: "none", + size: "none", }; // Generalized sorting function @@ -83,11 +77,6 @@ } const compareFunctions = { - time: (a, b) => { - const timeA = new Date("1970-01-01T" + a.time).getTime(); - const timeB = new Date("1970-01-01T" + b.time).getTime(); - return sortOrder === "asc" ? timeA - timeB : timeB - timeA; - }, ticker: (a, b) => { const tickerA = a.ticker.toUpperCase(); const tickerB = b.ticker.toUpperCase(); @@ -95,34 +84,19 @@ ? tickerA.localeCompare(tickerB) : tickerB.localeCompare(tickerA); }, - expiry: (a, b) => { - const timeA = new Date(a.date_expiration); - const timeB = new Date(b.date_expiration); + date: (a, b) => { + const timeA = new Date(a.date); + const timeB = new Date(b.date); return sortOrder === "asc" ? timeA - timeB : timeB - timeA; }, - dte: (a, b) => { - const timeA = new Date(a.date_expiration); - const timeB = new Date(b.date_expiration); - return sortOrder === "asc" ? timeA - timeB : timeB - timeA; - }, - strike: (a, b) => { - const strikeA = parseFloat(a.strike_price); - const strikeB = parseFloat(b.strike_price); - return sortOrder === "asc" ? strikeA - strikeB : strikeB - strikeA; - }, - spot: (a, b) => { - const spotA = parseFloat(a.underlying_price); - const spotB = parseFloat(b.underlying_price); - return sortOrder === "asc" ? spotA - spotB : spotB - spotA; - }, price: (a, b) => { const priceA = parseFloat(a.price); const priceB = parseFloat(b.price); return sortOrder === "asc" ? priceA - priceB : priceB - priceA; }, premium: (a, b) => { - const premiumA = parseFloat(a.cost_basis); - const premiumB = parseFloat(b.cost_basis); + const premiumA = parseFloat(a.premium); + const premiumB = parseFloat(b.premium); return sortOrder === "asc" ? premiumA - premiumB : premiumB - premiumA; }, size: (a, b) => { @@ -130,44 +104,27 @@ const volB = parseFloat(b?.size); return sortOrder === "asc" ? volA - volB : volB - volA; }, - vol: (a, b) => { + volume: (a, b) => { const volA = parseFloat(a.volume); const volB = parseFloat(b.volume); return sortOrder === "asc" ? volA - volB : volB - volA; }, - oi: (a, b) => { - const oiA = parseFloat(a.open_interest); - const oiB = parseFloat(b.open_interest); - return sortOrder === "asc" ? oiA - oiB : oiB - oiA; + dailyVolume: (a, b) => { + const volA = parseFloat(a.dailyVolumePercentage); + const volB = parseFloat(b.dailyVolumePercentage); + return sortOrder === "asc" ? volA - volB : volB - volA; }, - callPut: (a, b) => { - const callPutA = a.put_call?.toUpperCase(); - const callPutB = b.put_call?.toUpperCase(); - return sortOrder === "asc" - ? callPutA.localeCompare(callPutB) - : callPutB.localeCompare(callPutA); + avgVolume: (a, b) => { + const volA = parseFloat(a.avgVolumePercentage); + const volB = parseFloat(b.avgVolumePercentage); + return sortOrder === "asc" ? volA - volB : volB - volA; }, - sentiment: (a, b) => { - const sentimentOrder = { BULLISH: 1, NEUTRAL: 2, BEARISH: 3 }; - const sentimentA = sentimentOrder[a?.sentiment?.toUpperCase()] || 4; - const sentimentB = sentimentOrder[b?.sentiment?.toUpperCase()] || 4; - return sortOrder === "asc" - ? sentimentA - sentimentB - : sentimentB - sentimentA; - }, - type: (a, b) => { + assetType: (a, b) => { const typeOrder = { SWEEP: 1, TRADE: 2 }; - const typeA = typeOrder[a.option_activity_type?.toUpperCase()] || 3; - const typeB = typeOrder[b.option_activity_type?.toUpperCase()] || 3; + const typeA = typeOrder[a.assetType?.toUpperCase()] || 3; + const typeB = typeOrder[b.assetType?.toUpperCase()] || 3; return sortOrder === "asc" ? typeA - typeB : typeB - typeA; }, - exec: (a, b) => { - const tickerA = a?.execution_estimate?.toUpperCase(); - const tickerB = b?.execution_estimate?.toUpperCase(); - return sortOrder === "asc" - ? tickerA.localeCompare(tickerB) - : tickerB.localeCompare(tickerA); - }, }; // Sort using the appropriate comparison function @@ -183,15 +140,18 @@ itemCount={displayedData.length} itemSize={40} > -
+
sortData("time")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + on:click={() => sortData("date")} + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > - Time + Date
sortData("ticker")} - class="td cursor-pointer select-none bg-[#1E222D] font-bold text-slate-300 text-xs text-start uppercase" + class="td cursor-pointer select-none bg-[#121217] font-bold text-slate-300 text-xs text-start uppercase" > Symbol sortData("price")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Price
sortData("premium")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Premium sortData("size")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Size
sortData("vol")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + on:click={() => sortData("volume")} + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Volume
sortData("vol")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + on:click={() => sortData("dailyVolume")} + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > % Daily Volume
sortData("vol")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + on:click={() => sortData("avgVolume")} + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > % 30D Volume
sortData("vol")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + on:click={() => sortData("sector")} + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Sector sortData("vol")} - class="td cursor-pointer select-none bg-[#1E222D] text-slate-300 font-bold text-xs text-start uppercase" + class="td cursor-pointer select-none bg-[#121217] text-slate-300 font-bold text-xs text-start uppercase" > Issue Type
- {reformatDate(displayedData[index]?.date_expiration)} + {formatToNewYorkTime(displayedData[index]?.date)}
@@ -448,7 +410,7 @@ style="justify-content: center;" class="td text-sm sm:text-[1rem] text-start text-white" > - {@html abbreviateNumber(displayedData[index]?.cost_basis, true, true)} + {@html abbreviateNumber(displayedData[index]?.premium, true, true)}
- {new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(displayedData[index]?.volume)} + {@html abbreviateNumber(displayedData[index]?.volume, false, true)}
- {new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(displayedData[index]?.volume)} + {displayedData[index]?.dailyVolumePercentage > 0.01 + ? displayedData[index]?.dailyVolumePercentage?.toFixed(2) + "%" + : "< 0.01%"}
- {new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(displayedData[index]?.volume)} + {displayedData[index]?.avgVolume > 0.01 + ? displayedData[index]?.avgVolume?.toFixed(2) + "%" + : "< 0.01%"}
- Healthcare + {displayedData[index]?.sector?.length > 13 + ? displayedData[index]?.sector?.slice(0, 13) + "..." + : displayedData[index]?.sector}
- Stock + {displayedData[index]?.assetType}
@@ -537,7 +496,6 @@ .th { display: none; font-weight: 700; - background-color: #09090b; } .th > .td { @@ -571,27 +529,4 @@ min-width: 0px; white-space: nowrap; } - - .heartbeat { - animation: heartbeat-animation 0.3s; - animation-timing-function: ease-in-out; - } - - @keyframes heartbeat-animation { - 0% { - transform: rotate(0deg) scale(0.95); - } - 25% { - transform: rotate(10deg) scale(1.05); - } - 50% { - transform: rotate(0deg) scale(1.2); - } - 75% { - transform: rotate(-10deg) scale(1.05); - } - 100% { - transform: rotate(0deg) scale(0.95); - } - } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d71120c7..284ad872 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -657,11 +657,11 @@ export function abbreviateNumber(number, addDollarSign = false, color = false) { if (color) { if (suffix === "K") { - suffix = 'K'; + suffix = 'K'; } else if (suffix === "M") { - suffix = 'M'; + suffix = 'M'; } else if (suffix === "B") { - suffix = 'B'; + suffix = 'B'; } } diff --git a/src/routes/dark-pool-flow/+page.server.ts b/src/routes/dark-pool-flow/+page.server.ts index 60709bf0..48127555 100644 --- a/src/routes/dark-pool-flow/+page.server.ts +++ b/src/routes/dark-pool-flow/+page.server.ts @@ -1,9 +1,9 @@ -export const load = async ({ locals, cookies }) => { - const { apiURL, apiKey, pb, user } = locals; +export const load = async ({ locals }) => { + const { apiURL, apiKey } = locals; - const getOptionsFlowFeed = async () => { + const getFlowData = async () => { // make the POST request to the endpoint - const response = await fetch(apiURL + "/options-flow-feed", { + const response = await fetch(apiURL + "/dark-pool-flow-feed", { method: "GET", headers: { "Content-Type": "application/json", @@ -15,42 +15,8 @@ export const load = async ({ locals, cookies }) => { return output; }; - const getPredefinedCookieRuleOfList = async () => { - // make the POST request to the endpoint - const ruleOfList = cookies.get("options-flow-filter-cookie") ?? []; - const output = - ruleOfList?.length !== 0 - ? JSON.parse(ruleOfList) - : [ - { name: "cost_basis", value: "any" }, - { name: "date_expiration", value: "any" }, - ]; - return output; - }; - - const getOptionsWatchlist = async () => { - let output; - try { - output = ( - await pb?.collection("optionsWatchlist").getFullList({ - filter: `user="${user?.id}"`, - }) - )?.at(0); - if (output === undefined) { - output = { optionsId: [] }; - } - } catch (e) { - //console.log(e) - output = { optionsId: [] }; - } - return output; - }; - - // Make sure to return a promise return { - getOptionsFlowFeed: await getOptionsFlowFeed(), - getPredefinedCookieRuleOfList: await getPredefinedCookieRuleOfList(), - getOptionsWatchlist: await getOptionsWatchlist(), + getFlowData: await getFlowData(), }; }; diff --git a/src/routes/dark-pool-flow/+page.svelte b/src/routes/dark-pool-flow/+page.svelte index d9af059f..41ba7479 100644 --- a/src/routes/dark-pool-flow/+page.svelte +++ b/src/routes/dark-pool-flow/+page.svelte @@ -14,7 +14,6 @@ import * as DropdownMenu from "$lib/components/shadcn/dropdown-menu/index.js"; import * as Popover from "$lib/components/shadcn/popover/index.js"; import { Button } from "$lib/components/shadcn/button/index.js"; - import { Calendar } from "$lib/components/shadcn/calendar/index.js"; import CalendarIcon from "lucide-svelte/icons/calendar"; import { page } from "$app/stores"; @@ -25,8 +24,6 @@ export let data; let shouldLoadWorker = writable(false); - let optionsWatchlist = data?.getOptionsWatchlist; - let ruleOfList = data?.getPredefinedCookieRuleOfList || []; let displayRules = []; @@ -62,25 +59,7 @@ defaultCondition: "over", defaultValue: "any", }, - open_interest: { - label: "Open Interest", - step: ["100K", "10K", "1K"], - defaultCondition: "over", - defaultValue: "any", - }, - volumeOIRatio: { - label: "Volume / Open Interest", - step: ["100%", "80%", "60%", "50%", "30%", "15%", "10%", "5%"], - defaultCondition: "over", - defaultValue: "any", - }, - sizeOIRatio: { - label: "Size / Open Interest", - step: ["100%", "80%", "60%", "50%", "30%", "15%", "10%", "5%"], - defaultCondition: "over", - defaultValue: "any", - }, - cost_basis: { + premium: { label: "Premium", step: [ "10M", @@ -98,43 +77,12 @@ defaultCondition: "over", defaultValue: "any", }, - moneyness: { - label: "Moneyness", - step: ["ITM", "OTM"], - defaultValue: "any", - }, - flowType: { - label: "Flow Type", - step: ["Repeated Flow"], - defaultValue: "any", - }, - put_call: { - label: "Contract Type", - step: ["Calls", "Puts"], - defaultValue: "any", - }, - sentiment: { - label: "Sentiment", - step: ["Bullish", "Neutral", "Bearish"], - defaultValue: "any", - }, - execution_estimate: { - label: "Execution", - step: ["Above Ask", "Below Bid", "At Ask", "At Bid", "At Midpoint"], - defaultValue: "any", - }, option_activity_type: { label: "Option Type", step: ["Sweep", "Trade"], defaultValue: "any", }, - date_expiration: { - label: "Date Expiration", - step: ["250", "180", "100", "80", "60", "50", "30", "20", "10", "5", "0"], - defaultCondition: "over", - defaultValue: "any", - }, - underlying_type: { + assetType: { label: "Asset Type", step: ["Stock", "ETF"], defaultValue: "any", @@ -148,7 +96,7 @@ "sentiment", "execution_estimate", "option_activity_type", - "underlying_type", + "assetType", ]; // Generate allRows from allRules @@ -199,7 +147,7 @@ ruleOfList?.some((rule) => rule.name === row.rule), ); shouldLoadWorker.set(true); - await saveCookieRuleOfList(); + //await saveCookieRuleOfList(); } async function handleResetAll() { @@ -217,7 +165,7 @@ ruleOfList.some((rule) => rule.name === row.rule), ); displayedData = rawData; - await saveCookieRuleOfList(); + //await saveCookieRuleOfList(); } function changeRule(state: string) { @@ -249,7 +197,7 @@ case "underlying_type": newRule = { name: ruleName, - value: Array.isArray(valueMappings[ruleName]) + value: Array?.isArray(valueMappings[ruleName]) ? valueMappings[ruleName] : [valueMappings[ruleName]], }; // Ensure value is an array @@ -310,7 +258,6 @@ filteredData = event.data?.filteredData ?? []; displayedData = filteredData; console.log("handle Message"); - calculateStats(displayedData); //console.log(displayedData) }; @@ -435,7 +382,7 @@ // Trigger worker load and save cookie shouldLoadWorker.set(true); - await saveCookieRuleOfList(); + //await saveCookieRuleOfList(); } async function stepSizeValue(value, condition) { @@ -467,12 +414,8 @@ } } - const currentTime = new Date( - new Date().toLocaleString("en-US", { timeZone: "America/New_York" }), - )?.getTime(); - const nyseDate = new Date( - data?.getOptionsFlowFeed?.at(0)?.date ?? null, + data?.getFlowData?.at(0)?.date ?? null, )?.toLocaleString("en-US", { month: "short", day: "numeric", @@ -480,7 +423,7 @@ timeZone: "Europe/Berlin", }); - let rawData = data?.getOptionsFlowFeed?.filter((item) => + let rawData = data?.getFlowData?.filter((item) => Object?.values(item)?.every( (value) => value !== null && @@ -491,9 +434,6 @@ )), ), ); - rawData?.forEach((item) => { - item.dte = daysLeft(item?.date_expiration); - }); let displayedData = []; @@ -517,7 +457,7 @@ mode = !mode; if (mode === true && selectedDate !== undefined) { selectedDate = undefined; - rawData = data?.getOptionsFlowFeed; + rawData = data?.getFlowData; displayedData = [...rawData]; shouldLoadWorker.set(true); } @@ -527,17 +467,12 @@ }); } } - + /* async function websocketRealtimeData() { newData = []; try { socket = new WebSocket(data?.wsURL + "/options-flow-reader"); - /* - socket.addEventListener("open", () => { - const ids = rawData.map(item => item.id); - sendMessage(JSON.stringify({ ids })); - }); - */ + socket.addEventListener("message", (event) => { const totalVolume = displayCallVolume + displayPutVolume; @@ -567,11 +502,7 @@ } } - /* - if (previousCallVolume !== displayCallVolume && !muted && audio) { - audio?.play(); - } - */ + } catch (e) { console.error("Error processing WebSocket message:", e); } @@ -603,16 +534,7 @@ setTimeout(() => websocketRealtimeData(), 400); } } - - function daysLeft(targetDate) { - const targetTime = new Date(targetDate).getTime(); - const difference = targetTime - currentTime; - - const millisecondsPerDay = 1000 * 60 * 60 * 24; - const daysLeft = Math?.ceil(difference / millisecondsPerDay); - - return daysLeft; - } + */ async function saveCookieRuleOfList() { const postData = { @@ -628,9 +550,11 @@ }); // make a POST request to the server with the FormData object } + /* $: if ($isOpen) { websocketRealtimeData(); } + */ onMount(async () => { if (filterQuery?.length > 0) { @@ -647,7 +571,6 @@ audio = new Audio(notifySound); displayedData = rawData; - calculateStats(rawData); if (!syncWorker) { const SyncWorker = await import("./workers/filterWorker?worker"); @@ -678,65 +601,7 @@ } }); - function calculateStats(data) { - const { - callVolumeSum, - putVolumeSum, - bullishCount, - bearishCount, - neutralCount, - } = data?.reduce( - (acc, item) => { - const volume = parseInt(item?.volume); - - if (item?.put_call === "Calls") { - acc.callVolumeSum += volume; - } else if (item?.put_call === "Puts") { - acc.putVolumeSum += volume; - } - - if (item?.sentiment === "Bullish") { - acc.bullishCount += 1; - } else if (item?.sentiment === "Bearish") { - acc.bearishCount += 1; - } else if (item?.sentiment === "Neutral") { - acc.neutralCount += 1; - } - - return acc; - }, - { - callVolumeSum: 0, - putVolumeSum: 0, - bullishCount: 0, - bearishCount: 0, - neutralCount: 0, - }, - ); - - if (bullishCount > bearishCount) { - flowSentiment = "Bullish"; - } else if (bullishCount < bearishCount) { - flowSentiment = "Bearish"; - } else if (neutralCount > bearishCount && neutralCount > bullishCount) { - flowSentiment = "Neutral"; - } else { - flowSentiment = "-"; - } - - putCallRatio = callVolumeSum !== 0 ? putVolumeSum / callVolumeSum : 0; - - callPercentage = - callVolumeSum + putVolumeSum !== 0 - ? Math.floor((callVolumeSum / (callVolumeSum + putVolumeSum)) * 100) - : 0; - putPercentage = - callVolumeSum + putVolumeSum !== 0 ? 100 - callPercentage : 0; - - displayCallVolume = callVolumeSum; - displayPutVolume = putVolumeSum; - } - + /* const getHistoricalFlow = async () => { // Create a delay using setTimeout wrapped in a Promise if (data?.user?.tier === "Pro") { @@ -747,7 +612,6 @@ ruleOfList.some((rule) => rule.name === row.rule), ); displayedData = []; - calculateStats(displayedData); await new Promise((resolve) => setTimeout(resolve, 300)); @@ -769,12 +633,7 @@ }); rawData = await response?.json(); - if (rawData?.length !== 0) { - rawData?.forEach((item) => { - item.dte = daysLeft(item?.date_expiration); - }); - shouldLoadWorker.set(true); - } + shouldLoadWorker.set(true); } catch (error) { console.error("Error fetching historical flow:", error); rawData = []; @@ -787,6 +646,7 @@ }); } }; + */ function handleInput(event) { filterQuery = event.target.value; @@ -863,7 +723,7 @@
-
-
- Flow Sentiment - {flowSentiment} -
-
- - -
-
- Put/Call - - {putCallRatio?.toFixed(3)} - -
- -
- - - - - - = 1 - ? 0 - : 100 - (putCallRatio * 100)?.toFixed(2)} - > - - - -
- {putCallRatio?.toFixed(2)} -
-
- -
- - -
-
- Call Flow - - {new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(displayCallVolume)} - -
- -
- - - - - - - - - -
- {callPercentage}% -
-
- -
- - -
-
- Put Flow - - {new Intl.NumberFormat("en", { - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }).format(displayPutVolume)} - -
- -
- - - - - - - - - -
- {putPercentage}% -
-
- -
-
-
-
{#if displayedData?.length !== 0} -
- +
+
{:else}