diff --git a/src/app/features/charts/charts.module.ts b/src/app/features/charts/charts.module.ts
index 1f1b2b02..c00cb82d 100644
--- a/src/app/features/charts/charts.module.ts
+++ b/src/app/features/charts/charts.module.ts
@@ -7,6 +7,7 @@ import {
BoxPlotComponent,
CandlestickChartComponent,
MedianChartComponent,
+ MedianBarChartComponent,
RowChartComponent,
ScoreChartComponent,
ScoreBarChartComponent,
@@ -19,6 +20,7 @@ import {
BoxPlotComponent,
CandlestickChartComponent,
MedianChartComponent,
+ MedianBarChartComponent,
RowChartComponent,
ScoreChartComponent,
ScoreBarChartComponent,
@@ -30,6 +32,7 @@ import {
BoxPlotComponent,
CandlestickChartComponent,
MedianChartComponent,
+ MedianBarChartComponent,
RowChartComponent,
ScoreChartComponent,
ScoreBarChartComponent,
diff --git a/src/app/features/charts/components/index.ts b/src/app/features/charts/components/index.ts
index 189d12e4..9d00708b 100644
--- a/src/app/features/charts/components/index.ts
+++ b/src/app/features/charts/components/index.ts
@@ -1,6 +1,7 @@
export * from './box-plot-chart';
export * from './candlestick-chart';
export * from './median-chart';
+export * from './median-barchart';
export * from './row-chart';
export * from './score-chart';
export * from './score-barchart';
diff --git a/src/app/features/charts/components/median-barchart/index.ts b/src/app/features/charts/components/median-barchart/index.ts
new file mode 100644
index 00000000..a587af3d
--- /dev/null
+++ b/src/app/features/charts/components/median-barchart/index.ts
@@ -0,0 +1 @@
+export * from './median-barchart.component';
diff --git a/src/app/features/charts/components/median-barchart/median-barchart.component.html b/src/app/features/charts/components/median-barchart/median-barchart.component.html
new file mode 100644
index 00000000..cdccce2d
--- /dev/null
+++ b/src/app/features/charts/components/median-barchart/median-barchart.component.html
@@ -0,0 +1,12 @@
+
diff --git a/src/app/features/charts/components/median-barchart/median-barchart.component.scss b/src/app/features/charts/components/median-barchart/median-barchart.component.scss
new file mode 100644
index 00000000..530405ea
--- /dev/null
+++ b/src/app/features/charts/components/median-barchart/median-barchart.component.scss
@@ -0,0 +1,26 @@
+#median-barchart {
+ .x-axis-label, .y-axis-label {
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ color: var(--color-chart-axis-label);
+ fill: var(--color-chart-axis-label);
+ }
+
+ .y-axis .tick {
+ text {
+ font-size: var(--font-size-sm);
+ }
+ }
+
+ .x-axis .tick {
+ text {
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ }
+ }
+
+ .bar-labels {
+ font-size: var(--font-size-sm);
+ text-anchor: middle;
+ }
+}
\ No newline at end of file
diff --git a/src/app/features/charts/components/median-barchart/median-barchart.component.spec.ts b/src/app/features/charts/components/median-barchart/median-barchart.component.spec.ts
new file mode 100644
index 00000000..6b73bc01
--- /dev/null
+++ b/src/app/features/charts/components/median-barchart/median-barchart.component.spec.ts
@@ -0,0 +1,219 @@
+// -------------------------------------------------------------------------- //
+// External
+// -------------------------------------------------------------------------- //
+import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+// -------------------------------------------------------------------------- //
+// Internal
+// -------------------------------------------------------------------------- //
+import { MedianBarChartComponent } from './';
+import { HelperService } from '../../../../core/services';
+import { geneMock1 } from '../../../../testing';
+import { MedianExpression } from '../../../../models/genes';
+
+// -------------------------------------------------------------------------- //
+// Tests
+// -------------------------------------------------------------------------- //
+const XAXIS_LABEL = 'BRAIN REGION';
+const YAXIS_LABEL = 'LOG2CPM';
+const FULL_MOCK_DATA = geneMock1.median_expression;
+const TISSUES = ['TCX', 'PHG', 'STG'];
+const SMALL_MOCK_DATA = [
+ {
+ min: -2.54173091337051,
+ first_quartile: -1.03358030635935,
+ median: 0.483801733963266,
+ mean: -0.517398199667356,
+ third_quartile: -0.0800759845652829,
+ max: 2.32290808871289,
+ tissue: TISSUES[0],
+ },
+ {
+ min: -2.44077907413711,
+ first_quartile: -0.592671867557559,
+ median: 0.013739530502129,
+ mean: -0.0324143336865,
+ third_quartile: 0.49577213202412,
+ max: 2.23019575245731,
+ tissue: TISSUES[1],
+ },
+ {
+ min: -5.03189866356294,
+ first_quartile: -1.02644563959975,
+ median: 0.176348063122062,
+ mean: -0.323038107200895,
+ third_quartile: 0.391874711168331,
+ max: 1.9113258251877,
+ tissue: TISSUES[2],
+ },
+];
+
+describe('Component: BarChart - Median', () => {
+ let fixture: ComponentFixture;
+ let component: MedianBarChartComponent;
+ let element: HTMLElement;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [MedianBarChartComponent],
+ imports: [RouterTestingModule],
+ providers: [HelperService],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MedianBarChartComponent);
+ component = fixture.componentInstance;
+ });
+
+ const setUp = (
+ data: MedianExpression[] = FULL_MOCK_DATA,
+ xAxisLabel: string = XAXIS_LABEL,
+ yAxisLabel: string = YAXIS_LABEL
+ ) => {
+ component.data = data;
+ component.xAxisLabel = xAxisLabel;
+ component.yAxisLabel = yAxisLabel;
+
+ fixture.detectChanges();
+ element = fixture.nativeElement;
+ const chart = element.querySelector('svg g');
+ return { chart };
+ };
+
+ it('should create', () => {
+ setUp();
+ expect(component).toBeTruthy();
+ });
+
+ 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();
+ });
+
+ it('should not render the chart if all values are negative', () => {
+ const { chart } = setUp(
+ SMALL_MOCK_DATA.map((obj) => {
+ return { ...obj, median: -1 * obj.median };
+ })
+ );
+
+ expect(element.querySelector('.chart-no-data')).toBeTruthy();
+
+ expect(chart).toBeFalsy();
+ });
+
+ 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();
+ });
+
+ it('should render the chart axes', () => {
+ const { chart } = setUp();
+
+ expect(chart?.querySelector('.x-axis')).toBeTruthy();
+ expect(chart?.querySelector('.y-axis')).toBeTruthy();
+ });
+
+ it('should render the correct number of bars', () => {
+ const { chart } = setUp();
+
+ expect(chart?.querySelectorAll('rect').length).toEqual(
+ FULL_MOCK_DATA.length
+ );
+ });
+
+ it('should not render bars for negative values', () => {
+ const { chart } = setUp([
+ { ...SMALL_MOCK_DATA[0], median: -0.01 },
+ ...SMALL_MOCK_DATA.slice(1),
+ ]);
+
+ expect(chart?.querySelectorAll('rect').length).toEqual(
+ SMALL_MOCK_DATA.length - 1
+ );
+ });
+
+ it('should render the bar labels', () => {
+ const { chart } = setUp();
+
+ expect(chart?.querySelectorAll('text.bar-labels').length).toEqual(
+ FULL_MOCK_DATA.length
+ );
+ });
+
+ it('should render the labels for both axes', () => {
+ const { chart } = setUp();
+
+ expect(chart?.querySelector('.x-axis-label')?.textContent).toEqual(
+ XAXIS_LABEL
+ );
+ expect(chart?.querySelector('.y-axis-label')?.textContent).toEqual(
+ YAXIS_LABEL
+ );
+ });
+
+ it('should render the meaningful expression threshold when values are larger than threshold', () => {
+ const { chart } = setUp();
+
+ expect(
+ chart?.querySelector('.meaningful-expression-threshold-line')
+ ).toBeTruthy();
+ });
+
+ it('should not render the meaningful expression threshold when all values are smaller than threshold', () => {
+ const { chart } = setUp(SMALL_MOCK_DATA);
+
+ expect(
+ chart?.querySelector('.meaningful-expression-threshold-line')
+ ).toBeFalsy();
+ });
+
+ 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]);
+ });
+ });
+
+ it('should show and hide tooltip', () => {
+ setUp();
+
+ const tooltip = element.querySelector('#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');
+ });
+});
diff --git a/src/app/features/charts/components/median-barchart/median-barchart.component.ts b/src/app/features/charts/components/median-barchart/median-barchart.component.ts
new file mode 100644
index 00000000..810ac541
--- /dev/null
+++ b/src/app/features/charts/components/median-barchart/median-barchart.component.ts
@@ -0,0 +1,318 @@
+// -------------------------------------------------------------------------- //
+// External
+// -------------------------------------------------------------------------- //
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ HostListener,
+ Input,
+ OnChanges,
+ OnDestroy,
+ SimpleChanges,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import * as d3 from 'd3';
+
+// -------------------------------------------------------------------------- //
+// Internal
+// -------------------------------------------------------------------------- //
+import { MedianExpression } from '../../../../models';
+import { HelperService } from '../../../../core/services';
+
+// -------------------------------------------------------------------------- //
+// Component
+// -------------------------------------------------------------------------- //
+@Component({
+ selector: 'median-barchart',
+ templateUrl: './median-barchart.component.html',
+ styleUrls: ['./median-barchart.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+})
+export class MedianBarChartComponent implements OnChanges, AfterViewInit, OnDestroy
+{
+ private chartInitialized = false;
+ private tooltipInitialized = false;
+ private _data: MedianExpression[] = [];
+ private chart!: d3.Selection;
+ private tooltip!: d3.Selection;
+ private MEANINGFUL_EXPRESSION_THRESHOLD = Math.log2(5);
+ private maxValueY = -1;
+
+ private MIN_CHART_WIDTH = 500;
+ private CHART_HEIGHT = 350;
+ private chartMargin = { top: 20, right: 20, bottom: 65, left: 65 };
+ private chartXScale!: d3.ScaleBand;
+ private chartXAxisDrawn!: d3.Selection;
+ private chartXAxisLabel!: d3.Selection;
+ private chartBars!: d3.Selection;
+ private chartScoreLabels!: d3.Selection;
+ private chartThresholdLine!: d3.Selection;
+ private shouldShowThresholdLine = false;
+
+ private resizeTimer: ReturnType | number = 0;
+
+ get data() {
+ return this._data;
+ }
+ @Input() set data(data: MedianExpression[]) {
+ this._data = data
+ .filter((el) => el.median && el.median > 0)
+ .sort((a, b) => a.tissue.localeCompare(b.tissue));
+ this.maxValueY = d3.max(this._data, (d) => d.median) || 0;
+ this.shouldShowThresholdLine = this.MEANINGFUL_EXPRESSION_THRESHOLD <= this.maxValueY;
+ }
+
+ @Input() shouldResize = true;
+ @Input() xAxisLabel = '';
+ @Input() yAxisLabel = 'LOG2 CPM';
+
+ @ViewChild('medianBarChartContainer') medianBarChartContainer: ElementRef = {} as ElementRef;
+ @ViewChild('chart') chartRef: ElementRef = {} as ElementRef;
+ @ViewChild('tooltip') tooltipRef: ElementRef = {} as ElementRef;
+
+ constructor(private helperService: HelperService) {}
+
+ @HostListener('window:resize', ['$event.target'])
+ onResize() {
+ if (this.shouldResize && this.chartInitialized) {
+ const self = this;
+ const divSize = this.medianBarChartContainer.nativeElement.getBoundingClientRect().width;
+ clearTimeout(this.resizeTimer);
+ this.resizeTimer = setTimeout(() => {
+ self.resizeChart(divSize);
+ }, 100);
+ }
+ };
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (
+ (changes._data && !changes._data.firstChange) ||
+ (changes.xAxisLabel && !changes.xAxisLabel.firstChange) ||
+ (changes.yAxisLabel && !changes.yAxisLabel.firstChange)
+ ) {
+ if (this._data.length === 0) {
+ this.clearChart();
+ this.hideChart();
+ } else {
+ this.clearChart();
+ this.showChart();
+ this.createChart();
+ }
+ }
+ }
+
+ ngAfterViewInit(): void {
+ if (this._data.length === 0) this.hideChart();
+ else this.createChart();
+ }
+
+ ngOnDestroy(): void {
+ this.destroyChart();
+ }
+
+ clearChart() {
+ const svg = d3.select(this.chartRef.nativeElement);
+ svg.selectAll('*').remove();
+ }
+
+ hideChart() {
+ const svg = d3.select(this.chartRef.nativeElement);
+ svg.style('display', 'none');
+ }
+
+ showChart() {
+ const svg = d3.select(this.chartRef.nativeElement);
+ svg.style('display', 'block');
+ }
+
+ destroyChart() {
+ if (this.chartInitialized) this.chart.remove();
+ if (this.tooltipInitialized) this.tooltip.remove();
+ }
+
+ showTooltip(text: string, x: number, y: number): void {
+ this.tooltip = d3
+ .select(this.tooltipRef.nativeElement)
+ .style('left', `${x}px`)
+ .style('top', `${y}px`)
+ .style('display', 'block')
+ .html(text);
+ this.tooltipInitialized = true;
+ }
+
+ hideTooltip() {
+ if (this.tooltipInitialized) {
+ this.tooltip.style('display', 'none');
+ }
+ }
+
+ getBarCenterX(tissue: string, xScale: d3.ScaleBand): number {
+ return (xScale(tissue) || 0) + xScale.bandwidth() / 2;
+ }
+
+ // get the current width allotted to this chart or default
+ getChartBoundingWidth(): number {
+ return (
+ d3.select(this.chartRef.nativeElement).node()?.getBoundingClientRect()
+ .width || this.MIN_CHART_WIDTH
+ );
+ }
+
+ createChart() {
+ if (this._data.length > 0) {
+ const barColor = this.helperService.getColor('secondary');
+ const width = this.getChartBoundingWidth();
+ const height = this.CHART_HEIGHT;
+ const innerWidth = width - this.chartMargin.left - this.chartMargin.right;
+ const innerHeight = height - this.chartMargin.top - this.chartMargin.bottom;
+
+ this.chart = d3
+ .select(this.chartRef.nativeElement)
+ .attr('width', width)
+ .attr('height', height)
+ .append('g')
+ .attr('transform', `translate(${this.chartMargin.left}, ${this.chartMargin.top})`);
+
+ // SCALES
+ this.chartXScale = d3
+ .scaleBand()
+ .domain(this._data.map((d) => d.tissue))
+ .range([0, innerWidth])
+ .padding(0.2);
+
+ const yScale = d3
+ .scaleLinear()
+ .domain([0, this.maxValueY])
+ .nice()
+ .range([innerHeight, 0]);
+
+ // BARS
+ this.chartBars = this.chart
+ .selectAll('.medianbars')
+ .data(this._data)
+ .enter()
+ .append('rect')
+ .attr('class', 'medianbars')
+ .attr('x', (d) => this.chartXScale(d.tissue) as number)
+ .attr('y', (d) => yScale(d.median || 0))
+ .attr('width', this.chartXScale.bandwidth())
+ .attr('height', (d) => innerHeight - yScale(d.median || 0))
+ .attr('fill', barColor);
+
+ // SCORE LABELS
+ this.chartScoreLabels = this.chart
+ .selectAll('.bar-labels')
+ .data(this._data)
+ .enter()
+ .append('text')
+ .attr('class', 'bar-labels')
+ .attr('x', (d) => this.getBarCenterX(d.tissue, this.chartXScale))
+ .attr('y', (d) => yScale(d.median || 0) - 5)
+ .text((d) => this.helperService.roundNumber(d.median || 0, 2));
+
+ // X-AXIS
+ const xAxis = d3.axisBottom(this.chartXScale);
+ this.chartXAxisDrawn = this.chart
+ .append('g')
+ .attr('class', 'x-axis')
+ .attr('transform', `translate(0, ${innerHeight})`)
+ .call(xAxis.tickSizeOuter(0));
+ this.chartXAxisDrawn
+ .selectAll('.tick')
+ .on('mouseenter', (_, tissue) => {
+ const tooltipText = this.helperService.getGCTColumnTooltipText(
+ tissue as string
+ );
+ this.showTooltip(
+ tooltipText,
+ this.getBarCenterX(tissue as string, this.chartXScale) + this.chartMargin.left,
+ height - this.chartMargin.top
+ );
+ })
+ .on('mouseleave', () => {
+ this.hideTooltip();
+ });
+
+ // Y-AXIS
+ const yAxis = d3.axisLeft(yScale);
+ this.chart.append('g').attr('class', 'y-axis').call(yAxis);
+
+ // X-AXIS LABEL
+ this.chartXAxisLabel = this.chart
+ .append('text')
+ .attr('class', 'x-axis-label')
+ .attr('x', innerWidth / 2)
+ .attr('y', innerHeight + this.chartMargin.bottom)
+ .attr('text-anchor', 'middle')
+ .text(this.xAxisLabel);
+
+ // Y-AXIS LABEL
+ this.chart
+ .append('text')
+ .attr('class', 'y-axis-label')
+ .attr('x', -innerHeight / 2)
+ .attr('y', -this.chartMargin.left)
+ .attr('dy', '1em')
+ .attr('text-anchor', 'middle')
+ .attr('transform', 'rotate(-90)')
+ .text(this.yAxisLabel);
+
+ // THRESHOLD LINE
+ if (this.shouldShowThresholdLine) {
+ this.chartThresholdLine = this.chart
+ .append('line')
+ .attr('class', 'meaningful-expression-threshold-line')
+ .attr('x1', 0)
+ .attr('x2', innerWidth)
+ .attr('y1', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD))
+ .attr('y2', yScale(this.MEANINGFUL_EXPRESSION_THRESHOLD))
+ .attr('stroke', 'red');
+ }
+
+ this.chartInitialized = true;
+ }
+ }
+
+ resizeChart = (divSize: number): void => {
+ // calculate new width
+ const width = Math.max(divSize, this.MIN_CHART_WIDTH);
+ const innerWidth = width - this.chartMargin.left - this.chartMargin.right;
+
+ // update chart size
+ this.chart.attr('width', width);
+
+ // update chartXScale
+ this.chartXScale.range([0, innerWidth]);
+
+ // update bars
+ this.chartBars
+ .transition()
+ .attr('x', (d) => this.chartXScale(d.tissue) as number)
+ .attr('width', this.chartXScale.bandwidth());
+
+ // update score labels
+ this.chartScoreLabels
+ .transition()
+ .attr('x', (d) => this.getBarCenterX(d.tissue, this.chartXScale));
+
+ // update drawn x-axis
+ const xAxis = d3.axisBottom(this.chartXScale);
+ this.chartXAxisDrawn
+ .transition()
+ .call(xAxis.tickSizeOuter(0));
+
+ // update x-axis label
+ this.chartXAxisLabel
+ .transition()
+ .attr('x', innerWidth / 2);
+
+ // update threshold line
+ if (this.shouldShowThresholdLine) {
+ this.chartThresholdLine
+ .transition()
+ .attr('x2', innerWidth);
+ }
+ };
+}
diff --git a/src/app/features/charts/components/score-barchart/score-barchart.component.html b/src/app/features/charts/components/score-barchart/score-barchart.component.html
index 39d8fadf..6d1a768e 100644
--- a/src/app/features/charts/components/score-barchart/score-barchart.component.html
+++ b/src/app/features/charts/components/score-barchart/score-barchart.component.html
@@ -1,5 +1,5 @@
-
-
+
+
No data is currently available.
diff --git a/src/app/features/charts/components/score-barchart/score-barchart.component.scss b/src/app/features/charts/components/score-barchart/score-barchart.component.scss
index 996bd408..52d195fd 100644
--- a/src/app/features/charts/components/score-barchart/score-barchart.component.scss
+++ b/src/app/features/charts/components/score-barchart/score-barchart.component.scss
@@ -1,49 +1,4 @@
-$tooltip-color: #63676C;
-
#score-barchart {
- position: relative;
- height: 350px;
-
- svg {
- width: 100%;
- }
-
- #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;
- }
- }
- }
-
.negative-bars {
&:hover {
fill: transparent;
@@ -66,27 +21,4 @@ $tooltip-color: #63676C;
user-select: none;
}
}
-
- .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%;
- }
}
\ No newline at end of file
diff --git a/src/app/features/genes/components/gene-evidence-rna/gene-evidence-rna.component.html b/src/app/features/genes/components/gene-evidence-rna/gene-evidence-rna.component.html
index 9d8063ca..b0785dea 100644
--- a/src/app/features/genes/components/gene-evidence-rna/gene-evidence-rna.component.html
+++ b/src/app/features/genes/components/gene-evidence-rna/gene-evidence-rna.component.html
@@ -40,10 +40,10 @@
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index f668e58c..711d0ab4 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -80,6 +80,7 @@ $extra-colors: (
border: map.get($main-colors, 'gray-300'),
separator: map.get($main-colors, 'gray-300'),
shadow: #c7c5c5,
+ tooltip: #63676C,
);
:root {
diff --git a/src/styles/components/_chart_d3.scss b/src/styles/components/_chart_d3.scss
new file mode 100644
index 00000000..b26c952f
--- /dev/null
+++ b/src/styles/components/_chart_d3.scss
@@ -0,0 +1,94 @@
+.chart-d3 {
+ position: relative;
+ height: 350px;
+
+ svg {
+ width: 100%;
+ }
+
+ .bar-labels {
+ cursor: default;
+
+ &:hover {
+ cursor: default;
+ }
+ }
+
+ // -------------------------------------------------------------------------- //
+ // EMPTY CHART
+ // -------------------------------------------------------------------------- //
+ .chart-no-data {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+
+ .chart-no-data-text {
+ font-size: var(--font-size-lg);
+ font-style: italic;
+ color: var(--color-gray-600);
+ }
+ }
+
+ // -------------------------------------------------------------------------- //
+ // AXES
+ // -------------------------------------------------------------------------- //
+ .y-axis-label,
+ .x-axis-label {
+ text-transform: uppercase;
+ cursor: default;
+ }
+
+ .x-axis .tick, .y-axis .tick {
+ cursor: default;
+ }
+
+ // -------------------------------------------------------------------------- //
+ // TOOLTIP
+ // -------------------------------------------------------------------------- //
+ #tooltip {
+ position: absolute;
+ text-align: center;
+ padding: 5px;
+ font-size: 14px;
+ background-color: var(--color-tooltip);
+ 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-above {
+ transform: translate(calc(-50%), calc(50% - 12px));
+
+ &::before {
+ top: -9px;
+ border-top: 0;
+ border-bottom-color: var(--color-tooltip);
+ }
+ }
+
+ &.arrow-below {
+ transform: translate(calc(-50%), calc(-100% - 20px));
+
+ &::before {
+ bottom: -9px;
+ border-bottom: 0;
+ border-top-color: var(--color-tooltip);
+ }
+ }
+ }
+}
diff --git a/src/styles/styles.scss b/src/styles/styles.scss
index 8479b2e6..a6aa2e51 100644
--- a/src/styles/styles.scss
+++ b/src/styles/styles.scss
@@ -22,6 +22,7 @@
@import 'components/tooltip';
@import 'components/table';
@import 'components/chart';
+@import 'components/chart_d3';
@import 'components/icon';
@import 'components/shame';