1219 lines
41 KiB
Svelte
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}%*
|
|
</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}%*
|
|
</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}%*
|
|
</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}%*
|
|
</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">
|
|
* 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>
|