Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

247 interactive chart #255

Merged
merged 8 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { type z } from 'zod'
import { type HeatLoadAnalysisZod } from '#types/index'

type HeatLoadAnalysisZod = z.infer<typeof HeatLoadAnalysisZod>
export function AnalysisHeader(props: { usage_data: any }) {
import { type z } from 'zod';
import { type UsageDataSchema } from '#/types/types.ts';

export function AnalysisHeader({ usage_data }: { usage_data: UsageDataSchema}) {
// Example usage_data
// new Map([[
// "estimated_balance_point",
Expand Down Expand Up @@ -34,27 +32,30 @@ export function AnalysisHeader(props: { usage_data: any }) {
// 3312125.0171753373
// ]])

const summaryOutputs = props.usage_data?.get('summary_output')
// Extract the summary_output from usage_data
const summaryOutputs = usage_data?.summary_output;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future issues:

  1. Remove ?. optional chaining operator from user_data
  2. How do we want to handle ?. optional chaining operators in general? They indicate that something is not as expected, which will have consequences somewhere in the code. Those cases should be handled.

Copy link
Member Author

@thiagobardini thiagobardini Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll handle this using Zod validation, so we can safely remove the ?. operator. The schema ensures default values. No need for optional chaining since Zod validates the structure and provides defaults where necessary. The validation is in types/index.ts, ensuring consistency.

This a example:

export const summaryOutputSchema = z.object({
  estimated_balance_point: z.number().optional().default('N/A'),
  other_fuel_usage: z.number().optional().default('N/A'),
  average_indoor_temperature: z.number().optional().default('N/A'),
  difference_between_ti_and_tbp: z.number().optional().default('N/A'),
  design_temperature: z.number().optional().default('N/A'),
  whole_home_heat_loss_rate: z.number().optional().default('N/A'),
  standard_deviation_of_heat_loss_rate: z.number().optional().default('N/A'),
  average_heat_load: z.number().optional().default('N/A'),
  maximum_heat_load: z.number().optional().default('N/A'),
});

Since we have validation with Zod, we no longer need to manually use ?. or write something like this: {summaryOutputs?.average_indoor_temperature ?? 'N/A'} °F, because Zod will handle it for us.

Let me know what you think!

Copy link
Member

@thadk thadk Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Briefly talked about tradeoffs of using optional in meeting, esp mentioning required form fields being driven by Zod. To me that means if we take the .optional() approach, we may need multiple zod objects for the same piece.
Let's cover this in #258

// Calculate the number of billing periods included in Heating calculations
const heatingAnalysisTypeRecords = props.usage_data
?.get('billing_records')
?.filter((billingRecord: any) => billingRecord.get('analysis_type') == 1)
const heatingAnalysisTypeRecords = usage_data?.billing_records?.filter(
(billingRecord) => billingRecord.analysis_type === 1,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const heatingAnalysisTypeRecords = usage_data?.billing_records?.filter(
// If heatingAnalysisTypeRecords comes out undefined, it gets handled later
const heatingAnalysisTypeRecords = usage_data?.billing_records?.filter(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

// Do wee need this code instead? (billingRecord) => billingRecord.analysis_type !== "NOT_ALLOWED_IN_CALCULATIONS",
);

const recordsIncludedByDefault = heatingAnalysisTypeRecords?.filter(
(billingRecord: any) =>
billingRecord.get('default_inclusion_by_calculation') == true &&
billingRecord.get('inclusion_override') == false,
).length
(billingRecord) =>
billingRecord.default_inclusion_by_calculation === true &&
billingRecord.inclusion_override === false,
).length;

const recordsIncludedByOverride = heatingAnalysisTypeRecords?.filter(
(billingRecord: any) =>
billingRecord.get('default_inclusion_by_calculation') == false &&
billingRecord.get('inclusion_override') == true,
).length
(billingRecord) =>
billingRecord.default_inclusion_by_calculation === false &&
billingRecord.inclusion_override === true,
).length;

const numRecordsForHeatingCalculations =
recordsIncludedByDefault + recordsIncludedByOverride
(recordsIncludedByDefault || 0) + (recordsIncludedByOverride || 0);


return (
<div className="section-title">
Expand All @@ -64,14 +65,14 @@ export function AnalysisHeader(props: { usage_data: any }) {
<div className="item-title-small">
Average Indoor Temperature <br />
<div className="item">
{summaryOutputs?.get('average_indoor_temperature')} °F
</div>{' '}
{summaryOutputs?.average_indoor_temperature} °F
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle the case where summaryOutputs is undefined

Suggested change
{summaryOutputs?.average_indoor_temperature} °F
{summaryOutputs?.average_indoor_temperature ?? `N/A`} °F

Copy link
Member Author

@thiagobardini thiagobardini Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

<br />
Balance Point Temperature
<br />
<div className="item">
{summaryOutputs?.get('estimated_balance_point')} °F
</div>{' '}
{summaryOutputs?.estimated_balance_point} °F
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{summaryOutputs?.estimated_balance_point} °F
{summaryOutputs?.estimated_balance_point ?? `N/A`} °F

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

<br />
</div>
</div>
Expand All @@ -83,8 +84,8 @@ export function AnalysisHeader(props: { usage_data: any }) {
Daily non-heating Usage <br />
<div className="item">
{/* Rounding to two decimal places */}
{summaryOutputs?.get('other_fuel_usage').toFixed(2)} therms
</div>{' '}
{summaryOutputs?.other_fuel_usage?.toFixed(2)} therms
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{summaryOutputs?.other_fuel_usage?.toFixed(2)} therms
{summaryOutputs?.other_fuel_usage?.toFixed(2) ?? `N/A`} therms

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

</div>
</div>
<div className="basis-1/3">
Expand All @@ -93,19 +94,17 @@ export function AnalysisHeader(props: { usage_data: any }) {
<div className="item">
{/* Rounding to two decimal places */}
{(
summaryOutputs?.get('standard_deviation_of_heat_loss_rate') *
100
).toFixed(2)}{' '}
summaryOutputs?.standard_deviation_of_heat_loss_rate * 100
)?.toFixed(2)}{' '}
%
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
)?.toFixed(2)}{' '}
)?.toFixed(2) ?? `N/A`}{' '}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

</div>{' '}
</div>
<br />
Whole-home UA
<br />
<div className="item">
{/* Rounding to zero decimal places */}
{summaryOutputs?.get('whole_home_heat_loss_rate').toFixed(0)}{' '}
BTU/h-°F
</div>{' '}
{summaryOutputs?.whole_home_heat_loss_rate?.toFixed(0)} BTU/h-°F
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{summaryOutputs?.whole_home_heat_loss_rate?.toFixed(0)} BTU/h-°F
{summaryOutputs?.whole_home_heat_loss_rate?.toFixed(0) ?? `N/A`} BTU/h-°F

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zod solution: #255 (comment)

<br />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Upload } from 'lucide-react'
import { Suspense, /* lazy */ } from 'react'

import { Button } from '#/app/components/ui/button.tsx'
import { type UsageDataSchema } from '#/types/types.ts';
import { AnalysisHeader } from './AnalysisHeader.tsx'
import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx'

Expand All @@ -11,40 +11,46 @@ import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx'
// import { Input } from '#/app/components/ui/input.tsx'
// import { Label } from '#/app/components/ui/label.tsx'


// export function EnergyUseHistory(props: EnergyUseProps) {
export function EnergyUseHistory(props: { usage_data: any }) {
// console.log(`EnergyUseHistory:`, props.usage_data);
export function EnergyUseHistory({
usage_data,
}: {
usage_data: UsageDataSchema
}) {
const titleClass = 'text-5xl font-extrabold tracking-wide mt-10'
// const subtitleClass = 'text-2xl font-semibold text-zinc-950 mt-9'

return (

<div>
<h2 className={`${titleClass} pb-6`}>Energy Use History</h2>
<div>
<Suspense fallback={'<div>Blah</div>'}>
<input
id="energy_use_upload"
aria-label="Upload your energy billing company's bill."
//onChange
accept=".xml,.csv,application/xml,text/xml,text/csv,application/csv,application/x-csv,text/comma-separated-values,text/x-comma-separated-values"
name="energy_use_upload"
type="file"
/>
<Button type="submit"> <Upload className="h-4 w-4 mr-2" /> Upload</Button>
</Suspense>
(<a className="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 gap-1" href="https://github.com/codeforboston/home-energy-analysis-tool/issues/162#issuecomment-2246594484">Get example file here</a>)

</div>
{props.usage_data && <AnalysisHeader usage_data={ props.usage_data } /> }
{props.usage_data && <EnergyUseHistoryChart usage_data={props.usage_data} />}

<input
id="energy_use_upload"
aria-label="Upload your energy billing company's bill."
accept=".xml,.csv,application/xml,text/xml,text/csv,application/csv,application/x-csv,text/comma-separated-values,text/x-comma-separated-values"
name="energy_use_upload"
type="file"
/>
<Button type="submit">
<Upload className="mr-2 h-4 w-4" /> Upload
</Button>

<a
className="inline-flex items-center gap-1 rounded-md border border-transparent bg-secondary px-2.5 py-0.5 text-xs font-semibold text-secondary-foreground transition-colors hover:bg-secondary/80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
href="https://github.com/codeforboston/home-energy-analysis-tool/issues/162#issuecomment-2246594484"
>
Get example file here
</a>

{usage_data && (
<>
<AnalysisHeader usage_data={usage_data} />
<EnergyUseHistoryChart usage_data={usage_data} />
</>
)}
</div>
)
}



// const file = event.target.files?.[0]

// if (file) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { type z } from 'zod'
import { NaturalGasUsageData, type NaturalGasBillRecord as NaturalGasBillRecordZod } from '#types/index'
import { type UsageDataSchema, type BillingRecordsSchema } from '#/types/types.ts'
import { Checkbox } from '../../../../components/ui/checkbox.tsx'

import {
Expand All @@ -15,7 +16,7 @@
import NonHeatingUsage from './assets/NonHeatingUsage.png'
import NotAllowedInCalculations from './assets/NotAllowedInCalculations.png'

import { tr } from '@faker-js/faker'

Check warning on line 19 in heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx

View workflow job for this annotation

GitHub Actions / ⬣ Heat-Stack - ESLint

`@faker-js/faker` import should occur before import of `react`

// type NaturalGasBillRecord = z.infer<typeof NaturalGasBillRecordZod>
// const naturalGasBillRecord01: NaturalGasBillRecord = {
Expand Down Expand Up @@ -53,23 +54,35 @@
// naturalGasBillRecord04,
// ]





// export function EnergyUseHistoryChart(props: z.infer<typeof NaturalGasUsageData>) {
export function EnergyUseHistoryChart(props: { usage_data: any }) {
console.log("EnergyUseHistoryChart Component:", props.usage_data?.get('billing_records'))

const billingRecords = props.usage_data?.get('billing_records')

const handleOverrideCheckboxChange = () => {
console.log("handleOverrideCheckboxChange")

export function EnergyUseHistoryChart({ usage_data }: { usage_data: UsageDataSchema }) {
const [billingRecords, setBillingRecords] = useState<BillingRecordsSchema>([])

useEffect(() => {
if (usage_data?.billing_records) {
// Process the billing records directly without converting from Map
setBillingRecords(usage_data.billing_records)
}
}, [usage_data])

const handleOverrideCheckboxChange = (index: number) => {
setBillingRecords((prevRecords) => {
const newRecords = structuredClone(prevRecords)
const period = newRecords[index]

if (period) {
const currentOverride = period.inclusion_override
// Toggle 'inclusion_override'
period.inclusion_override = !currentOverride
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the expected effects from this mutation?


newRecords[index] = { ...period }
}

return newRecords
})
}

return (
<Table id='EnergyUseHistoryChart'>
<Table id="EnergyUseHistoryChart">
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">#</TableHead>
Expand All @@ -85,23 +98,21 @@
</TableRow>
</TableHeader>
<TableBody>
{/* {naturalGasBill.map((period, index) => { */}
{billingRecords.map((period: Map<string, any>, index: number) => {

const startDate = new Date(period.get('period_start_date'))
const endDate = new Date(period.get('period_end_date'))
{billingRecords.map((period, index) => {
const startDate = new Date(period.period_start_date)
const endDate = new Date(period.period_end_date)

// Calculate days in period
const timeInPeriod = endDate.getTime() - startDate.getTime()
const daysInPeriod = Math.round(timeInPeriod / (1000 * 3600 * 24))

// Set Analysis Type image and checkbox setting
const analysisType = period.get('analysis_type')
const analysisType = period.analysis_type
let analysisType_Image = undefined
let overrideCheckboxDisabled = false

/* switch case for 1, -1, 0 */
switch (analysisType){
switch (analysisType) {
case 1:
analysisType_Image = HeatingUsage
break
Expand All @@ -113,35 +124,35 @@
overrideCheckboxDisabled = true
break
}

// Adjust inclusion for user input
let calculatedInclusion = period.get('default_inclusion_by_calculation')
if (period.get('inclusion_override')) {
let calculatedInclusion = period.default_inclusion_by_calculation
if (period.inclusion_override) {
calculatedInclusion = !calculatedInclusion
}

const variant = calculatedInclusion ? 'included' : 'excluded'

return (
<TableRow key={index} variant={variant}>
<TableCell className="font-medium">{index + 1}</TableCell>
<TableCell><img src={analysisType_Image} alt='Analysis Type'></img></TableCell>
<TableCell>
<img src={analysisType_Image} alt="Analysis Type" />
</TableCell>
<TableCell>{startDate.toLocaleDateString()}</TableCell>
<TableCell>{endDate.toLocaleDateString()}</TableCell>
<TableCell>{daysInPeriod}</TableCell>
<TableCell>{period.get('usage')}</TableCell>
<TableCell>{period.usage}</TableCell>
<TableCell>
{period.get('whole_home_heat_loss_rate')?
period.get('whole_home_heat_loss_rate').toFixed(0)
:
"-"
}
</TableCell>
{period.whole_home_heat_loss_rate
? period.whole_home_heat_loss_rate.toFixed(0)
: '-'}
</TableCell>
<TableCell>
<Checkbox
checked={period.get('inclusion_override')}
<Checkbox
checked={period.inclusion_override}
disabled={overrideCheckboxDisabled}
onClick={handleOverrideCheckboxChange}
onClick={() => handleOverrideCheckboxChange(index)}
/>
</TableCell>
</TableRow>
Expand Down
Loading
Loading