diff --git a/src/lib/components/AnalystEstimate.svelte b/src/lib/components/AnalystEstimate.svelte index 14bec02c..4e65c624 100644 --- a/src/lib/components/AnalystEstimate.svelte +++ b/src/lib/components/AnalystEstimate.svelte @@ -17,6 +17,8 @@ let xData = []; let optionsRevenue = null; let optionsEPS = null; + let optionsRevenueGrowth = null; + let optionsEPSGrowth = null; let revenueDateList = []; let avgRevenueList = []; let lowRevenueList = []; @@ -26,8 +28,53 @@ let lowEPSList = []; let highEPSList = []; + let revenueGrowthList = []; + let epsGrowthList = []; + let displayData = "Revenue"; + function computeGrowthList(tableActualRevenue, tableForecastRevenue) { + return tableActualRevenue?.map((item, index) => { + // If it's the first item or the list is empty, return null + if (index === 0 || tableActualRevenue.length === 0) { + return null; + } + + // If actual value is null, check forecast + if (item?.val === null) { + const prevForecastVal = tableForecastRevenue[index - 1]?.val ?? 0; + const currentForecastVal = tableForecastRevenue[index]?.val ?? 0; + + // Avoid division by zero when calculating forecast growth + if (prevForecastVal === 0 || currentForecastVal === 0) { + return null; // Return null if previous or current forecast is 0 + } + + const forecastGrowth = + ((currentForecastVal - prevForecastVal) / Math.abs(prevForecastVal)) * + 100; + + // Return rounded forecast growth value, or null if 0 + return 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; + + // Avoid division by zero when calculating actual growth + if (prevActualVal === 0 || currentActualVal === 0) { + return null; // Return null if previous or current actual value is 0 + } + + const actualGrowth = + ((currentActualVal - prevActualVal) / Math.abs(prevActualVal)) * 100; + + // Return rounded actual growth value, or null if 0 + return actualGrowth !== 0 ? Number(actualGrowth.toFixed(2)) : null; + }); + } + function findIndex(data) { const currentYear = new Date().getFullYear(); @@ -105,8 +152,8 @@ }, animation: false, grid: { - left: "2%", - right: "2%", + left: "5%", + right: "5%", bottom: "2%", top: "5%", containLabel: true, @@ -183,6 +230,56 @@ ], }; + 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" : "EPS Growth", + data: (dataType === "Revenue" ? revenueGrowthList : epsGrowthList) + ?.slice(1) + .map((value) => ({ + value, + itemStyle: { + color: value >= 0 ? "#00FC50" : "#D9220E", // Green for >= 0, Red for < 0 + }, + })), + type: "bar", + smooth: true, + }, + ], + + tooltip: { + trigger: "axis", + }, + }; + let currentYearSuffix = new Date().getFullYear().toString().slice(-2); let searchString = `FY${currentYearSuffix}`; @@ -190,6 +287,7 @@ if (dataType === "Revenue") { let currentYearIndex = dates?.findIndex((date) => date === searchString); optionsRevenue = option; + optionsRevenueGrowth = optionsGrowth; revenueDateList = dates?.slice(currentYearIndex) || []; avgRevenueList = avgList?.slice(currentYearIndex) || []; lowRevenueList = lowList?.slice(currentYearIndex) || []; @@ -198,6 +296,8 @@ let currentYearIndex = dates?.findIndex((date) => date === searchString); optionsEPS = option; + optionsEPSGrowth = optionsGrowth; + epsDateList = dates?.slice(currentYearIndex) || []; avgEPSList = avgList?.slice(currentYearIndex) || []; lowEPSList = lowList?.slice(currentYearIndex) || []; @@ -213,6 +313,9 @@ tableActualEPS = []; tableForecastEPS = []; + revenueGrowthList = []; + epsGrowthList = []; + let filteredData = analystEstimateList?.filter((item) => item.date >= 2015) ?? []; @@ -229,38 +332,26 @@ numOfAnalysts: item?.numOfAnalysts, }); }); + //EPS Data - let forwardPeStart = false; filteredData?.forEach((item) => { - const fy = Number(String(item?.date)?.slice(-2)); - const actualVal = item?.eps ?? null; - const forecastVal = item?.estimatedEpsAvg; - tableActualEPS?.push({ - FY: fy, - val: actualVal, + FY: Number(String(item?.date)?.slice(-2)), + val: item?.eps, + }); + tableForecastEPS?.push({ + FY: Number(String(item?.date)?.slice(-2)), + val: item?.estimatedEpsAvg, }); - - if (actualVal === null) { - forwardPeStart = true; - } - - const forecastEntry: any = { - FY: fy, - val: forecastVal, - numOfAnalysts: item?.numOfAnalysts, - }; - - // Add forwardPe if the condition is met - if (forwardPeStart && forecastVal !== null) { - forecastEntry.forwardPe = - Math.round((data.getStockQuote.price / forecastVal) * 100) / 100; - } else { - forecastEntry.forwardPe = null; - } - - tableForecastEPS?.push(forecastEntry); }); + + //Values coincide with table values for crosscheck + revenueGrowthList = computeGrowthList( + tableActualRevenue, + tableForecastRevenue, + ); + + epsGrowthList = computeGrowthList(tableActualEPS, tableForecastEPS); } $: { @@ -269,11 +360,10 @@ analystEstimateList = []; analystEstimateList = data?.getAnalystEstimate || []; if (analystEstimateList?.length !== 0) { + prepareData(); $analystEstimateComponent = true; getPlotOptions("Revenue"); getPlotOptions("EPS"); - - prepareData(); } else { $analystEstimateComponent = false; } @@ -335,66 +425,30 @@ > Revenue Growth - {#each tableActualRevenue as item, index} + {#each computeGrowthList(tableActualRevenue, tableForecastRevenue) as growth, index} - {#if index === 0 || tableActualRevenue?.length === 0} + {#if index === 0 || growth === null} n/a - {:else if item?.val === null} - {#if tableForecastRevenue[index]?.val - tableForecastRevenue[index - 1]?.val > 0} - - {(() => { - const previousVal = - tableForecastRevenue[index - 1]?.val ?? 0; - const currentVal = - tableForecastRevenue[index]?.val ?? 0; - const change = - ((currentVal - previousVal) / - Math.abs(previousVal)) * - 100; - return isFinite(change) - ? `${change.toFixed(2)}%*` - : "n/a"; - })()} - - {:else if tableForecastRevenue[index]?.val - tableForecastRevenue[index - 1]?.val < 0} - - {(() => { - const previousVal = - tableForecastRevenue[index - 1]?.val ?? 0; - const currentVal = - tableForecastRevenue[index]?.val ?? 0; - const change = - ((currentVal - previousVal) / - Math.abs(previousVal)) * - 100; - return isFinite(change) - ? `${change.toFixed(2)}%*` - : "n/a"; - })()} - - {:else} - n/a - {/if} - {:else if item?.val - tableActualRevenue[index - 1]?.val > 0} - - {( - ((item?.val - tableActualRevenue[index - 1]?.val) / - Math.abs(tableActualRevenue[index - 1]?.val)) * - 100 - )?.toFixed(2)}% - - {:else if item?.val - tableActualRevenue[index - 1]?.val < 0} - - {( - ((item?.val - tableActualRevenue[index - 1]?.val) / - Math.abs(tableActualRevenue[index - 1]?.val)) * - 100 - )?.toFixed(2)}% + {:else if tableActualRevenue[index]?.val === null} + + {growth}%* {:else} - 0.00% + 0 + ? "text-[#00FC50] before:content-['+']" + : growth < 0 + ? "text-[#FF2F1F]" + : ""} + > + {growth}% + {/if} {/each} @@ -425,50 +479,30 @@ > EPS Growth - {#each tableActualEPS as item, index} + {#each computeGrowthList(tableActualEPS, tableForecastEPS) as growth, index} - {#if index === 0 || tableActualEPS?.length === 0} + {#if index === 0 || growth === null} n/a - {:else if item?.val === null} - {#if tableForecastEPS[index]?.val - tableForecastEPS[index - 1]?.val > 0} - - {( - ((tableForecastEPS[index]?.val - - tableForecastEPS[index - 1]?.val) / - Math.abs(tableForecastEPS[index - 1]?.val)) * - 100 - )?.toFixed(2)}%* - - {:else if tableForecastEPS[index]?.val - tableForecastEPS[index - 1]?.val < 0} - - {( - ((tableForecastEPS[index]?.val - - tableForecastEPS[index - 1]?.val) / - Math.abs(tableForecastEPS[index - 1]?.val)) * - 100 - )?.toFixed(2)}%* - - {/if} - {:else if item?.val - tableActualEPS[index - 1]?.val > 0} - - {( - ((item?.val - tableActualEPS[index - 1]?.val) / - Math.abs(tableActualEPS[index - 1]?.val)) * - 100 - )?.toFixed(2)}% - - {:else if item?.val - tableActualEPS[index - 1]?.val < 0} - - {( - ((item?.val - tableActualEPS[index - 1]?.val) / - Math.abs(tableActualEPS[index - 1]?.val)) * - 100 - )?.toFixed(2)}% + {:else if tableActualRevenue[index]?.val === null} + + {growth}%* {:else} - 0.00% + 0 + ? "text-[#00FC50] before:content-['+']" + : growth < 0 + ? "text-[#FF2F1F]" + : ""} + > + {growth}% + {/if} {/each} @@ -647,6 +681,115 @@ +
+

Revenue Growth

+
+
+ {#if optionsRevenueGrowth !== null} + + {/if} +
+
+ + + {#each revenueDateList as date} + + {/each} + + + {#each highRevenueList as val, index} + + {/each} + + {#each avgRevenueList as val, index} + + {/each} + + {#each lowRevenueList as val, index} + + {/each} + +
Revenue Growth{date}
High + {#if data?.user?.tier !== "Pro" && index >= highRevenueList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
Avg + {#if data?.user?.tier !== "Pro" && index >= avgRevenueList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
Low + {#if data?.user?.tier !== "Pro" && index >= lowRevenueList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
+
+
+
+

EPS Forecast

@@ -755,6 +898,114 @@
+
+

EPS Growth

+
+
+ {#if optionsEPSGrowth !== null} + + {/if} +
+
+ + + {#each epsDateList as date} + + {/each} + + + {#each highEPSList as val, index} + + {/each} + + {#each avgEPSList as val, index} + + {/each} + + {#each lowEPSList as val, index} + + {/each} + +
EPS Growth{date}
High + {#if data?.user?.tier !== "Pro" && index >= highEPSList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
Avg + {#if data?.user?.tier !== "Pro" && index >= avgEPSList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
Low + {#if data?.user?.tier !== "Pro" && index >= lowEPSList?.length - 2} + Pro + {:else} + {abbreviateNumber(val)} + {/if} +
+
+
+