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

Refactor median-chart #1199

Merged
merged 6 commits into from
Mar 13, 2024

Conversation

hallieswan
Copy link
Contributor

  • 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>
Copy link
Contributor Author

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();
Copy link
Contributor Author

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');
Copy link
Contributor Author

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

Comment on lines 102 to 112
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();
});
Copy link
Contributor Author

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

Comment on lines +114 to +123
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();
});
Copy link
Contributor Author

@hallieswan hallieswan Jun 16, 2023

Choose a reason for hiding this comment

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

Case where medianlogcpm values are positive --

develop feature
main_ENSG00000100376_largerValues feature_ENSG00000100376_largerValues

Comment on lines 140 to 149
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
);
});
Copy link
Contributor Author

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

Comment on lines 178 to 184
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();
});
Copy link
Contributor Author

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
main_ENSG00000202337_lowValue feature_ENSG00000202337_lowValue

Copy link
Contributor

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

Copy link
Contributor

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!

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 see this when running the branch locally, but that's fine.

Comment on lines +186 to +196
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]);
});
});
Copy link
Contributor Author

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

Comment on lines +93 to +100
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();
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

develop feature
main_ENSG00000234956_noData feature_ENSG00000234956_noData

Comment on lines 48 to 61
this._data = data
.filter((el) => el.medianlogcpm && el.medianlogcpm > 0)
Copy link
Contributor Author

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));
Copy link
Contributor Author

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

Comment on lines 51 to 65
this.maxValueY = Math.max(
this.MEANINGFUL_EXPRESSION_THRESHOLD,
d3.max(this._data, (d) => d.medianlogcpm) || 0
);
Copy link
Contributor Author

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

Comment on lines 134 to 161
// get the current width allotted to this chart or default
getChartBoundingWidth(): number {
return (
d3.select(this.chartRef.nativeElement).node()?.getBoundingClientRect()
.width || 500
);
}
Copy link
Contributor Author

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

Copy link
Contributor

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.

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 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.

Copy link
Contributor Author

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))
Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor

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))
Copy link
Contributor Author

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

Copy link
Contributor

@sagely1 sagely1 Jun 28, 2023

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure! Here are some screenshots demonstrating the different x-axis tick options:

without outer ticks* with outer ticks
median-barchart-xAxisTicks-noOuterTick median-barchart-xAxisTicks-outerTicks

*implementation currently in this PR

dimension: any;
group: any;

constructor(private helperService: HelperService) {}
Copy link
Contributor Author

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

Comment on lines 247 to 270
.attr('y1', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD))
.attr('y2', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD))
Copy link
Contributor Author

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
main_ENSG00000183856_closeThreshold feature_ENSG00000183856_closeThrehsold

Copy link
Contributor

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.

Copy link
Contributor

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.

Comment on lines 255 to 259
@HostListener('window:resize', ['$event'])
onResize() {
this.clearChart();
this.createChart();
}
Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

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

nice

Copy link
Contributor Author

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">
Copy link
Contributor Author

@hallieswan hallieswan Jun 16, 2023

Choose a reason for hiding this comment

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

Allows this chart to use the shared styles

develop feature
main_ENSG00000100376_scoreBarChart feature_ENSG00000100376_scoreBarChart

@@ -1,84 +1,14 @@
$tooltip-color: #63676C;
Copy link
Contributor Author

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

Comment on lines 4 to 46
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;
}
}
}

Copy link
Contributor Author

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

Comment on lines -62 to -91
.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%;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved into shared stylesheet

Comment on lines +5 to +7
svg {
width: 100%;
}
Copy link
Contributor Author

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

Comment on lines +26 to +30
.chart-no-data-text {
font-size: var(--font-size-lg);
font-style: italic;
color: var(--color-gray-600);
}
Copy link
Contributor Author

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

Comment on lines 74 to 82
&.arrow-above {
transform: translate(calc(-50%), calc(50%));

&::before {
top: -9px;
border-top: 0;
border-bottom-color: var(--color-tooltip);
}
}
Copy link
Contributor Author

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

Comment on lines +2 to +3
position: relative;
height: 350px;
Copy link
Contributor Author

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

Comment on lines +198 to +222
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');
});
});
Copy link
Contributor Author

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

@hallieswan hallieswan marked this pull request as ready for review June 16, 2023 23:28
@hallieswan hallieswan requested a review from sagely1 June 16, 2023 23:28
@hallieswan hallieswan self-assigned this Jun 16, 2023
Copy link
Contributor

@sagely1 sagely1 left a 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
@hallieswan hallieswan force-pushed the refactor-median-chart branch 2 times, most recently from d378ecb to bb5a078 Compare July 7, 2023 21:29
@@ -0,0 +1,12 @@
<div id="median-barchart" #medianBarChartContainer class="chart-d3">
Copy link
Contributor Author

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

Comment on lines +43 to +44
private MIN_CHART_WIDTH = 500;
private CHART_HEIGHT = 350;
Copy link
Contributor Author

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 => {
Copy link
Contributor Author

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));
Copy link
Contributor Author

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

@hallieswan hallieswan force-pushed the refactor-median-chart branch from e52af66 to 8129a6d Compare July 10, 2023 23:15
@hallieswan hallieswan requested a review from sagely1 July 19, 2023 19:55
Copy link
Contributor

@sagely1 sagely1 left a comment

Choose a reason for hiding this comment

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

Looks good!

@sagely1 sagely1 requested a review from JessterB February 15, 2024 23:00
Copy link
Contributor

@JessterB JessterB left a 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.

@sagely1 sagely1 merged commit 50a9b1f into Sage-Bionetworks:develop Mar 13, 2024
3 checks passed
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.

3 participants