1174 lines
39 KiB
Svelte
1174 lines
39 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";
|
|
import { mode } from "mode-watcher";
|
|
|
|
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 = {
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
plotOptions: {
|
|
series: {
|
|
animation: false,
|
|
marker: {
|
|
enabled: false,
|
|
states: {
|
|
hover: { enabled: false }, // Disable marker on hover
|
|
select: { enabled: false }, // Disable marker on selection
|
|
},
|
|
},
|
|
},
|
|
},
|
|
chart: {
|
|
type: "line",
|
|
backgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
plotBackgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
height: 360,
|
|
animation: false,
|
|
},
|
|
title: {
|
|
text: null,
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
useHTML: true,
|
|
backgroundColor: "rgba(0, 0, 0, 0.8)", // Semi-transparent black
|
|
borderColor: "rgba(255, 255, 255, 0.2)", // Slightly visible white border
|
|
borderWidth: 1,
|
|
style: {
|
|
color: "#fff",
|
|
fontSize: "16px",
|
|
padding: "10px",
|
|
},
|
|
borderRadius: 4,
|
|
formatter: function () {
|
|
// Format the x value to display time in hh:mm format
|
|
let tooltipContent = `<span class=" m-auto text-[1rem] font-[501]">${
|
|
this?.x
|
|
}</span><br>`;
|
|
|
|
// Loop through each point in the shared tooltip
|
|
this.points?.forEach((point) => {
|
|
tooltipContent += `<span class=" font-semibold text-sm">${point.series.name}:</span>
|
|
<span class=" font-normal text-sm" >${abbreviateNumber(
|
|
point.y,
|
|
)}</span><br>`;
|
|
});
|
|
|
|
return tooltipContent;
|
|
},
|
|
},
|
|
xAxis: {
|
|
categories: dates,
|
|
type: "datetime",
|
|
endOnTick: false,
|
|
categories: dates,
|
|
crosshair: {
|
|
color: $mode === "light" ? "black" : "white", // Set the color of the crosshair line
|
|
width: 1, // Adjust the line width as needed
|
|
dashStyle: "Solid",
|
|
},
|
|
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "black" : "white",
|
|
fontSize: "12px",
|
|
},
|
|
},
|
|
},
|
|
yAxis: {
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#d1d5dc" : "#111827",
|
|
labels: {
|
|
style: { color: $mode === "light" ? "black" : "white" },
|
|
},
|
|
title: { text: null },
|
|
opposite: true,
|
|
},
|
|
series: [
|
|
{
|
|
name: "Actual",
|
|
data: valueList,
|
|
color: $mode === "light" ? "#2C6288" : "white",
|
|
animation: false,
|
|
},
|
|
{
|
|
name: "Avg",
|
|
data: avgList,
|
|
color: $mode === "light" ? "#2C6288" : "white",
|
|
dashStyle: "Dash", // Dashed line style
|
|
animation: false,
|
|
marker: {
|
|
enabled: false,
|
|
},
|
|
},
|
|
{
|
|
name: "Low",
|
|
data: lowList,
|
|
// If you want a dashed line with a different color, set the series color to that color.
|
|
color: $mode === "light" ? "#8AAAC0" : "#c2c7cf",
|
|
dashStyle: "Dash",
|
|
animation: false,
|
|
},
|
|
{
|
|
name: "High",
|
|
data: highList,
|
|
color: $mode === "light" ? "#8AAAC0" : "#c2c7cf",
|
|
dashStyle: "Dash",
|
|
animation: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
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 = {
|
|
credits: {
|
|
enabled: false,
|
|
},
|
|
legend: {
|
|
enabled: false,
|
|
},
|
|
plotOptions: {
|
|
series: {
|
|
animation: false,
|
|
},
|
|
},
|
|
chart: {
|
|
type: "column",
|
|
backgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
plotBackgroundColor: $mode === "light" ? "#fff" : "#09090B",
|
|
height: 360,
|
|
animation: false,
|
|
},
|
|
title: {
|
|
text: null,
|
|
},
|
|
tooltip: {
|
|
shared: true,
|
|
useHTML: true,
|
|
backgroundColor: "rgba(0, 0, 0, 0.8)", // Semi-transparent black
|
|
borderColor: "rgba(255, 255, 255, 0.2)", // Slightly visible white border
|
|
borderWidth: 1,
|
|
style: {
|
|
color: "white",
|
|
fontSize: "14px",
|
|
padding: "10px",
|
|
},
|
|
borderRadius: 4,
|
|
formatter: function () {
|
|
// Find the main series point (exclude error bar points)
|
|
const mainPoint = this.points.find(
|
|
(p) => p.series.type !== "errorbar",
|
|
);
|
|
const idx = mainPoint.point.index;
|
|
const mainValue = mainPoint.y;
|
|
let tooltipContent = `<b style="font-weight:501;">${dates[idx]}</b><br>`;
|
|
|
|
// Use highGrowthList and lowGrowthList from outer scope
|
|
const high = highGrowthList[idx];
|
|
const low = lowGrowthList[idx];
|
|
|
|
if (high && high !== "N/A") {
|
|
tooltipContent += `<span style="font-weight:501;">High:</span> ${high.toFixed(2)}<br>`;
|
|
}
|
|
if (mainValue && mainValue !== "N/A") {
|
|
tooltipContent += `<span style="font-weight:501;">Avg:</span> ${mainValue.toFixed(2)}<br>`;
|
|
}
|
|
if (low && low !== "N/A") {
|
|
tooltipContent += `<span style="font-weight:501;">Low:</span> ${low.toFixed(2)}<br>`;
|
|
}
|
|
return tooltipContent;
|
|
},
|
|
},
|
|
|
|
xAxis: {
|
|
categories: dates,
|
|
type: "datetime",
|
|
labels: {
|
|
style: {
|
|
color: $mode === "light" ? "black" : "white",
|
|
fontSize: "12px",
|
|
},
|
|
},
|
|
},
|
|
yAxis: {
|
|
gridLineWidth: 1,
|
|
gridLineColor: $mode === "light" ? "#d1d5dc" : "#111827",
|
|
labels: {
|
|
style: { color: $mode === "light" ? "black" : "white" },
|
|
},
|
|
title: { text: null },
|
|
opposite: true,
|
|
},
|
|
series: [
|
|
{
|
|
// Dynamically set the series name based on dataType
|
|
name:
|
|
dataType === "Revenue"
|
|
? "Revenue Growth"
|
|
: dataType === "EPS"
|
|
? "EPS Growth"
|
|
: dataType === "NetIncome"
|
|
? "Net Income Growth"
|
|
: "EBITDA Growth",
|
|
data: growthList?.map((value) => ({
|
|
y: value,
|
|
// Set color based on the sign of the value
|
|
color:
|
|
value >= 0
|
|
? $mode === "light"
|
|
? "#338D73"
|
|
: "#00FC50"
|
|
: "#ED3333",
|
|
borderColor:
|
|
value >= 0
|
|
? $mode === "light"
|
|
? "#338D73"
|
|
: "#00FC50"
|
|
: "#ED3333",
|
|
borderRadius: "1px",
|
|
})),
|
|
zIndex: 5,
|
|
// 'smooth' is not applicable for column charts
|
|
},
|
|
{
|
|
name: "Error Bars",
|
|
type: "errorbar",
|
|
// Prepare data as [low, high] pairs for each index.
|
|
data: growthList?.map((value, index) => {
|
|
const high = highGrowthList[index];
|
|
const low = lowGrowthList[index];
|
|
// If either high or low is null/undefined, return nulls.
|
|
return high != null && low != null ? [low, high] : [null, null];
|
|
}),
|
|
color: $mode === "light" ? "black" : "white",
|
|
lineWidth: 1, // Thicker lines for error bars
|
|
whiskerLength: 10, // Adjust whisker length as needed
|
|
zIndex: 10,
|
|
// Disable tooltip for error bar points
|
|
tooltip: {
|
|
pointFormatter: function () {
|
|
return "";
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
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 || $mode) {
|
|
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 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-2 m-auto overflow-x-auto pr-5 sm:pr-0"
|
|
>
|
|
<table
|
|
class="table table-sm table-compact no-scrollbar rounded-none sm:rounded-md w-full bg-white dark:bg-table border border-gray-300 dark:border-gray-800 m-auto"
|
|
>
|
|
<thead class="text-muted dark:text-white">
|
|
<tr class="">
|
|
<th class=" font-semibold text-sm text-start">Fiscal Year</th>
|
|
{#each xData as item}
|
|
<td class="z-20 font-semibold text-sm text-end"
|
|
>{"FY" + item}</td
|
|
>
|
|
{/each}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr class=" ">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>
|
|
Revenue
|
|
</th>
|
|
{#each tableCombinedRevenue as item}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{item?.val === "0.00" ||
|
|
item?.val === null ||
|
|
item?.val === 0
|
|
? "n/a"
|
|
: abbreviateNumber(item?.val.toFixed(2))}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr class="bg-[#F6F7F8] dark:bg-odd">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>
|
|
Revenue Growth
|
|
</th>
|
|
{#each computeGrowthList(tableActualRevenue, tableCombinedRevenue) as item, index}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{#if index === 0 || item?.growth === null}
|
|
n/a
|
|
{:else if tableActualRevenue[index]?.val === null}
|
|
<span
|
|
class="text-orange-600 dark:text-orange-400 {item?.growth >
|
|
0
|
|
? "before:content-['+']"
|
|
: ''}"
|
|
>
|
|
{item?.growth}%*
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class={item?.growth > 0
|
|
? "text-green-600 dark:text-[#00FC50] before:content-['+']"
|
|
: item?.growth < 0
|
|
? "text-red-600 dark:text-[#FF2F1F]"
|
|
: ""}
|
|
>
|
|
{item?.growth}%
|
|
</span>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr class="">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>
|
|
EPS
|
|
</th>
|
|
{#each tableCombinedEPS as item}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{item?.val === "0.00" ||
|
|
item?.val === null ||
|
|
item?.val === 0
|
|
? "-"
|
|
: abbreviateNumber(item?.val.toFixed(2))}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr class="bg-[#F6F7F8] dark:bg-odd">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] font-normal text-start"
|
|
>
|
|
EPS Growth
|
|
</th>
|
|
{#each computeGrowthList(tableActualEPS, tableCombinedEPS) as item, index}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{#if index === 0 || item?.growth === null}
|
|
n/a
|
|
{:else if tableActualRevenue[index]?.val === null}
|
|
<span
|
|
class="text-orange-600 dark:text-orange-400 {item?.growth >
|
|
0
|
|
? "before:content-['+']"
|
|
: ''}"
|
|
>
|
|
{item?.growth}%*
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class={item?.growth > 0
|
|
? "text-green-600 dark:text-[#00FC50] before:content-['+']"
|
|
: item?.growth < 0
|
|
? "text-red-600 dark:text-[#FF2F1F]"
|
|
: ""}
|
|
>
|
|
{item?.growth}%
|
|
</span>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
<tr>
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>
|
|
Net Income
|
|
</th>
|
|
{#each tableCombinedNetIncome as item}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{item?.val === "0.00" ||
|
|
item?.val === null ||
|
|
item?.val === 0
|
|
? "n/a"
|
|
: abbreviateNumber(item?.val.toFixed(2))}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr class="bg-[#F6F7F8] dark:bg-odd">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] font-normal text-start"
|
|
>
|
|
Net Income Growth
|
|
</th>
|
|
{#each computeGrowthList(tableActualNetIncome, tableCombinedNetIncome) as item, index}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{#if index === 0 || item?.growth === null}
|
|
n/a
|
|
{:else if tableActualNetIncome[index]?.val === null}
|
|
<span
|
|
class="text-orange-600 dark:text-orange-400 {item?.growth >
|
|
0
|
|
? "before:content-['+']"
|
|
: ''}"
|
|
>
|
|
{item?.growth}%*
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class={item?.growth > 0
|
|
? "text-green-600 dark:text-[#00FC50] before:content-['+']"
|
|
: item?.growth < 0
|
|
? "text-red-600 dark:text-[#FF2F1F]"
|
|
: ""}
|
|
>
|
|
{item?.growth}%
|
|
</span>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr>
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>
|
|
EBITDA
|
|
</th>
|
|
{#each tableCombinedEbitda as item}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{item?.val === "0.00" ||
|
|
item?.val === null ||
|
|
item?.val === 0
|
|
? "n/a"
|
|
: abbreviateNumber(item?.val.toFixed(2))}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr class="bg-[#F6F7F8] dark:bg-odd">
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] font-normal text-start"
|
|
>
|
|
EBITDA Growth
|
|
</th>
|
|
{#each computeGrowthList(tableActualEbitda, tableCombinedEbitda) as item, index}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{#if index === 0 || item?.growth === null}
|
|
n/a
|
|
{:else if tableActualEbitda[index]?.val === null}
|
|
<span
|
|
class="text-orange-600 dark:text-orange-400 {item?.growth >
|
|
0
|
|
? "before:content-['+']"
|
|
: ''}"
|
|
>
|
|
{item?.growth}%*
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class={item?.growth > 0
|
|
? "text-green-600 dark:text-[#00FC50] before:content-['+']"
|
|
: item?.growth < 0
|
|
? "text-red-600 dark:text-[#FF2F1F]"
|
|
: ""}
|
|
>
|
|
{item?.growth}%
|
|
</span>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
|
|
<tr>
|
|
<th
|
|
class=" whitespace-nowrap text-sm sm:text-[1rem] text-start font-normal"
|
|
>No. Analysts</th
|
|
>
|
|
{#each tableCombinedRevenue as item}
|
|
<td class=" text-sm sm:text-[1rem] text-end">
|
|
{#if item?.FY > 24}
|
|
{item?.numOfAnalysts === (null || 0)
|
|
? "n/a"
|
|
: item?.numOfAnalysts}
|
|
{:else}
|
|
-
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class=" text-sm mt-2">
|
|
Historical EPS numbers are GAAP, while forecasted numbers may be
|
|
non-GAAP.
|
|
</div>
|
|
<div class="text-orange-600 dark:text-orange-400 text-sm mt-2">
|
|
* 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 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"
|
|
config={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"
|
|
config={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"
|
|
config={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"
|
|
config={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"
|
|
config={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"
|
|
config={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"
|
|
config={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"
|
|
config={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>
|