diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx index f5223fd3..fc77291a 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx @@ -1,48 +1,189 @@ +import React, { useMemo } from 'react' import { - ScatterChart, - Scatter, + ComposedChart, + Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + Label, + Scatter, } from 'recharts' +import { SummaryOutputSchema } from '../../../../../../types/types' +import { Icon } from '../../../icon' +import { HeatLoadGrid } from '../HeatLoadGrid' +import { + COLOR_GREY_LIGHT, + COLOR_ORANGE, + COLOR_BLUE, +} from '../constants' +import { + calculateAvgHeatLoad, + calculateMaxHeatLoad, +} from '../utility/heat-load-calculations' +import { buildHeatLoadGraphData } from '../utility/build-heat-load-graph-data' +import { HeatLoadGraphToolTip } from './HeatLoadGraphToolTip' +import { CustomLegend } from './HeatLoadGraphLegend' +import { DESIGN_SET_POINT } from '../../../../../global_constants' + +const X_AXIS_BUFFER_PERCENTAGE_MAX = 1.3; // 30% buffer +const Y_AXIS_ROUNDING_UNIT = 10000; // Rounding unit for minY and maxY +const Y_AXIS_MIN_VALUE = 0; // Always start the Y axis at 0 + +const roundDownToNearestTen = (n: number) => Math.floor(n / 10) * 10; // Used for determining the start temperature on the X axis + +type HeatLoadProps = { + heatLoadSummaryOutput: SummaryOutputSchema +} + +/** + * HeatLoad component renders a chart displaying the heating system demand based on different outdoor temperatures. + * It includes two lines for the maximum and average heat loads, with scatter points at the design temperature. + * + * @param {HeatLoadProps} props - The props containing heat load data to render the chart. + * @returns {JSX.Element} - The rendered chart component. + */ +export function HeatLoad({ + heatLoadSummaryOutput, +}: HeatLoadProps): JSX.Element { + const { design_temperature, whole_home_heat_loss_rate } = heatLoadSummaryOutput + const minTemperature = roundDownToNearestTen(design_temperature - 10) // Start temperature rounded down from design temperature for visual clarity + const maxTemperature = DESIGN_SET_POINT + 2 // end the X axis at the DESIGN_SET_POINT plus 2f for visual clarity + + /** + * useMemo to build the HeatLoad graph data. + */ + const data = useMemo(() => { + return buildHeatLoadGraphData( + heatLoadSummaryOutput, + minTemperature, + DESIGN_SET_POINT, + maxTemperature, + ) + }, [heatLoadSummaryOutput]) + + /** + * useMemo to iterate through the data and calculate the min and max values for the Y axis. + */ + const { minYValue, maxYValue } = useMemo(() => { + let minValue = Infinity + let maxValue = 0 + + for (const point of data) { + const maxLine = point.maxLine || 0 + const avgLine = point.avgLine || 0 + + minValue = Math.min(minValue, maxLine || Infinity, avgLine || Infinity) + maxValue = Math.max(maxValue, maxLine, avgLine) + } -// data from Karle Heat Load Analysis Beta 7 2023-07-11 -const data = [ - { x: 0, y: 74015 }, - { x: 60.5, y: 10045 }, - { x: 67, y: 3172 }, - { x: 70, y: 0 }, - { x: 8.4, y: 65133 }, -] + // seet min and max Y axis values + const minY = Y_AXIS_MIN_VALUE + const adjustedMaxYValue = maxValue * X_AXIS_BUFFER_PERCENTAGE_MAX; + const maxY = Math.ceil(adjustedMaxYValue / Y_AXIS_ROUNDING_UNIT) * Y_AXIS_ROUNDING_UNIT + + return { minYValue: minY, maxYValue: maxY } + }, [data]) -export function HeatLoad() { return ( -
-
Heating System Demand
+
+ + Heating System Demand + + +
- - + + + + + + + + } /> + + {/* Line for maximum heat load */} + + + {/* Line for average heat load */} + + + {/* Scatter point for maximum heat load at design temperature */} + - - - - + + {/* Scatter point for average heat load at design temperature */} + + + +
+ +
) } diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphLegend.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphLegend.tsx new file mode 100644 index 00000000..6ff7ca95 --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphLegend.tsx @@ -0,0 +1,28 @@ +import { + COLOR_ORANGE, + COLOR_BLUE, +} from '../constants' + +export const CustomLegend = () => { + const legendItems = [ + { name: "Maximum, no internal or solar gain", color: COLOR_ORANGE, type: "line" }, + { name: "Average, with internal & solar gain", color: COLOR_BLUE, type: "line" }, + { name: "Maximum at design temperature", color: COLOR_ORANGE, type: "diamond" }, + { name: "Average at design temperature", color: COLOR_BLUE, type: "diamond" } + ]; + + return ( +
+ {legendItems.map((item, index) => ( +
+ {item.type === 'line' ? ( +
+ ) : ( +
+ )} + {item.name} +
+ ))} +
+ ); +}; diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphToolTip.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphToolTip.tsx new file mode 100644 index 00000000..925ae62c --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoadGraphToolTip.tsx @@ -0,0 +1,35 @@ +type HeatLoadGraphToolTipProps = { + payload?: Array<{ payload: { temperature?: number }; value?: number; name?: string }> +} + +/** + * CustomTooltip renders a tooltip for the heat load chart. + * @param {object} props - The props containing data for the tooltip. + * @returns {JSX.Element} - The rendered tooltip element. + */ +export const HeatLoadGraphToolTip = ( + props: HeatLoadGraphToolTipProps, +): JSX.Element => { + const { payload } = props + const temperature = payload?.[0]?.payload?.temperature ?? null + const value = payload?.[0]?.value ?? null + const name = payload?.[0]?.name ?? '' + + if (temperature !== null) { + return ( +
+
{`${Number(value).toLocaleString()} BTU/h`}
+
{`${temperature}°F ${name.replace('Line', ' Heat Load').replace('Point', ' at Design Temperature')}`}
+
+ ) + } + + return ( +
+
{`${Number(value).toLocaleString()} BTU/h`}
+
+ {name.replace('Line', ' Heat Load').replace('Point', ' at Design Temperature')} +
+
+ ) +} diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadAnalysis.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadAnalysis.tsx index c0393dbb..059f7ad2 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadAnalysis.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadAnalysis.tsx @@ -1,8 +1,13 @@ // import { AnalysisHeader } from './AnalysisHeader.tsx' +import React from 'react' import { HeatLoad } from './Graphs/HeatLoad.tsx' import { WholeHomeUAComparison } from './Graphs/WholeHomeUAComparison.tsx' -export function Graphs() { +interface GraphsProps { + heatLoadSummaryOutput: any; +} + +export function Graphs({ heatLoadSummaryOutput }: GraphsProps) { const fuel_type = 'Natural Gas' const titleClassTailwind = 'text-5xl font-extrabold tracking-wide' const componentMargin = 'mt-10' @@ -14,7 +19,7 @@ export function Graphs() { Fuel Type {fuel_type} {/* */} - +
) diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadGrid.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadGrid.tsx new file mode 100644 index 00000000..f56cde06 --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HeatLoadGrid.tsx @@ -0,0 +1,55 @@ +import React from 'react' + +type HeatLoadGridProps = { + setPoint: number + averageHeatLoad: number + maxHeatLoad: number +} + +/** + * HeatLoadGrid is a stateless functional component that displays key summary data + * in a grid format. The grid includes the set point temperature, maximum heat load, + * and average heat load values. + * + * @component + * @param {HeatLoadGridProps} props - The props for the HeatLoadGrid component. + * @param {number} props.setPoint - The set point temperature in degrees Fahrenheit. + * @param {number} props.averageHeatLoad - The average heat load in BTU/h. + * @param {number} props.maxHeatLoad - The maximum heat load in BTU/h. + * @returns {JSX.Element} - A styled grid displaying the set point, max heat load, and average heat load. + */ +export const HeatLoadGrid = ({ + setPoint, + averageHeatLoad, + maxHeatLoad, +}: HeatLoadGridProps) => { + return ( +
+
+ {/* Grid Item 1 */} +
+
+
Set Point
+
{`${setPoint} °F`}
+
+
+ + {/* Grid Item 2 */} +
+
+
Max Heat Load
+
{`${maxHeatLoad} BTU/h`}
+
+
+ + {/* Grid Item 3 */} +
+
+
Average Heat Load
+
{`${averageHeatLoad} BTU/h`}
+
+
+
+
+ ) +} diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/constants.ts b/heat-stack/app/components/ui/heat/CaseSummaryComponents/constants.ts new file mode 100644 index 00000000..d9820d20 --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/constants.ts @@ -0,0 +1,6 @@ +// Constants for chart styling +export const COLOR_ORANGE = '#FF5733' +export const COLOR_BLUE = '#8884d8' +export const COLOR_GREY = '#999999' +export const COLOR_GREY_LIGHT = '#cccccc' +export const COLOR_WHITE = '#fff' diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/build-heat-load-graph-data.ts b/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/build-heat-load-graph-data.ts new file mode 100644 index 00000000..dd09b2ff --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/build-heat-load-graph-data.ts @@ -0,0 +1,99 @@ +import { SummaryOutputSchema } from '../../../../../../types/types'; +import { + calculateAvgHeatLoad, + calculateAvgHeatLoadEndPoint, + calculateMaxHeatLoad, +} from './heat-load-calculations'; + +type HeatLoadGraphPoint = { + temperature: number + avgLine?: number + avgPoint?: number + maxLine?: number + maxPoint?: number +}; + +/** + * Calculate the heat load data points for max and avg lines. + * The data is computed based on the provided heat load summary and the design temperature. + * The returned data points are used for graphing the average and maximum heat load over a range of temperatures. + * + * @param {SummaryOutputSchema} heatLoadSummaryOutput - The heat load summary data. + * @param {number} designSetPoint - The design temperature set point, typically 70°F. + * @returns {HeatLoadGraphPoint[]} - An array of data points for the lines and scatter points, each containing a temperature and associated heat load values. + */ +export const buildHeatLoadGraphData = ( + heatLoadSummaryOutput: SummaryOutputSchema, + startTemperature: number, + designSetPoint: number, + endTemperature: number, +): HeatLoadGraphPoint[] => { + const { design_temperature, whole_home_heat_loss_rate, average_indoor_temperature, estimated_balance_point } = heatLoadSummaryOutput; + + const avgHeatLoad = calculateAvgHeatLoad( + heatLoadSummaryOutput, + design_temperature, + designSetPoint, + ); + + const maxHeatLoad = calculateMaxHeatLoad( + whole_home_heat_loss_rate, + design_temperature, + designSetPoint, + ); + + // Points for Avg line + const avgLineStartPoint = { + temperature: startTemperature, + avgLine: calculateAvgHeatLoad( + heatLoadSummaryOutput, + startTemperature, + designSetPoint, + ), + }; + + const avgLineDesignTemperaturePoint = { + temperature: design_temperature, + avgLine: avgHeatLoad, + avgPoint: avgHeatLoad, + }; + + const avgLineEndPoint = { + temperature: calculateAvgHeatLoadEndPoint(estimated_balance_point, designSetPoint, average_indoor_temperature), + avgLine: 0, + }; + + // Points for Max line + const maxLineStartPoint = { + temperature: startTemperature, + maxLine: calculateMaxHeatLoad( + whole_home_heat_loss_rate, + startTemperature, + designSetPoint, + ), + }; + + const maxLineDesignTemperaturePoint = { + temperature: design_temperature, + maxLine: maxHeatLoad, + maxPoint: maxHeatLoad, + }; + + const maxLineEndPoint = { + temperature: designSetPoint, + maxLine: calculateMaxHeatLoad( + whole_home_heat_loss_rate, + endTemperature, + designSetPoint, + ), + }; + + return [ + avgLineStartPoint, + avgLineDesignTemperaturePoint, + avgLineEndPoint, + maxLineStartPoint, + maxLineDesignTemperaturePoint, + maxLineEndPoint + ] +} diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/heat-load-calculations.ts b/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/heat-load-calculations.ts new file mode 100644 index 00000000..0f5d7c31 --- /dev/null +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/utility/heat-load-calculations.ts @@ -0,0 +1,69 @@ +import { SummaryOutputSchema } from '../../../../../../types/types' +// Utility file for helper functions related to calculating heat load +// calculations are based on documentation found here: https://docs.google.com/document/d/16WlqY3ofq4xpalsfwRuYBWMbeUHfXRvbWU69xxVNCGM/edit?tab=t.0#heading=h.tl7o1hwvhavz + +/** + * Calculates the maximum heat load based on the given temperature and heat load summary. + * The result is rounded to the nearest integer. + * + * @param {number} whole_home_heat_loss_rate - The heat loss rate for the whole home. + * @param {number} temperature - The current temperature to use in the calculation. + * @param {number} [designSetPoint] - The design set point temperature. + * @returns {number} - The calculated maximum heat load. + */ +export function calculateMaxHeatLoad( + whole_home_heat_loss_rate: number, + temperature: number, + designSetPoint: number, +): number { + return Math.max(0, (designSetPoint - temperature) * whole_home_heat_loss_rate) +} + +/** + * Calculates the average heat load based on the given temperature and heat load summary. + * The result is rounded to the nearest integer. + * + * @param {SummaryOutputSchema} heatLoadSummary - The summary data that includes heat loss rates and indoor temperature details. + * @param {number} temperature - The current temperature to use in the calculation. + * @param {number} [designSetPoint] - The design set point temperature. + * @returns {number} - The calculated average heat load. + */ +export function calculateAvgHeatLoad( + heatLoadSummary: SummaryOutputSchema, + temperature: number, + designSetPoint: number, +): number { + const { + whole_home_heat_loss_rate, + average_indoor_temperature, + estimated_balance_point, + } = heatLoadSummary + return Math.max( + 0, + (designSetPoint - + average_indoor_temperature + + estimated_balance_point - + temperature) * + whole_home_heat_loss_rate, + ) +} + +/** + * Calculates the average heat load endpoint based on the balance point temperature, + * design set point, and average indoor temperature. + * + * This function computes the endpoint of the average heat load calculation, which can + * be used as part of broader heat load analyses. + * + * @param {number} balancePointTemperature - The balance point temperature used in the calculation. + * @param {number} designSetPoint - The design set point temperature. + * @param {number} averageIndoorTemperature - The average indoor temperature. + * @returns {number} - The calculated average heat load endpoint. + */ +export function calculateAvgHeatLoadEndPoint( + balancePointTemperature: number, + designSetPoint: number, + averageIndoorTemperature: number, +): number { + return balancePointTemperature + designSetPoint - averageIndoorTemperature; +} diff --git a/heat-stack/app/global_constants.ts b/heat-stack/app/global_constants.ts new file mode 100644 index 00000000..7ee94942 --- /dev/null +++ b/heat-stack/app/global_constants.ts @@ -0,0 +1 @@ +export const DESIGN_SET_POINT = 70 // Design set point (70°F), defined in external documentation - https://docs.google.com/document/d/16WlqY3ofq4xpalsfwRuYBWMbeUHfXRvbWU69xxVNCGM/edit?tab=t.0 diff --git a/heat-stack/app/routes/_heat+/heatloadanalysis.tsx b/heat-stack/app/routes/_heat+/heatloadanalysis.tsx index 5360e271..9a603d1c 100644 --- a/heat-stack/app/routes/_heat+/heatloadanalysis.tsx +++ b/heat-stack/app/routes/_heat+/heatloadanalysis.tsx @@ -1,5 +1,12 @@ +import React from 'react' import { Graphs } from '../../components/ui/heat/CaseSummaryComponents/HeatLoadAnalysis.tsx' -export default function HeatLoadAnalysis() { - return +interface HeatLoadAnalysisProps { + heatLoadSummaryOutput: any; +} + +export default function HeatLoadAnalysis({ + heatLoadSummaryOutput, +}: HeatLoadAnalysisProps) { + return } diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 25be9d32..cd027b5c 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -58,6 +58,7 @@ import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryCompon import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx' import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' import HeatLoadAnalysis from './heatloadanalysis.tsx' +import React from 'react' /** Modeled off the conform example at * https://github.com/epicweb-dev/web-forms/blob/b69e441f5577b91e7df116eba415d4714daacb9d/exercises/03.schema-validation/03.solution.conform-form/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L48 */ @@ -399,10 +400,10 @@ Traceback (most recent call last): File "", line 32, const gasBillDataWithUserAdjustments = foo; /* processed_energy_bills is untested here */ - const billingRecords = foo.get('processed_energy_bills') - billingRecords.forEach((record: any) => { - record.set('inclusion_override', true); - }); + // const billingRecords = foo.get('processed_energy_bills') + // billingRecords.forEach((record: any) => { + // record.set('inclusion_override', true); + // }); // foo.set('processed_energy_bills', null) // foo.set('processed_energy_bills', billingRecords) //console.log("(after customization) gasBillDataWithUserAdjustments billing records[0]", gasBillDataWithUserAdjustments.get('processed_energy_bills')[0]) @@ -482,7 +483,10 @@ export default function Inputs() { // const location = useLocation(); // console.log(`location:`, location); // `.state` is `null` const lastResult = useActionData() - + const parsedLastResult = hasDataProperty(lastResult) + ? JSON.parse(lastResult.data, reviver) as Map: undefined; + + const heatLoadSummaryOutput = parsedLastResult ? Object.fromEntries(parsedLastResult?.get('heat_load_output')) : undefined; /* @ts-ignore */ // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) @@ -516,8 +520,7 @@ export default function Inputs() { temp1.get('balance_point_graph').get('records')[0].get('heat_loss_rate') *//* @ts-ignore */ - // console.log("HeatLoad chart", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('balance_point_graph')?.get('records'): undefined) - + // console.log("HeatLoad chart", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('balance_point_graph')?.get('records'): undefined) type ActionResult = | SubmissionResult | { data: string } @@ -588,7 +591,7 @@ export default function Inputs() { - {show_usage_data && } + {show_usage_data && } ) }