frontend/src/lib/components/AnalystEstimate.svelte
2025-01-09 12:11:13 +01:00

1219 lines
41 KiB
Svelte

<script lang="ts">
import { analystEstimateComponent, stockTicker } from "$lib/store";
import { abbreviateNumber, computeGrowthSingleList } from "$lib/utils";
import EstimationGraph from "$lib/components/EstimationGraph.svelte";
import Lazy from "svelte-lazy";
export let data;
let analystEstimateList = [];
let isLoaded = false;
let xData = [];
let optionsRevenue = null;
let optionsEPS = null;
let optionsNetIncome = null;
let optionsEbitda = null;
let optionsRevenueGrowth = null;
let optionsEPSGrowth = null;
let optionsNetIncomeGrowth = null;
let optionsEbitdaGrowth = null;
let revenueDateList = [];
let avgRevenueList = [];
let lowRevenueList = [];
let highRevenueList = [];
let epsDateList = [];
let avgEPSList = [];
let lowEPSList = [];
let highEPSList = [];
let netIncomeDateList = [];
let avgNetIncomeList = [];
let lowNetIncomeList = [];
let highNetIncomeList = [];
let ebitdaDateList = [];
let avgEbitdaList = [];
let lowEbitdaList = [];
let highEbitdaList = [];
let revenueAvgGrowthList = [];
let epsAvgGrowthList = [];
let netIncomeAvgGrowthList = [];
let ebitdaAvgGrowthList = [];
function fillMissingDates(dates, highGrowthList) {
// Get the current year
const currentYear = new Date().getFullYear();
const currentFiscalYear = `FY${currentYear % 100}`; // Get the current fiscal year (e.g., 2024 => FY24)
// Create a map from the highGrowthList for quick lookup
const highGrowthMap = new Map(
highGrowthList?.map((item) => [`FY${item.FY}`, item]),
);
// Generate the complete list based on the dates array
return dates?.map((date) => {
// Check if the date is the current fiscal year or it exists in the map
const data = highGrowthMap?.get(date) || {
FY: date?.slice(2),
val: null,
growth: null,
};
// If the fiscal year is the current one, set val and growth to null
if (date === currentFiscalYear) {
data.val = null;
data.growth = null;
}
return data;
});
}
function computeGrowthList(tableActualRevenue, tableForecastRevenue) {
return tableActualRevenue?.map((item, index) => {
const currentFY = item?.FY;
// If it's the first item or the list is empty, return null growth
if (index === 0 || tableActualRevenue.length === 0) {
return { FY: currentFY, growth: null };
}
// If actual value is null, compute growth based on forecast values
if (item?.val === null) {
const prevForecastVal = tableForecastRevenue[index - 1]?.val ?? 0;
const currentForecastVal = tableForecastRevenue[index]?.val ?? 0;
if (prevForecastVal === 0 || currentForecastVal === 0) {
return { FY: currentFY, growth: null };
}
const forecastGrowth =
((currentForecastVal - prevForecastVal) / Math.abs(prevForecastVal)) *
100;
return {
FY: currentFY,
growth:
forecastGrowth !== 0 ? Number(forecastGrowth?.toFixed(2)) : null,
};
}
// Compute actual growth for non-null actual values
const prevActualVal = tableActualRevenue[index - 1]?.val ?? 0;
const currentActualVal = item?.val ?? 0;
if (prevActualVal === 0 || currentActualVal === 0) {
return { FY: currentFY, growth: null };
}
const actualGrowth =
((currentActualVal - prevActualVal) / Math.abs(prevActualVal)) * 100;
return {
FY: currentFY,
growth: actualGrowth !== 0 ? Number(actualGrowth.toFixed(2)) : null,
};
});
}
function findIndex(data) {
const currentYear = new Date().getFullYear();
// Find the index where the item's date is greater than or equal to the current year and revenue is null
let index = data.findIndex(
(item) => item.date >= currentYear && item?.revenue === null,
);
// Check if there is any item for the current year with non-null revenue
const hasNonNullRevenue = data?.some(
(item) => item.date === currentYear && item.revenue !== null,
);
// Add +1 to the index if the condition is met
return index !== -1 && hasNonNullRevenue ? index + 1 : index;
}
let tableActualRevenue = [];
let tableForecastRevenue = [];
let tableCombinedRevenue = [];
let tableActualEPS = [];
let tableForecastEPS = [];
let tableCombinedEPS = [];
let tableActualNetIncome = [];
let tableCombinedNetIncome = [];
let tableForecastNetIncome = [];
let tableActualEbitda = [];
let tableCombinedEbitda = [];
let tableForecastEbitda = [];
function getPlotOptions(dataType: string) {
let dates = [];
let valueList = [];
let avgList = [];
let lowList = [];
let highList = [];
let filteredData =
analystEstimateList?.filter((item) => item.date >= 2019) ?? [];
const stopIndex = findIndex(filteredData);
if (filteredData) {
filteredData.forEach((item, index) => {
const date = item.date?.toString().slice(-2);
const isAfterStartIndex = stopIndex <= index + 1;
dates.push(`FY${date}`);
switch (dataType) {
case "Revenue":
valueList.push(isAfterStartIndex ? null : item.revenue);
avgList.push(isAfterStartIndex ? item?.estimatedRevenueAvg : null);
lowList.push(isAfterStartIndex ? item?.estimatedRevenueLow : null);
highList.push(
isAfterStartIndex ? item?.estimatedRevenueHigh : null,
);
break;
case "EPS":
valueList.push(isAfterStartIndex ? null : item.eps);
avgList.push(isAfterStartIndex ? item.estimatedEpsAvg : null);
lowList.push(isAfterStartIndex ? item.estimatedEpsLow : null);
highList.push(isAfterStartIndex ? item.estimatedEpsHigh : null);
break;
case "NetIncome":
valueList.push(isAfterStartIndex ? null : item.netIncome);
avgList.push(isAfterStartIndex ? item.estimatedNetIncomeAvg : null);
lowList.push(isAfterStartIndex ? item.estimatedNetIncomeLow : null);
highList.push(
isAfterStartIndex ? item.estimatedNetIncomeHigh : null,
);
break;
case "Ebitda":
valueList.push(isAfterStartIndex ? null : item.ebitda);
avgList.push(isAfterStartIndex ? item.estimatedEbitdaAvg : null);
lowList.push(isAfterStartIndex ? item.estimatedEbitdaLow : null);
highList.push(isAfterStartIndex ? item.estimatedEbitdaHigh : null);
break;
default:
break;
}
});
}
try {
const lastValue = valueList[stopIndex - 2];
avgList[stopIndex - 2] = lastValue;
lowList[stopIndex - 2] = lastValue;
highList[stopIndex - 2] = lastValue;
} catch (e) {
console.log(e);
}
// Normalize the data if needed (not required in this case, but leaving it here for reference)
let currentYearSuffix = new Date().getFullYear()?.toString().slice(-2);
let searchString = `FY${currentYearSuffix}`;
let currentYearIndex = dates?.findIndex((date) => date === searchString);
// Assign to global variables based on dataType
if (dataType === "Revenue") {
revenueDateList = dates?.slice(currentYearIndex) || [];
avgRevenueList =
avgList?.slice(currentYearIndex)?.map((val, index) => ({
FY: revenueDateList[index]?.slice(2),
val: val,
})) || [];
lowRevenueList =
lowList?.slice(currentYearIndex)?.map((val, index) => ({
FY: revenueDateList[index]?.slice(2),
val: val,
})) || [];
highRevenueList =
highList?.slice(currentYearIndex)?.map((val, index) => ({
FY: revenueDateList[index]?.slice(2),
val: val,
})) || [];
} else if (dataType === "EPS") {
epsDateList = dates?.slice(currentYearIndex) || [];
avgEPSList =
avgList?.slice(currentYearIndex)?.map((val, index) => ({
FY: epsDateList[index]?.slice(2),
val: val,
})) || [];
lowEPSList =
lowList?.slice(currentYearIndex)?.map((val, index) => ({
FY: epsDateList[index]?.slice(2),
val: val,
})) || [];
highEPSList =
highList?.slice(currentYearIndex)?.map((val, index) => ({
FY: epsDateList[index]?.slice(2),
val: val,
})) || [];
} else if (dataType === "NetIncome") {
netIncomeDateList = dates?.slice(currentYearIndex) || [];
avgNetIncomeList =
avgList?.slice(currentYearIndex)?.map((val, index) => ({
FY: netIncomeDateList[index]?.slice(2),
val: val,
})) || [];
lowNetIncomeList =
lowList?.slice(currentYearIndex)?.map((val, index) => ({
FY: netIncomeDateList[index]?.slice(2),
val: val,
})) || [];
highNetIncomeList =
highList?.slice(currentYearIndex)?.map((val, index) => ({
FY: netIncomeDateList[index]?.slice(2),
val: val,
})) || [];
} else if (dataType === "Ebitda") {
ebitdaDateList = dates?.slice(currentYearIndex) || [];
avgEbitdaList =
avgList?.slice(currentYearIndex)?.map((val, index) => ({
FY: ebitdaDateList[index]?.slice(2),
val: val,
})) || [];
lowEbitdaList =
lowList?.slice(currentYearIndex)?.map((val, index) => ({
FY: ebitdaDateList[index]?.slice(2),
val: val,
})) || [];
highEbitdaList =
highList?.slice(currentYearIndex)?.map((val, index) => ({
FY: ebitdaDateList[index]?.slice(2),
val: val,
})) || [];
}
const growthList = dates?.map((date) => {
const fy = parseInt(date.replace("FY", ""), 10); // Extract numeric FY value
const listToUse =
dataType === "Revenue"
? revenueAvgGrowthList
: dataType === "EPS"
? epsAvgGrowthList
: dataType === "NetIncome"
? netIncomeAvgGrowthList
: ebitdaAvgGrowthList; // Select the correct growth list
const growth = listToUse?.find((r) => r.FY === fy); // Find matching FY
return growth ? growth?.growth : null; // Return growth or null if not found
});
const option = {
silent: true,
tooltip: {
trigger: "axis",
hideDelay: 100, // Set the delay in milliseconds
},
animation: false,
grid: {
left: "5%",
right: "5%",
bottom: "2%",
top: "5%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: dates,
axisLabel: {
color: "#fff",
},
},
tooltip: {
trigger: "axis",
hideDelay: 100,
borderColor: "#969696", // Black border color
borderWidth: 1, // Border width of 1px
backgroundColor: "#313131", // Optional: Set background color for contrast
textStyle: {
color: "#fff", // Optional: Text color for better visibility
},
formatter: function (params) {
// Get the timestamp from the first parameter
const timestamp = params[0].axisValue;
// Sort the params array to arrange High, Avg, Low
const sortedParams = params.sort((a, b) => {
const order = { High: 0, Avg: 1, Low: 2 };
return order[a.seriesName] - order[b.seriesName];
});
// Initialize result with timestamp
let result = timestamp + "<br/>";
// Loop through each sorted series data
sortedParams.forEach((param) => {
result +=
param.seriesName + ": " + abbreviateNumber(param.value) + "<br/>";
});
return result;
},
},
yAxis: [
{
type: "value",
splitLine: {
show: false, // Disable x-axis grid lines
},
axisLabel: {
show: false, // Hide y-axis labels
},
},
],
series: [
{
name: "Actual",
data: valueList,
type: "line",
itemStyle: {
color: "#fff", // Change line plot color to white
},
showSymbol: false, // Show symbols for line plot points
},
{
name: "Avg",
data: avgList,
type: "line",
itemStyle: {
color: "#fff", // Change line plot color to green
},
lineStyle: {
type: "dashed", // Set the line type to dashed
},
showSymbol: false, // Show symbols for line plot points
},
{
name: "Low",
data: lowList,
type: "line",
itemStyle: {
color: "#3CB2EF", // Change line plot color to green
},
lineStyle: {
type: "dashed", // Set the line type to dashed
color: "#c2c7cf",
},
showSymbol: false, // Show symbols for line plot points
},
{
name: "High",
data: highList,
type: "line",
itemStyle: {
color: "#3CB2EF", // Change line plot color to green
},
lineStyle: {
type: "dashed", // Set the line type to dashed
color: "#c2c7cf",
},
showSymbol: false, // Show symbols for line plot points
},
],
};
let highGrowthList = [];
let lowGrowthList = [];
if (dataType === "Revenue") {
highGrowthList = computeGrowthSingleList(highRevenueList, avgRevenueList);
lowGrowthList = computeGrowthSingleList(lowRevenueList, avgRevenueList);
} else if (dataType === "EPS") {
highGrowthList = computeGrowthSingleList(highEPSList, avgEPSList);
lowGrowthList = computeGrowthSingleList(lowEPSList, avgEPSList);
} else if (dataType === "NetIncome") {
highGrowthList = computeGrowthSingleList(
highNetIncomeList,
avgNetIncomeList,
);
lowGrowthList = computeGrowthSingleList(
lowNetIncomeList,
avgNetIncomeList,
);
} else if (dataType === "Ebitda") {
highGrowthList = computeGrowthSingleList(highEbitdaList, avgEbitdaList);
lowGrowthList = computeGrowthSingleList(lowEbitdaList, avgEbitdaList);
}
highGrowthList = fillMissingDates(dates, highGrowthList)?.map(
(item) => item?.growth,
);
lowGrowthList = fillMissingDates(dates, lowGrowthList)?.map(
(item) => item?.growth,
);
const optionsGrowth = {
animation: false,
grid: {
left: "5%",
right: "5%",
bottom: "2%",
top: "5%",
containLabel: true,
},
xAxis: {
type: "category",
boundaryGap: false,
data: dates,
axisLabel: {
color: "#fff",
},
},
yAxis: [
{
type: "value",
splitLine: {
show: false, // Disable x-axis grid lines
},
axisLabel: {
show: false, // Hide y-axis labels
},
},
],
series: [
{
name:
dataType === "Revenue"
? "Revenue Growth"
: dataType === "EPS"
? "EPS Growth"
: dataType === "NetIncome"
? "Net Income Growth"
: "EBITDA Growth",
data: growthList?.map((value) => ({
value,
itemStyle: {
color: value >= 0 ? "#00FC50" : "#D9220E", // Green for >= 0, Red for < 0
},
})),
type: "bar",
smooth: true,
z: 5, // Ensure the bar chart has a lower z-index than the error bars
},
{
name: "Error Bars",
type: "custom",
renderItem: (params, api) => {
const xValue = api.value(0);
const yValue = api.value(1);
// Select high and low lists based on dataType
const highList = highGrowthList;
const lowList = lowGrowthList;
// Retrieve the corresponding high and low values
const high = highList[params.dataIndex];
const low = lowList[params.dataIndex];
// Skip rendering error bars if high or low values are null or undefined
if (high == null || low == null) return; // Null or undefined values are skipped
const x = api.coord([xValue, yValue])[0];
const highCoord = api.coord([xValue, high])[1];
const lowCoord = api.coord([xValue, low])[1];
return {
type: "group",
children: [
{
type: "line",
shape: {
x1: x,
y1: highCoord,
x2: x,
y2: lowCoord,
},
style: {
stroke: "#fff",
lineWidth: 2, // Set thicker line width
},
},
{
type: "line",
shape: {
x1: x - 5,
y1: highCoord,
x2: x + 5,
y2: highCoord,
},
style: {
stroke: "#fff",
lineWidth: 2, // Set thicker line width
},
},
{
type: "line",
shape: {
x1: x - 5,
y1: lowCoord,
x2: x + 5,
y2: lowCoord,
},
style: {
stroke: "#fff",
lineWidth: 2, // Set thicker line width
},
},
],
};
},
encode: {
x: 0, // Map x-axis values
y: 1, // Map y-axis values
},
data: growthList?.map((value, index) => [index, value]), // Prepare data for error bars
z: 10, // Bring the error bars to the front
},
],
tooltip: {
trigger: "axis",
hideDelay: 100,
borderColor: "#969696", // Black border color
borderWidth: 1, // Border width of 1px
backgroundColor: "#313131", // Optional: Set background color for contrast
textStyle: {
color: "#fff", // Optional: Text color for better visibility
},
formatter: (params) => {
const dataIndex = params[0].dataIndex;
const mainValue = params[0].value;
// Select high and low lists based on dataType
const highList = highGrowthList;
const lowList = lowGrowthList;
// Retrieve the corresponding high and low values
const high = highList[dataIndex];
const low = lowList[dataIndex];
// Only show High and Low if they are not "N/A"
let tooltipContent = `<b>${dates[dataIndex]}</b><br>`;
if (high && high !== "N/A") {
tooltipContent += `High: ${high.toFixed(2)}<br>`;
}
if (mainValue && mainValue !== "N/A") {
tooltipContent += `Avg: ${mainValue.toFixed(2)}<br>`;
}
if (low && low !== "N/A") {
tooltipContent += `Low: ${low.toFixed(2)}<br>`;
}
return tooltipContent;
},
},
};
if (dataType === "Revenue") {
optionsRevenue = option;
optionsRevenueGrowth = optionsGrowth;
} else if (dataType === "EPS") {
optionsEPS = option;
optionsEPSGrowth = optionsGrowth;
} else if (dataType === "NetIncome") {
optionsNetIncome = option;
optionsNetIncomeGrowth = optionsGrowth;
} else if (dataType === "Ebitda") {
optionsEbitda = option;
optionsEbitdaGrowth = optionsGrowth;
}
}
//To-do: Optimize this piece of shit
function prepareData() {
tableActualRevenue = [];
tableForecastRevenue = [];
tableCombinedRevenue = [];
tableActualEPS = [];
tableCombinedEPS = [];
tableForecastEPS = [];
tableActualNetIncome = [];
tableCombinedNetIncome = [];
tableForecastNetIncome = [];
tableActualEbitda = [];
tableCombinedEbitda = [];
tableForecastEbitda = [];
revenueAvgGrowthList = [];
epsAvgGrowthList = [];
netIncomeAvgGrowthList = [];
ebitdaAvgGrowthList = [];
let filteredData =
analystEstimateList?.filter((item) => item.date >= 2015) ?? [];
xData = filteredData?.map(({ date }) => Number(String(date)?.slice(-2)));
//============================//
//Revenue Data
filteredData?.forEach((item) => {
tableActualRevenue?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.revenue,
});
tableForecastRevenue?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.estimatedRevenueAvg,
numOfAnalysts: item?.numOfAnalysts,
});
});
tableCombinedRevenue = tableActualRevenue?.map((item1) => {
// Find the corresponding item in data2 based on "FY"
const item2 = tableForecastRevenue?.find(
(item2) => item2?.FY === item1?.FY,
);
// If the value in data1 is null, replace it with the value from data2
return {
FY: item1.FY,
val: item1.val === null ? item2.val : item1.val,
numOfAnalysts: item2.numOfAnalysts,
};
});
//============================//
//NetIncome Data
filteredData?.forEach((item) => {
tableActualNetIncome?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.netIncome,
});
tableForecastNetIncome?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.estimatedNetIncomeAvg,
});
});
tableCombinedNetIncome = tableActualNetIncome?.map((item1) => {
// Find the corresponding item in data2 based on "FY"
const item2 = tableForecastNetIncome?.find(
(item2) => item2?.FY === item1?.FY,
);
// If the value in data1 is null, replace it with the value from data2
return {
FY: item1.FY,
val: item1.val === null ? item2.val : item1.val,
};
});
//============================//
//Ebitda Data
filteredData?.forEach((item) => {
tableActualEbitda?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.ebitda,
});
tableForecastEbitda?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.estimatedEbitdaAvg,
});
});
tableCombinedEbitda = tableActualEbitda?.map((item1) => {
// Find the corresponding item in data2 based on "FY"
const item2 = tableForecastEbitda?.find(
(item2) => item2?.FY === item1?.FY,
);
// If the value in data1 is null, replace it with the value from data2
return {
FY: item1.FY,
val: item1.val === null ? item2.val : item1.val,
};
});
//============================//
//EPS Data
filteredData?.forEach((item) => {
tableActualEPS?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.eps,
});
tableForecastEPS?.push({
FY: Number(String(item?.date)?.slice(-2)),
val: item?.estimatedEpsAvg,
});
});
tableCombinedEPS = tableActualEPS?.map((item1) => {
// Find the corresponding item in data2 based on "FY"
const item2 = tableForecastEPS?.find((item2) => item2?.FY === item1?.FY);
// If the value in data1 is null, replace it with the value from data2
return {
FY: item1.FY,
val: item1.val === null ? item2.val : item1.val,
};
});
//Values coincide with table values for crosscheck
revenueAvgGrowthList = computeGrowthList(
tableActualRevenue,
tableCombinedRevenue,
);
netIncomeAvgGrowthList = computeGrowthList(
tableActualNetIncome,
tableCombinedNetIncome,
);
ebitdaAvgGrowthList = computeGrowthList(
tableActualEbitda,
tableCombinedEbitda,
);
epsAvgGrowthList = computeGrowthList(tableActualEPS, tableCombinedEPS);
}
$: {
if ($stockTicker && typeof window !== "undefined") {
isLoaded = false;
analystEstimateList = [];
analystEstimateList = data?.getAnalystEstimate || [];
if (analystEstimateList?.length !== 0) {
prepareData();
$analystEstimateComponent = true;
getPlotOptions("Revenue");
getPlotOptions("EPS");
getPlotOptions("NetIncome");
getPlotOptions("Ebitda");
} else {
$analystEstimateComponent = false;
}
isLoaded = true;
}
}
</script>
<section class="overflow-hidden text-white h-full pb-8 sm:pb-2">
<main class="overflow-hidden">
<div class="w-full m-auto">
<div class="flex flex-row items-center"></div>
{#if isLoaded}
{#if analystEstimateList?.length !== 0}
<div
class="no-scrollbar flex justify-start items-center w-screen sm:w-full mt-6 m-auto overflow-x-scroll pr-5 sm:pr-0"
>
<table
class="table table-sm table-pin-cols table-compact rounded-none sm:rounded-md w-full bg-table border border-gray-800"
>
<thead class="">
<tr class="">
<th
class="bg-primary border-b border-[#000] text-white font-semibold text-sm text-start"
>Fiscal Year</th
>
{#each xData as item}
<td
class="z-20 bg-primary border-b border-[#000] text-white font-semibold text-sm text-end bg-default"
>{"FY" + item}</td
>
{/each}
</tr>
</thead>
<tbody class="shadow-md">
<tr class="bg-primary border-b-[#27272A]">
<th
class="text-white whitespace-nowrap text-sm sm:text-[1rem] text-start font-medium bg-primary border-b border-[#27272A]"
>
Revenue
</th>
{#each tableCombinedRevenue as item}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium border-b border-[#27272A] bg-default"
>
{item?.val === "0.00" ||
item?.val === null ||
item?.val === 0
? "n/a"
: abbreviateNumber(item?.val.toFixed(2))}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="bg-primary whitespace-nowrap text-sm sm:text-[1rem] text-white text-start font-medium border-b border-[#27272A]"
>
Revenue Growth
</th>
{#each computeGrowthList(tableActualRevenue, tableCombinedRevenue) as item, index}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium bg-default"
>
{#if index === 0 || item?.growth === null}
n/a
{:else if tableActualRevenue[index]?.val === null}
<span
class="text-orange-400 {item?.growth > 0
? "before:content-['+']"
: ''}"
>
{item?.growth}%&#42;
</span>
{:else}
<span
class={item?.growth > 0
? "text-[#00FC50] before:content-['+']"
: item?.growth < 0
? "text-[#FF2F1F]"
: ""}
>
{item?.growth}%
</span>
{/if}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#09090B]">
<th
class="text-white whitespace-nowrap text-sm sm:text-[1rem] text-start font-medium bg-primary border-b border-[#27272A]"
>
EPS
</th>
{#each tableCombinedEPS as item}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium border-b border-[#27272A] bg-default"
>
{item?.val === "0.00" ||
item?.val === null ||
item?.val === 0
? "-"
: abbreviateNumber(item?.val.toFixed(2))}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="bg-primary whitespace-nowrap text-sm sm:text-[1rem] text-white text-start font-medium border-b border-[#27272A]"
>
EPS Growth
</th>
{#each computeGrowthList(tableActualEPS, tableCombinedEPS) as item, index}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium bg-default"
>
{#if index === 0 || item?.growth === null}
n/a
{:else if tableActualRevenue[index]?.val === null}
<span
class="text-orange-400 {item?.growth > 0
? "before:content-['+']"
: ''}"
>
{item?.growth}%&#42;
</span>
{:else}
<span
class={item?.growth > 0
? "text-[#00FC50] before:content-['+']"
: item?.growth < 0
? "text-[#FF2F1F]"
: ""}
>
{item?.growth}%
</span>
{/if}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="text-white whitespace-nowrap text-sm sm:text-[1rem] text-start font-medium bg-primary border-b border-[#27272A]"
>
Net Income
</th>
{#each tableCombinedNetIncome as item}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium border-b border-[#27272A] bg-default"
>
{item?.val === "0.00" ||
item?.val === null ||
item?.val === 0
? "n/a"
: abbreviateNumber(item?.val.toFixed(2))}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="bg-primary whitespace-nowrap text-sm sm:text-[1rem] text-white text-start font-medium border-b border-[#27272A]"
>
Net Income Growth
</th>
{#each computeGrowthList(tableActualNetIncome, tableCombinedNetIncome) as item, index}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium bg-default"
>
{#if index === 0 || item?.growth === null}
n/a
{:else if tableActualNetIncome[index]?.val === null}
<span
class="text-orange-400 {item?.growth > 0
? "before:content-['+']"
: ''}"
>
{item?.growth}%&#42;
</span>
{:else}
<span
class={item?.growth > 0
? "text-[#00FC50] before:content-['+']"
: item?.growth < 0
? "text-[#FF2F1F]"
: ""}
>
{item?.growth}%
</span>
{/if}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="text-white whitespace-nowrap text-sm sm:text-[1rem] text-start font-medium bg-primary border-b border-[#27272A]"
>
EBITDA
</th>
{#each tableCombinedEbitda as item}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium border-b border-[#27272A] bg-default"
>
{item?.val === "0.00" ||
item?.val === null ||
item?.val === 0
? "n/a"
: abbreviateNumber(item?.val.toFixed(2))}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="bg-primary whitespace-nowrap text-sm sm:text-[1rem] text-white text-start font-medium border-b border-[#27272A]"
>
EBITDA Growth
</th>
{#each computeGrowthList(tableActualEbitda, tableCombinedEbitda) as item, index}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium bg-default"
>
{#if index === 0 || item?.growth === null}
n/a
{:else if tableActualEbitda[index]?.val === null}
<span
class="text-orange-400 {item?.growth > 0
? "before:content-['+']"
: ''}"
>
{item?.growth}%&#42;
</span>
{:else}
<span
class={item?.growth > 0
? "text-[#00FC50] before:content-['+']"
: item?.growth < 0
? "text-[#FF2F1F]"
: ""}
>
{item?.growth}%
</span>
{/if}
</td>
{/each}
</tr>
<tr class="bg-primary border-b-[#27272A]">
<th
class="text-white whitespace-nowrap text-sm sm:text-[1rem] text-start font-medium bg-primary border-b border-[#27272A]"
>No. Analysts</th
>
{#each tableCombinedRevenue as item}
<td
class="text-white text-sm sm:text-[1rem] text-end font-medium border-b border-[#27272A] bg-default"
>
{#if item?.FY > 24}
{item?.numOfAnalysts === (null || 0)
? "n/a"
: item?.numOfAnalysts}
{:else}
-
{/if}
</td>
{/each}
</tr>
</tbody>
</table>
</div>
<div class="text-white text-sm mt-2">
Historical EPS numbers are GAAP, while forecasted numbers may be
non-GAAP.
</div>
<div class="text-orange-400 text-sm mt-2">
&#42; This value depends on the forecast
</div>
<!--
<div class="mt-5 text-gray-100 text-sm sm:text-[1rem] sm:rounded-md h-auto border border-gray-600 p-4">
<svg class="w-5 h-5 inline-block mr-0.5 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
>
For the current Fiscal Year we use available quarterly data. Complete annual data, used to compare against analyst estimates, is only finalized after the year ends.
</div>
-->
{/if}
{:else}
<div class="flex justify-center items-center h-80">
<div class="relative">
<label
class="bg-secondary rounded-md 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}
<div class="space-y-6 lg:grid lg:grid-cols-2 lg:gap-6 lg:space-y-0 mt-10">
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="Revenue"
options={optionsRevenue}
tableDataList={revenueDateList}
highDataList={highRevenueList}
avgDataList={avgRevenueList}
lowDataList={lowRevenueList}
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="Revenue Growth"
options={optionsRevenueGrowth}
tableDataList={revenueDateList}
highDataList={highRevenueList}
avgDataList={avgRevenueList}
lowDataList={lowRevenueList}
avgGrowthList={revenueAvgGrowthList}
graphType="growth"
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="EPS"
options={optionsEPS}
tableDataList={epsDateList}
highDataList={highEPSList}
avgDataList={avgEPSList}
lowDataList={lowEPSList}
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="EPS Growth"
options={optionsEPSGrowth}
tableDataList={epsDateList}
highDataList={highEPSList}
avgDataList={avgEPSList}
lowDataList={lowEPSList}
avgGrowthList={epsAvgGrowthList}
graphType="growth"
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="Net Income"
options={optionsNetIncome}
tableDataList={netIncomeDateList}
highDataList={highNetIncomeList}
avgDataList={avgNetIncomeList}
lowDataList={lowNetIncomeList}
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="Net Income Growth"
options={optionsNetIncomeGrowth}
tableDataList={netIncomeDateList}
highDataList={highNetIncomeList}
avgDataList={avgNetIncomeList}
lowDataList={lowNetIncomeList}
avgGrowthList={netIncomeAvgGrowthList}
graphType="growth"
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="EBITDA"
options={optionsEbitda}
tableDataList={ebitdaDateList}
highDataList={highEbitdaList}
avgDataList={avgEbitdaList}
lowDataList={lowEbitdaList}
/>
</Lazy>
<Lazy fadeOption={{ delay: 100, duration: 100 }} keep={true}>
<EstimationGraph
userTier={data?.user?.tier}
title="EBITDA Growth"
options={optionsEbitdaGrowth}
tableDataList={ebitdaDateList}
highDataList={highEbitdaList}
avgDataList={avgEbitdaList}
lowDataList={lowEbitdaList}
avgGrowthList={ebitdaAvgGrowthList}
graphType="growth"
/>
</Lazy>
</div>
</div>
</main>
</section>
<style>
.app {
height: 300px;
max-width: 100%; /* Ensure chart width doesn't exceed the container */
}
@media (max-width: 640px) {
.app {
height: 210px;
}
}
.chart {
width: 100%;
}
</style>