-
Notifications
You must be signed in to change notification settings - Fork 13
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
Refactor median-chart #1199
Refactor median-chart #1199
Conversation
hallieswan
commented
Jun 16, 2023
- Refactor median-chart to median-barchart to remove its dependency on dc.js
- Move styles shared between median-barchart and score-barchart into global stylesheet
#tooltip | ||
(mouseout)="hideTooltip()" | ||
></div> | ||
<svg #chart (window:resize)="onResize()"></svg> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an event binding so that the chart can be resized as the window is resized
component.xAxisLabel = xAxisLabel; | ||
component.yAxisLabel = yAxisLabel; | ||
|
||
fixture.detectChanges(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chart is created in ngOnChanges
, which is called in unit tests after fixture.detectChanges
is called the first time. Otherwise, ngOnChanges
isn't called when setting an input manually. Move fixture.detectChanges
into setUp
method, so that the correct data for the test is set when ngOnChanges
is called.
|
||
fixture.detectChanges(); | ||
element = fixture.nativeElement; | ||
const chart = element.querySelector('svg g'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the svg exists in the template and so may be present when the chart hasn't been created, use the first child added in createChart
to check whether the chart exists
it('should not render the chart if all values are negative', () => { | ||
const { chart } = setUp( | ||
SMALL_MOCK_DATA.map((obj) => { | ||
return { ...obj, medianlogcpm: -1 * obj.medianlogcpm }; | ||
}) | ||
); | ||
|
||
expect(element.querySelector('.chart-no-data')).toBeTruthy(); | ||
|
||
expect(chart).toBeFalsy(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Match the current behavior, which does not render the chart when all medianlogcpm values are negative
it('should render the chart if there is positive data', () => { | ||
const createChartSpy = spyOn(component, 'createChart').and.callThrough(); | ||
const { chart } = setUp(); | ||
|
||
expect(component.data?.length).toEqual(FULL_MOCK_DATA.length); | ||
expect(createChartSpy).toHaveBeenCalled(); | ||
expect(chart).toBeTruthy(); | ||
|
||
expect(element.querySelector('.chart-no-data')).toBeFalsy(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('should not render bars for negative values', () => { | ||
const { chart } = setUp([ | ||
{ ...SMALL_MOCK_DATA[0], medianlogcpm: -0.01 }, | ||
...SMALL_MOCK_DATA.slice(1), | ||
]); | ||
|
||
expect(chart?.querySelectorAll('rect').length).toEqual( | ||
SMALL_MOCK_DATA.length - 1 | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure that new implementation matches the current behavior, i.e. that negative medianlogcpm values are removed from the chart
it('should render the meaningful expression threshold even when all values are small', () => { | ||
const { chart } = setUp(SMALL_MOCK_DATA); | ||
|
||
expect( | ||
chart?.querySelector('.meaningful-expression-threshold-line') | ||
).toBeTruthy(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, the meaningful expression threshold is only shown when the max medianlogcpm value is >= the threshold. In the refactored chart, the threshold is always shown, so that the user has a reference for whether the value is meaningful, even when the values are very small. Happy to change this to match the current behavior, if that's preferred though!
develop | feature |
---|---|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a good question for @JessterB
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like an improvement to me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this when running the branch locally, but that's fine.
it('should alphabetize the x-axis values', () => { | ||
setUp(SMALL_MOCK_DATA); | ||
|
||
const sortedTissues = TISSUES.sort(); | ||
const xAxisTicks = element | ||
.querySelector('svg g .x-axis') | ||
?.querySelectorAll('.tick'); | ||
xAxisTicks?.forEach((val, index) => { | ||
expect(val.textContent).toEqual(sortedTissues[index]); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure new implementation matches current behavior, which alphabetizes the x-axis values
it('should not render the chart if there is no data', () => { | ||
const { chart } = setUp([]); | ||
|
||
expect(component.data?.length).toEqual(0); | ||
expect(element.querySelector('.chart-no-data')).toBeTruthy(); | ||
|
||
expect(chart).toBeFalsy(); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this._data = data | ||
.filter((el) => el.medianlogcpm && el.medianlogcpm > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove negative medianlogcpm values
@Input() set data(data: MedianExpression[]) { | ||
this._data = data | ||
.filter((el) => el.medianlogcpm && el.medianlogcpm > 0) | ||
.sort((a, b) => a.tissue.localeCompare(b.tissue)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And alphabetize the tissues, so the x-axis values are sorted
this.maxValueY = Math.max( | ||
this.MEANINGFUL_EXPRESSION_THRESHOLD, | ||
d3.max(this._data, (d) => d.medianlogcpm) || 0 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calculate a max value for the y-axis, so that the meaningful expression threshold is still shown if values are very small
// get the current width allotted to this chart or default | ||
getChartBoundingWidth(): number { | ||
return ( | ||
d3.select(this.chartRef.nativeElement).node()?.getBoundingClientRect() | ||
.width || 500 | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get the current width allotted to this chart, so that the width will update as the window is resized
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a suggestion, we can define 500 as a constant at the top of the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you might have moved a lot of the variables out for the scorebarchart component to the top. You could consider standardizing on that approach if you'd like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing, done in the next commit
.append('rect') | ||
.attr('class', 'medianbars') | ||
.attr('x', (d) => xScale(d.tissue) as number) | ||
.attr('y', (d) => yScale(d.medianlogcpm || 0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the medianlogcpm
field can be null, I needed to add || 0
(instead of d.medianlogcpm!
which results in a lint warning: Forbidden non-null assertion
). However, medianlogcpm
shouldn't ever be null here, since we remove objects with null medianlogcpm
values when setting _data. I handled the warning in a similar way throughout this component, but there's probably a better approach, so happy to adjust!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a sanity check, we might want to ask @JessterB if the data might ever be null in MongoDB and if that would possibly have any meaning in the charts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no null values in the data today. If there were nulls, I believe that we'd want to not plot them. That said, we have no plans to replace or update this data at this time. If we do replace the data someday, we should be able to remove any nulls during data processing anyway, so I wouldn't worry about it.
.append('g') | ||
.attr('class', 'x-axis') | ||
.attr('transform', `translate(0, ${innerHeight})`) | ||
.call(xAxis.tickSizeOuter(0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove outer x-axis ticks to more closely match the current implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love your screenshots, do you think you could add before / after for @JessterB? I figure she might have some thoughts :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dimension: any; | ||
group: any; | ||
|
||
constructor(private helperService: HelperService) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A good portion of this component is pulled directly from score-barchart, so in the future, could be worthwhile to refactor the shared methods into a base chart component that each chart extends
.attr('y1', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD)) | ||
.attr('y2', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the chart description, I expected that the threshold line would always be shown at log2(5), which is ~2.3. However on develop, when the max value of the chart is close to the threshold value, the threshold value is drawn at slightly less than 2.
I used yScale
to calculate the position for the threshold, so it is drawn at log2(5) in this case. However, I might've misunderstood how the threshold should be calculated, so happy to adjust as needed!
develop | feature |
---|---|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another great question for @JessterB. I haven't looked at this chart so I'm not 100% sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The threshold should be drawn at log2(5), which is what I see running this locally.
@HostListener('window:resize', ['$event']) | ||
onResize() { | ||
this.clearChart(); | ||
this.createChart(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handles resizing the chart when the window is resized --
feature_resizeMedianChart.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to use transitions when chart is resized on window resize to match the score-barchart transition:
median-chart-animation.mov
@@ -1,7 +1,7 @@ | |||
<div id="score-barchart"> | |||
<div id="score-barchart-tooltip" class="arrow-below tooltip-arrow" #tooltip (mouseout)="hideTooltip()"></div> | |||
<div id="score-barchart" class="chart-d3"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -1,84 +1,14 @@ | |||
$tooltip-color: #63676C; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into a new sass variable
position: relative; | ||
height: 350px; | ||
|
||
#score-barchart-tooltip { | ||
position: absolute; | ||
text-align: center; | ||
padding: 5px; | ||
font-size: 14px; | ||
background-color: $tooltip-color; | ||
color: white; | ||
display: none; | ||
z-index: 200; | ||
opacity: 0.9; | ||
width: 200px; | ||
cursor: pointer; | ||
border-radius: 5px; | ||
pointer-events: none; | ||
} | ||
|
||
.tooltip-arrow { | ||
&::before { | ||
content: ''; | ||
position: absolute; | ||
left: 50%; | ||
border: 10px solid transparent; | ||
transform: translateX(-50%); | ||
} | ||
|
||
&.arrow-below { | ||
transform: translate(calc(-50%), calc(-100% - 20px)); | ||
|
||
&::before { | ||
bottom: -9px; | ||
border-bottom: 0; | ||
border-top-color: $tooltip-color; | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into shared stylesheet - chart_d3.scss
.x-axis .tick { | ||
cursor: default; | ||
} | ||
|
||
.y-axis .tick { | ||
cursor: default; | ||
} | ||
|
||
text.x-axis-label { | ||
cursor: default; | ||
} | ||
|
||
text.y-axis-label { | ||
cursor: default; | ||
} | ||
|
||
.chart-no-data { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
height: 100%; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into shared stylesheet
svg { | ||
width: 100%; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure that the svg uses all available space, so that the chart's calculated width represents the total space this chart can take up and the chart will resize when the window is resized
.chart-no-data-text { | ||
font-size: var(--font-size-lg); | ||
font-style: italic; | ||
color: var(--color-gray-600); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added styling previously added by _chart.scss
src/styles/components/_chart_d3.scss
Outdated
&.arrow-above { | ||
transform: translate(calc(-50%), calc(50%)); | ||
|
||
&::before { | ||
top: -9px; | ||
border-top: 0; | ||
border-bottom-color: var(--color-tooltip); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add styling previously added by _chart.scss
position: relative; | ||
height: 350px; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The majority of this styling was moved from score-barchart.scss, so that the styles could be shared with median-barchart. I'll comment on the styling that isn't pulled from score-barchart
it('should show and hide tooltip', () => { | ||
setUp(); | ||
|
||
const tooltip = element.querySelector<HTMLElement>('#tooltip'); | ||
expect(tooltip?.textContent).toBeFalsy(); | ||
|
||
const xAxisTick = element.querySelector('svg g .x-axis .tick'); | ||
const mouseEnterEvent = new MouseEvent('mouseenter', { | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
xAxisTick?.dispatchEvent(mouseEnterEvent); | ||
|
||
expect(tooltip?.style.display).toEqual('block'); | ||
expect(tooltip?.textContent).toBeTruthy(); | ||
|
||
const mouseLeaveEvent = new MouseEvent('mouseleave', { | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
xAxisTick?.dispatchEvent(mouseLeaveEvent); | ||
|
||
expect(tooltip?.style.display).toEqual('none'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
develop
develop_tooltips.mov
feature
feature_tooltips.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. I haven't worked with this chart so some of the aesthetic/visual questions may be something Jess will have to weigh in on.
…between median-barchart and score-barchart into global stylesheet
d378ecb
to
bb5a078
Compare
@@ -0,0 +1,12 @@ | |||
<div id="median-barchart" #medianBarChartContainer class="chart-d3"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add #medianBarChartContainer
to get div size on window resize
private MIN_CHART_WIDTH = 500; | ||
private CHART_HEIGHT = 350; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move constants to top of the file
this.clearChart(); | ||
this.createChart(); | ||
} | ||
resizeChart = (divSize: number): void => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use transitions when the chart is resized on window resize
@@ -72,7 +72,7 @@ | |||
} | |||
|
|||
&.arrow-above { | |||
transform: translate(calc(-50%), calc(50%)); | |||
transform: translate(calc(-50%), calc(50% - 12px)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move tooltip closer to the x-axis label
…n threshold line when values are small
e52af66
to
8129a6d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good! I don't see any problem with merging and releasing this.