Skip to content

Commit

Permalink
223 heatload graph (#288)
Browse files Browse the repository at this point in the history
* Use rules engine output to populate heat load graph

* update graph record type

* run prettier for select files

* fix dataKeys

* Use correct data and make chart pretty

* Graph working, but needs clean up

* Clean up and comments

* Comments on calculation functions

* Add icon, still need to add values underneath chart

* restore ws changes to types

* Add columns at bottom of chart

* Fix reversed average and max in grid

* Fix responsiveness issues

* Extract utility and data building functions, tool tip and constants

* Add comment with link to external calculation docs

* Fix tool tip

* quick refactor

* clean up

* refactor calculating min and max for Y axis

* set start and end temps once

* Add 2f to x axis so tool tip is easier to invoke

* review changes to HeatLoad.tsx

* All issues except legend and tool type type errors addressed

* legend working with valid value for layout

* break out legend and fix avg heat load endpoint

* Adjust x axis ticks and make design set point a global constant

* fix HeatLoadGraphToolTip type error

* Remove hard coded data

* Add comment to calculateAvgHeatLoadEndPoint

* comment out inclusion_override

* Fix type errors

* re-comment tsconfig
  • Loading branch information
derekvmcintire authored Jan 15, 2025
1 parent 98edbe0 commit ab29024
Show file tree
Hide file tree
Showing 11 changed files with 484 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 13 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

All imports in the declaration are only used as types. Use `import type`
import { Icon } from '../../../icon'
import { HeatLoadGrid } from '../HeatLoadGrid'
import {

Check warning on line 16 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

`../constants` import should occur before import of `../HeatLoadGrid`
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'

Check warning on line 25 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

`../utility/build-heat-load-graph-data` import should occur before import of `../utility/heat-load-calculations`
import { HeatLoadGraphToolTip } from './HeatLoadGraphToolTip'
import { CustomLegend } from './HeatLoadGraphLegend'

Check warning on line 27 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

`./HeatLoadGraphLegend` import should occur before import of `./HeatLoadGraphToolTip`
import { DESIGN_SET_POINT } from '../../../../../global_constants'

Check warning on line 28 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

`../../../../../global_constants` import should occur before import of `../../../icon`

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])

Check warning on line 64 in heat-stack/app/components/ui/heat/CaseSummaryComponents/Graphs/HeatLoad.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

React Hook useMemo has missing dependencies: 'maxTemperature' and 'minTemperature'. Either include them or remove the dependency array

/**
* 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 (
<div>
<div className="item-title">Heating System Demand</div>
<div className="min-w-[625px] rounded-lg shadow-lg">
<span className="mb-4 text-lg font-semibold">
Heating System Demand
<Icon name="question-mark-circled" className="ps-1" size="md" />
</span>

<div className="relative w-full h-[400px]">
<ResponsiveContainer width="100%" height={400}>
<ScatterChart
<ComposedChart
margin={{
top: 20,
right: 20,
bottom: 20,
bottom: 50,
left: 100,
}}
data={data}
>
<CartesianGrid />
<CartesianGrid stroke={COLOR_GREY_LIGHT} strokeDasharray="3 3"/>

<XAxis
type="number"
dataKey="x"
name=" Outdoor Temperature"
unit="°F"
dataKey="temperature"
name="Outdoor Temperature"
domain={[minTemperature, maxTemperature]}
tickCount={(maxTemperature - minTemperature) / 4 } // Ensure whole number ticks
>
<Label
value="Outdoor Temperature (°F)"
position="bottom"
offset={20}
/>
</XAxis>

<YAxis type="number" name="Heat Load" domain={[minYValue, maxYValue]}>
<Label
value="Heat Load (BTU/h)"
position="left"
angle={-90}
offset={30}
/>
</YAxis>

<Tooltip content={<HeatLoadGraphToolTip />} />

{/* Line for maximum heat load */}
<Line
type="monotone"
dataKey="maxLine"
stroke={COLOR_ORANGE}
dot={false}
name="Maximum, no internal or solar gain"
/>

{/* Line for average heat load */}
<Line
type="monotone"
dataKey="avgLine"
stroke={COLOR_BLUE}
dot={false}
name="Average, with internal & solar gain"
/>

{/* Scatter point for maximum heat load at design temperature */}
<Scatter
dataKey="maxPoint"
fill={COLOR_ORANGE}
name="Maximum at design temperature"
shape="diamond"
legendType="diamond"
/>
<YAxis type="number" dataKey="y" name=" Heat Load" unit=" BTU/h" />
<Tooltip cursor={{ strokeDasharray: '3 3' }} />
<Scatter name="Heat Load" data={data} fill="#8884d8" />
</ScatterChart>

{/* Scatter point for average heat load at design temperature */}
<Scatter
dataKey="avgPoint"
fill={COLOR_BLUE}
name="Average at design temperature"
shape="diamond"
legendType="diamond"
/>
</ComposedChart>
</ResponsiveContainer>
<CustomLegend />
</div>

<HeatLoadGrid
setPoint={DESIGN_SET_POINT}
averageHeatLoad={calculateAvgHeatLoad(
heatLoadSummaryOutput,
design_temperature,
DESIGN_SET_POINT,
)}
maxHeatLoad={calculateMaxHeatLoad(
whole_home_heat_loss_rate,
design_temperature,
DESIGN_SET_POINT,
)}
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className="absolute top-6 right-6 bg-white border border-gray-200 rounded p-4 shadow-sm">
{legendItems.map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2 last:mb-0">
{item.type === 'line' ? (
<div className="w-8 h-0.5" style={{ backgroundColor: item.color }} />
) : (
<div className="w-3 h-3 rotate-45" style={{ backgroundColor: item.color }} />
)}
<span className="text-sm">{item.name}</span>
</div>
))}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<div className="tooltip-content rounded border border-gray-300 bg-white p-2">
<div>{`${Number(value).toLocaleString()} BTU/h`}</div>
<div>{`${temperature}°F ${name.replace('Line', ' Heat Load').replace('Point', ' at Design Temperature')}`}</div>
</div>
)
}

return (
<div className="tooltip-content rounded border border-gray-300 bg-white p-2">
<div>{`${Number(value).toLocaleString()} BTU/h`}</div>
<div>
{name.replace('Line', ' Heat Load').replace('Point', ' at Design Temperature')}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,7 +19,7 @@ export function Graphs() {
Fuel Type
{fuel_type}
{/* <AnalysisHeader /> */}
<HeatLoad />
<HeatLoad heatLoadSummaryOutput={heatLoadSummaryOutput} />
<WholeHomeUAComparison />
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto p-4">
<div className="grid grid-cols-3 gap-4">
{/* Grid Item 1 */}
<div className="flex items-center justify-center border-r-2 border-gray-300 p-6">
<div className="flex flex-col items-center">
<div className="text-gray-500">Set Point</div>
<div className="font-semibold">{`${setPoint} °F`}</div>
</div>
</div>

{/* Grid Item 2 */}
<div className="flex items-center justify-center border-r-2 border-gray-300 p-6">
<div className="flex flex-col items-center">
<div className="text-gray-500">Max Heat Load</div>
<div className="font-semibold">{`${maxHeatLoad} BTU/h`}</div>
</div>
</div>

{/* Grid Item 3 */}
<div className="flex items-center justify-center p-6">
<div className="flex flex-col items-center">
<div className="text-gray-500">Average Heat Load</div>
<div className="font-semibold">{`${averageHeatLoad} BTU/h`}</div>
</div>
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -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'
Loading

0 comments on commit ab29024

Please sign in to comment.