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

Heart Rate Chart #247

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Heart Rate Chart #247

wants to merge 6 commits into from

Conversation

reneefromhold
Copy link
Collaborator

@reneefromhold reneefromhold commented Apr 24, 2024

Overview

This line chart will display the heart rate data from Fitbit Heart Rate Intraday, or AppleHealth via the DeviceDataV2 endpoints.

  • DataSources are AppleHealth and Fitbit
  • AggregateOptions are min, max, count, avg, sum
  • aggregationIntervalMinutes- the number of minutes to aggregate on
  • LineColor - the color of the line.
  • Thresholds - For each threshold defined, a reference line will be displayed and the color of the data line will render in the color defined.

Security

REMINDER: All file contents are public.

  • [x ] I have ensured no secure credentials or sensitive information remain in code, metadata, comments, etc.
    • [ x] Please verify that you double checked that .storybook/preview.js does not contain your participant access key details.
    • [ x] There are no temporary testing changes committed such as API base URLs, access tokens, print/log statements, etc.
  • [ x] These changes do not introduce any security risks, or any such risks have been properly mitigated.

Describe briefly what security risks you considered, why they don't apply, or how they've been mitigated.

Checklist

Testing

Documentation

  • I have added relevant Storybook updates to this PR as well.
  • If this feature requires a developer doc update, tag @CareEvolution/api-docs.

Consider "Squash and merge" as needed to keep the commit history reasonable on main.

Reviewers

Assign to the appropriate reviewer(s). Minimally, a second set of eyes is needed ensure no non-public information is published. Consider also including:

  • Subject-matter experts
  • Style/editing reviewers
  • Others requested by the content owner

@chrisnowak
Copy link
Contributor

The x-axis here should be like the original mockup chart and just be spaced 3 hours apart with labels at 3am, 6am, 9am, 12pm, 3pm, 6pm, 9pm:
image

Additionally, I think a lot of the formatting / styling that I shared previously is not applied - e.g. this is showing solid black borders...my sense was that all we had to do was take the code from my branch and pipe in the data and fix up the gradient/threshold stuff

innerRef?: React.Ref<HTMLDivElement>
}

export default function MdhLineChart(props: LineChartProps) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I definitely wouldnt' call this "LineChart" as that is far too vague of a name. I think we should make this at least somewhat opininated.

Putting aside whether it makes sense to combine this with @ajmenca 's PR, I would propose that this be called "IntradayLineChart" and be hardcoded to a 24 hour interval with the x-axis as I described and it could take data as data points in that interval...do not think it makes sense to make a generally reusable "line chart" as that is what recharts already gives us

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I do agree LineChart is too vague.
What I do want to avoid is recreating the threshold interface and the display of it across components. We currently consume BarChartThreshold in vbuilder. Adding LineChartThreshold here is no help. Would be nice to have a simple ChartThreshold interface that is used across charts, along with not having to copy/paste the display of thresholds across components.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I will integrate the feedback around making it more opinionated and hang tight on the Threshold stuff

const xValues: any[] = data.map(d => d.date.getTime());
const xMax = Math.max(...xValues);
const xMin = Math.min(...xValues);
axis.xRange = [xMin, xMax];
Copy link
Contributor

Choose a reason for hiding this comment

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

Axis should be hardcoded

if (!props.axis || (props.axis && !props.axis.yRange)) {
const yValues: number[] = data.map(d => d.value);
const yMax = Math.max(...yValues);
axis.yRange = [0, yMax];
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why you need to do this...I think just not supplying a domain will be sufficient

<ReferenceLine key={`threshref_${index}`} y={threshold.value} stroke={resolveColor(layoutContext.colorScheme, threshold.referenceLineColor)} />
)}
<YAxis tick={YAxisTick} type='number' domain={axis?.yRange ?? [0,100]} />
<XAxis tick={XAxisTick} axisLine={true} dataKey="date" tickMargin={0} minTickGap={0} tickCount={24}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is where I think you need to specify the "interval" to get the axis ticks to show up at 3/6/9 etc.

previewState?: IntradayHeartRatePreviewState,
dataSource: DeviceDataV2Namespace,
aggregationOption: AggregationOption,
aggregationIntervalAmount: number,
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's just call this "aggregationIntervalMinutes" and not have aggregationIntervalType...this is intraday so no need to complicate with extra params

@@ -0,0 +1,29 @@
import MyDataHelps, { Guid, DeviceDataV2AggregateQuery, DeviceDataV2AggregatePage, DeviceDataV2Aggregate } from "@careevolution/mydatahelps-js";

export default async function (query: DeviceDataV2AggregateQuery): Promise<DeviceDataV2Aggregate[]> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this have to do with heart rate data - this seems like a general "queryAllDeviceDataV2" function more than anything to do with heart rates?

Copy link
Contributor

Choose a reason for hiding this comment

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

So seems like it would go in the same bucket as query all survey answers etc.


export default function IntradayHeartRateChart(props: IntradayHeartRateChartProps) {
const [data, setData] = useState<LineChartDataPoint[]>([]);
const [axis, setAxis] = useState<LineChartAxis>({ yRange: [0, 200], yIncrement: 30, xIncrement: props.aggregationIntervalAmount });
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think yRange needs to be specified at all or yIncrement - in my branch it worked fine without having to specify these

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The value of having some default x or y axis in place is so the chart renders when there is no data.
If nothing displays at all, the user may not be aware their device is is not connected. An empty chart could reinforce that the participant should connect their device

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we would want to mimic the "Daily data chart"'s no data state here which does not display a y-axis but does display the x-axis

Copy link
Contributor

Choose a reason for hiding this comment

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

@chrisnowak
Copy link
Contributor

Something kind of strange - the line seems to fly in from the right as the animation? I think we'd want to use whatever animation the default DailyDataChart uses for instance (which I think kind of comes up from the bottom)...


export interface IntradayHeartRateChartProps {
previewState?: IntradayHeartRatePreviewState,
dataSource: DeviceDataV2Namespace,
Copy link
Contributor

Choose a reason for hiding this comment

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

Per the spec this should accept multiple, e.g. Apple Health AND Fitbit:

It should query data for all data sources specified as props; if there happens to be data for 2 data sources for an interval it should use the data source that is first in the array (e.g. Fitbit by default)

dataPoints = dataPoints.sort((a, b) => a.date.getTime() - b.date.getTime());

if (dataPoints.length){
let yValues: number[] = dataPoints.map(d => d.value);
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you have a gap in the middle of the day e.g. where the user wasn't wearing their device?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@chrisnowak The line will continue without breaks.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right - my point is that is not the right behavior though, right? Since you need each heart rate data point to be represented at the time for the data point

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is corrected, and a story is in place for showing the behavior (line break).

@chrisnowak
Copy link
Contributor

Broadly still a little confused...my branch seems like it was a good chunk of the way there on the presentation layer...seemed mostly like it just needed:

  • Querying the data from the aggregates
  • Converting the data into the proper format; probably doing some math on the axes compared to the aggregate interval
  • Fixing up the gradients
  • Cleaning up a bit

This seems to have gone off in a a bit of a different direction and doesn't quite match the spec or the example screenshot?

@chrisnowak
Copy link
Contributor

Let's also figure out a way to get the line colors perfect - right now it obviously is a bit fuzzy - but the line should solidly change color when it is above the threshold...I can maybe help with that

…, generate all times for being able to showcase nulls as line gaps
@reneefromhold
Copy link
Collaborator Author

Something kind of strange - the line seems to fly in from the right as the animation? I think we'd want to use whatever animation the default DailyDataChart uses for instance (which I think kind of comes up from the bottom)...

This is now corrected when not using the date range navigator. When using the navigator the data will swing in from the same direction navigated to.

@reneefromhold
Copy link
Collaborator Author

Let's also figure out a way to get the line colors perfect - right now it obviously is a bit fuzzy - but the line should solidly change color when it is above the threshold...I can maybe help with that

@chrisnowak I've addressed most of the feedback, though getting the colors to sharply change at the threshold is still outstanding. Will work on that in next round of changes

@reneefromhold
Copy link
Collaborator Author

@chrisnowak The issue with line colors + incomplete data is fixed.

@ClaysGit
Copy link
Member

@CodiumAI-Agent /review

@CodiumAI-Agent
Copy link

PR Review 🔍

⏱️ Estimated effort to review [1-5]

4, due to the complexity and size of the PR, which includes multiple new components and helper functions, integration with external APIs, and complex data handling and visualization logic.

🧪 Relevant tests

No

⚡ Possible issues

Possible Bug: The getRandomInt function in IntradayHeartRateChart.data.ts does not handle the case where min is greater than max, which could lead to incorrect behavior or runtime errors.

Data Integrity: The loadForTest function in IntradayHeartRateChart.data.ts assumes that all data entries have unique timestamps. If two data points have the same timestamp, one will overwrite the other.

🔒 Security concerns

No

Code feedback:
relevant filesrc/components/container/IntradayHeartRateChart/IntradayHeartRateChart.data.ts
suggestion      

Consider adding error handling for the case where min is greater than max in the getRandomInt function to prevent potential runtime errors. [important]

relevant linefunction getRandomInt(min : number, max : number) {

relevant filesrc/components/container/IntradayHeartRateChart/IntradayHeartRateChart.data.ts
suggestion      

To ensure data integrity, modify the loadForTest function to handle cases where multiple data points have the same timestamp, perhaps by storing them in an array or using a more complex data structure. [important]

relevant linefunction loadForTest(data: DeviceDataV2Aggregate[]) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants