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

Survey Answer Data Chart #249

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

Survey Answer Data Chart #249

wants to merge 42 commits into from

Conversation

ajmenca
Copy link
Collaborator

@ajmenca ajmenca commented Apr 25, 2024

Overview

This PR adds a SurveyDataChart Container component. This chart will graph responses to a Survey over time (configured by specifying the Survey Name, Step Identifier, and optionally the Result Identifier). The presentation here was driven by a component I originally created for a customer where they have multiple surveys that calculate multiple scores per survey, and they want to see those scores side by side over time. As a specific example, the project was giving repeated instances of the Five Factor Wellness Inventory; this survey generates a total score and five second order scores, and the aim was to show all of those scores over time. The storybook entry I created is a simplified version of this:

image
image

The bigger swing I took here was to attempt to leverage the rendering code already established in the DailyDataChart by refactoring it into a more generic DataChart component that is used by both the DailyDataChart and the SurveyDataChart. This is a pattern that I think we should really be following more often; separating the responsibilities of retrieving data and presenting data. I've run into several cases in using the MDHUI library where I couldn't re-use one of the components because I needed to make the slightest tweak to the set of data being presented and couldn't because we're not separating these concerns (i.e. the SurveyTaskList gets a specific set of Tasks, rather than being a presentational component which renders a list of Tasks it's given, and we also provide an OpenSurveyTaskList which gets the data). So I took the plunge in this PR to try and start introducing and encouraging this pattern.

Behaviorally, the only change I ended up making to the rendering code was the addition of being able to display multiple sets of data on one graph (something we'd had in the DeviceDataMonthChart but then did not support in the DailyDataChart). I did this by having an optional dataKeys parameter for the DataChart. If you don't pass any, it assumes a basic default key of value which is how the DailyDataChart formats its basic data. So the Survey Chart can support lines, as shown above, as well as area and bar presentations (though you can get some truly awful color combinations of areas):
image
image

I also made it so that the "parent" container is the one who specifies how the hover detail works. This was mostly for handling rather different formatting if there's only one line versus multiple.

Backwards compatibility for the DailyDataChart should be unchanged; it's external interface was unmodified. All stories for it should still work, as verification.

Security

REMINDER: All file contents are public.

  • I have ensured no secure credentials or sensitive information remain in code, metadata, comments, etc.
    • Please verify that you double checked that .storybook/preview.js does not contain your participant access key details.
    • There are no temporary testing changes committed such as API base URLs, access tokens, print/log statements, etc.
  • 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

Summary by CodeRabbit

  • New Features

    • Introduced SurveyDataChart for rendering various types of survey data charts.
    • Added support for a new interval type "SixMonth" in DateRangeCoordinator and DateRangeNavigator.
    • Enhanced TimeSeriesChart with customizable options for line, bar, and area charts.
  • Improvements

    • Updated DailyDataChart to use the new TimeSeriesChart component for better consistency and customization.
    • Improved interval-based title generation in date helpers to include "SixMonth" intervals.
  • Bug Fixes

    • Fixed data processing and tooltip rendering in SurveyDataChart for better accuracy and user experience.

- Break the rendering portion of the DailyDataChart out from the Data Querying part
- Reuse the rendering to grab data from surveys instead.
- Support multiple lines on the line graph.
- Simplify presentation by always assuming an array of dataKeys but defaulting to a single entry array.
- Support multiple colors for all chart types.
@basyl-durnan
Copy link

Very cool!

  • Looks like you can graph multiple survey results on the same graph. Can those be results from the same and different surveys?
  • I noticed you said "survey step". While I imagine it will almost always be a survey step's result, would they be able to handle form item results as well?
  • Is there any validation to making sure we get numeric values? I of course hope study teams put validation on the result, but... you never know. How would it handle a non-numeric value?
  • What happens if a survey is completed multiple times in one day? I'm not sure I have an answer for what to do... some people may want to visually see all of them, others may want an average.
  • Forgive me for not trusting myself to parse the code behind this, but I assume there is some check in case surveys share similar field names, right? We probably specify the survey?
  • You can click on the bars to also see a legend, right?

@basyl-durnan
Copy link

Adding @Kbalon in case she has some thoughts and/or feedback.

@ajmenca
Copy link
Collaborator Author

ajmenca commented Apr 25, 2024

  • Looks like you can graph multiple survey results on the same graph. Can those be results from the same and different surveys?

Yes, each line is configured individually and the data is queried for it individually, so you can combine things.

  • I noticed you said "survey step". While I imagine it will almost always be a survey step's result, would they be able to handle form item results as well?

Yes, the configuration actually takes both a step identifier and an (optional) result identifier.

  • Is there any validation to making sure we get numeric values? I of course hope study teams put validation on the result, but... you never know. How would it handle a non-numeric value?

Not at the moment. A non numerical value will cause a failure in the graph; I can update it to just throw those values away if they come up though (which would really be the only option to proceed and graph what is valid).

  • What happens if a survey is completed multiple times in one day? I'm not sure I have an answer for what to do... some people may want to visually see all of them, others may want an average.

I can verify with a test, but I think the way I'm parsing the data, the later answer would be the one that would show up. I agree there's not exactly a predictable expectation for this sort of case. Seeing all the points wouldn't really be reasonable for this type of graph though; i.e. when you're making a line, which point do you run the line through?

  • Forgive me for not trusting myself to parse the code behind this, but I assume there is some check in case surveys share similar field names, right? We probably specify the survey?

Yup, survey name, step identifier, result identifier. I'll update the PR description for clarity.

  • You can click on the bars to also see a legend, right?

Hover, but yes - same behavior.

@basyl-durnan
Copy link

I think an error is probably best for non-numerical values. If someone is trying to graph results, we probably want to make it obvious there is an error in the survey values. The study team should enforce validation or a picklist.

Great point about showing multiple results on the same day. Let's revisit averages and such if that comes up.

Thanks!

@chrisnowak
Copy link
Contributor

I would definitely just throw the values away if it's non-numeric rather than have the graph error; makes it robust if you ever change anything in the survey definition etc...one string could crash the whole graph etc.

@basyl-durnan
Copy link

@chrisnowak I worry about a team that made an error and not realizing it for some time since it wouldn't stand out. However, I could imagine there being a "skip" or "prefer not to answer" option and they wouldn't want an error because of that; so probably best to dump.

Copy link
Collaborator

@reneefromhold reneefromhold left a comment

Choose a reason for hiding this comment

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

Overall, lg and worthwhile to have.
I had a few nits and comments on unused items.
I added a NoData story to the SurveyData component, but it was erroring (code below). Could be I am misunderstanding how to set it up. Can we have Defaults, NoData, and Live versions for the SurveyData.

/*
export const NoData = {
args: {
title: "No Data",
options: {
domainMin: 0,
lineColor: ["#e41a1c", "#377eb8", "#4daf4a"],
areaColor: ["#d41a1c", "#277eb8", "#3daf4a"]
},
intervalType: "Week",
weekStartsOn: "6DaysAgo",
lines: [{ label: "Creative Self", surveyName: "FFWEL", stepIdentifier: "CreativeSelf", resultIdentifier: "CreativeSelf", stroke: "#e41a1c" },
{ label: "Coping Self", surveyName: "FFWEL", stepIdentifier: "CopingSelf", resultIdentifier: "CopingSelf", stroke: "#377eb8" },
{ label: "Social Self", surveyName: "FFWEL", stepIdentifier: "SocialSelf", resultIdentifier: "SocialSelf", stroke: "#4daf4a" }],
valueFormatter: (value: number) => Number(value.toFixed(0)).toLocaleString(),
chartType: "Area",
previewDataProvider: (start: Date, end: Date) => {
const data: SurveyAnswersPage[] = [];
return Promise.resolve(data);
}
},
render: render
}; */

@chrisnowak
Copy link
Contributor

Again, as I mentioned a few times across the PR the use case for Survey Data wanting a 6 month view is pretty straight forward. The program that I was initially supporting has their survey cycle on a monthly cadence. So to graph those changes over time, you need more than the 1 month window that we currently support. I chose 6 months here as sort of an "intermediate" window for monthly data; 3 months wouldn't show enough data, and 1 year the graph would look mostly empty for a long range of time. 6 months was a happy medium in this case. It also happens to match the length of time that participants are in the specific program I'd worked with - but that's more of a coincidence than anything.

Do you think this would have satisfied that use case in a really solid manner then? I guess i'm feeling when it comes to view builder where this is just gonna not work/look good in a lot of cases...DateRangeCoordinator in view builder can't be defaulted to a specific start date for instance, so stuff is gonna move on the chart...feels like we're gonna have a lot of white space at the beginning/end of the graph for a 6 month interval etc...so for your case does it feel like it would be good enough out of box

In any case, if works for a 1 month / 1 week interval, that satisfies most of the use cases i've seen so addition of 6 month isn't a big deal to me, but I would probably not make it an option for DailyDataChart since it might cause a poor UX (that much data could take a long time to query in many cases, like Apple Health steps)...

mostly because it feels like we're heavily pushing toward the DateRangeCoordinator

DateRangeCoordinator/DateRangeContext are really designed for specific simplified intervals; I don't think anything is saying that it's appropriate for all use cases of time series data; I think 1000% there is room for stuff that doesn't sit in a date range coordinator...it's just that we have a lot of use cases existing where it makes sense to navigate on a day/week/month basis...plenty of summary use cases where we may want to graph longer-term longitudinal data and those likely require a different approach (i.e. calculating date range based on data)

@ajmenca
Copy link
Collaborator Author

ajmenca commented May 6, 2024

Do you think this would have satisfied that use case in a really solid manner then? I guess i'm feeling when it comes to view builder where this is just gonna not work/look good in a lot of cases...DateRangeCoordinator in view builder can't be defaulted to a specific start date for instance, so stuff is gonna move on the chart...feels like we're gonna have a lot of white space at the beginning/end of the graph for a 6 month interval etc...so for your case does it feel like it would be good enough out of box

I think this would have gotten me 99% of the way there. You're correct - from within ViewBuilder I would have still been slightly stuck - but I didn't build the project in ViewBuilder. So I would have probably used the DRC but passed it an explicit start date, because it does support that, it's just not being exposed. Or, I would have not had the DRC at all and just specified the start interval and intervalType directly on the SurveyDataChart. But, if ViewBuilder supported setting the startInterval on either the SurveyDataChart with no DRC or setting it on the DRC, I would have been totally fine.

@chrisnowak
Copy link
Contributor

Got it, makes sense. I mostly wanted to trace a path forward to view builder and make sure we were satisfying 1 anchor use case. Having 6 months as an option on SurveyAnswerChart makes sense to me; it is mostly on DateRangeCoordinator where it feels weird; since as you pointed out you may not have used it there at all and just anchored it to a specific date (which feels like the right answer to me in your case if the study is less than 6 mos long)

So maybe when we add this to view builder I would suggest we:

  • Do NOT add the 6 month option as an option for the date range coordinator itself; it probably will blow up DailyDataCharts etc.
  • Do add it to this new SurveyAnswerChart component as a possible interval type
  • Also add an editable "intervalStart" field which could be a string expression, e.g. it could be "{{enrollmentDate}}" or "05-01-2024", such that the chart could be anchored/stuck to a specific time period globally or per-participant
    • (This could be added to DailyDataChart as well)

That's my 2c though, i'll try to do a full review of this PR tomorrow

@ajmenca
Copy link
Collaborator Author

ajmenca commented May 7, 2024

Got it, makes sense. I mostly wanted to trace a path forward to view builder and make sure we were satisfying 1 anchor use case. Having 6 months as an option on SurveyAnswerChart makes sense to me; it is mostly on DateRangeCoordinator where it feels weird; since as you pointed out you may not have used it there at all and just anchored it to a specific date (which feels like the right answer to me in your case if the study is less than 6 mos long)

So maybe when we add this to view builder I would suggest we:

  • Do NOT add the 6 month option as an option for the date range coordinator itself; it probably will blow up DailyDataCharts etc.

  • Do add it to this new SurveyAnswerChart component as a possible interval type

  • Also add an editable "intervalStart" field which could be a string expression, e.g. it could be "{{enrollmentDate}}" or "05-01-2024", such that the chart could be anchored/stuck to a specific time period globally or per-participant

    • (This could be added to DailyDataChart as well)

That's my 2c though, i'll try to do a full review of this PR tomorrow

Most of this I agree with; I do have some extra thoughts on it that are not really relevant here. Maybe worth doing a quick re-group on Slack with other folks that have been deal with dates/graphs/view builder as a whole - or maybe we just have another long discussion on a View Builder Issue instead.

@ClaysGit
Copy link
Member

@CodiumAI-Agent /review

@CodiumAI-Agent
Copy link

PR Review 🔍

⏱️ Estimated effort to review [1-5]

4, because the PR introduces significant changes across multiple components, including refactoring and new features. The complexity of integrating a new chart type and modifying existing components to support new data types requires careful review to ensure functionality and maintainability.

🧪 Relevant tests

No

⚡ Possible issues

Possible Bug: The loadCurrentInterval function in SurveyDataChart.tsx does not handle errors from the previewDataProvider or queryAllSurveyAnswers promises. This could lead to unhandled promise rejections if the data fetching fails.

Data Integrity: The processPages function in SurveyDataChart.tsx assumes that the order of data returned from multiple promises matches the order of charts. This could lead to incorrect data being displayed if the promises resolve in a different order.

🔒 Security concerns

No

Code feedback:
relevant filesrc/components/container/SurveyDataChart/SurveyDataChart.tsx
suggestion      

Add error handling for promises in the loadCurrentInterval function to manage failed data fetching gracefully. This is important to prevent the application from crashing and to provide a better user experience by handling errors properly. [important]

relevant lineprops.previewDataProvider(intervalStart, intervalEnd)

relevant filesrc/components/container/SurveyDataChart/SurveyDataChart.tsx
suggestion      

Ensure the order of data corresponds to the charts configuration by implementing a more robust data matching mechanism in processPages. This could involve mapping responses directly to their respective chart configurations based on identifiers rather than relying on the order. [important]

relevant linevar dataRequests = props.charts.map(l => queryAllSurveyAnswers({

relevant filesrc/components/container/DailyDataChart/DailyDataChart.tsx
suggestion      

Consider adding a cleanup function in the useEffect hook to handle component unmount scenarios. This would prevent potential memory leaks and ensure that any ongoing asynchronous operations are canceled if the component is unmounted. [medium]

relevant line}, [props.intervalType, props.weekStartsOn, dateRangeContext]);

relevant filesrc/components/container/DailyDataChart/DailyDataChart.tsx
suggestion      

Refactor the if conditions checking intervalType for setting intervalStart to use a switch-case statement or a mapping object for cleaner and more maintainable code. This change would make it easier to add or modify behaviors based on intervalType in the future. [medium]

relevant lineif(dateRangeContext.intervalType === "SixMonth") {

Copy link
Contributor

@chrisnowak chrisnowak left a comment

Choose a reason for hiding this comment

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

I got pulled away from my review due to some other issues, so gonna post my comments now after my initial first pass - but I still want to dig in a bit deeper into TimeSeriesChart

src/components/container/DailyDataChart/DailyDataChart.tsx Outdated Show resolved Hide resolved
src/components/container/DailyDataChart/DailyDataChart.tsx Outdated Show resolved Hide resolved
src/components/container/DailyDataChart/DailyDataChart.tsx Outdated Show resolved Hide resolved
}

export interface BarChartOptions {
barColor?: ColorDefinition | ColorDefinition[],
Copy link
Contributor

Choose a reason for hiding this comment

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

Would definitely NOT do parallel arrays here...we should definitely figure out another way to structure this such that colors are attached to the actual series that is being represented here; it took me a while just to figure out how coloring each survey answer would work.

Realistically if we think about how you'd set this up in view builder; you'd want to add a survey answer to the chart and specify a color specific to that answer...I would suggest that each dataKey here is an object that has a color associated with it, and your SurveyAnswerChartParameters (or whatever we name it) has a color on it

Copy link
Contributor

Choose a reason for hiding this comment

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

So basically to figure out the color of the bar it would:

  • Check the thresholds; if over a threshold, use the threshold color
  • If the dataKey has a color specified, use the dataKey color
  • If the options has a color specified, use that color
  • Otherwise use var(--mdhui-color-primary)

Copy link
Collaborator Author

@ajmenca ajmenca May 21, 2024

Choose a reason for hiding this comment

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

I wrestled with this a bit because the parallel arrays didn't make me very happy either. I was trying to walk a balance of not having 3 different interfaces for doing things between 3 different related but different widgets (the DailyDataChart, the SurveyAnswerChart, and the TimeSeriesChart). I was trying to avoid having the DailyData and SurveyAnswer have "custom" interfaces that they each "adapted" into something for the TimeSeriesChart because that seemed more confusing. In particular I really wanted them to be able to share these base FooChartOptions classes. And in doing so, I was trying to not break the current interface of the DailyDataChart, so that any current users could keep on working, and I wouldn't have to change View Builder.

Honestly, I feel strongly that changing the dataKeys for the TimeSeriesChart into objects instead of the current string array doesn't make it clearer. I think that having two separate places where you can define the colors is harder to discover and understand as an end user, and the fact that we'd have to convey the hierarchy of where we look for and prioritize those colors is a situation I'd prefer to avoid.

Here's two suggestions:

  1. Move the dataKeys property off of the TimeSeriesChartProps and into each FooChartOptions.
export interface LineChartOptions {
    lineColor?: ColorDefinition | ColorDefinition[],
    dataKeys?: undefined | string[],
    connectNulls?: boolean,
    domainMin?: number | "Auto"
}

export interface BarChartOptions {
    barColor?: ColorDefinition | ColorDefinition[],
    dataKeys?: undefined | string[],
    thresholds?: BarChartThreshold[]
}

This at least colocates the parallel arrays so they're next to each other.

  1. Similar solution, but more like your suggestion, again removing the dataKeys property from TimeSeriesChart:
export interface LineChartOptions {
    lineColor?: ColorDefinition,
    seriesDefinition? : undefined | { dataKey: string, lineColor: ColorDefinition }[]
    connectNulls?: boolean,
    domainMin?: number | "Auto"
}

export interface BarChartOptions {
    barColor?: ColorDefinition,
    seriesDefinition? : undefined | { dataKey: string, barColor: ColorDefinition }[]
    thresholds?: BarChartThreshold[]
}

This has the issue of having to "prioritize" things, but at least in this case the properties are much closer together and maybe its more obvious?


export interface LineChartOptions {
lineColor?: ColorDefinition | ColorDefinition[],
connectNulls?: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm yeah, for survey answer data I can see this make sense since you might have large gaps in between

coderabbitai[bot]

This comment was marked as off-topic.

- Model Month behavior; by default go 6 months back from the current month, all the way back to the 1st.
- Fix tick mark generation on TimeSeriesChart for multiple possibilities of how the 6 month span starts, to always get the ticks in predictable spots.
- Add stories to demonstrate this behavior.
…eriesChart

- This was preventing the Loading indicator from ever being presented.
Can re-use because DailyDataChart is prevented from calling with 6Month parameter by other type checking.
coderabbitai[bot]

This comment was marked as off-topic.

thresholds?: BarChartThreshold[]
}

export interface BarChartThreshold {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we simplify this name to Threshold?

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 would prefer not to. I think having such a generic name isn't helpful to have as an export when it's use is pretty narrow to its usage in this class.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm indifferent to what it is named if Threshold is too generic.
ChartThresholds is another option.

Given a threshold lines are shown in both bar and line, it is confusing for the name to be bar chart specific.

@CareEvolution CareEvolution deleted a comment from coderabbitai bot May 21, 2024
@CareEvolution CareEvolution deleted a comment from ClaysGit May 21, 2024
@CareEvolution CareEvolution deleted a comment from coderabbitai bot May 21, 2024
@CareEvolution CareEvolution deleted a comment from coderabbitai bot May 21, 2024
@CareEvolution CareEvolution deleted a comment from ClaysGit May 21, 2024
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

7 participants