From 9597389d1fe89127d29b3eb89666b01e40662f4b Mon Sep 17 00:00:00 2001
From: hallieswan <26949006+hallieswan@users.noreply.github.com>
Date: Fri, 10 Jan 2025 10:08:15 -0800
Subject: [PATCH] feat(agora): migrate Agora to use shared boxplot component
(AG-1460) (#2956)
---
apps/agora/app/project.json | 2 +-
libs/agora/about/src/lib/about.component.html | 8 -
.../about/src/lib/about.component.stories.ts | 24 +
libs/agora/about/src/lib/about.component.ts | 3 +-
libs/agora/about/tsconfig.lib.json | 8 +-
.../box-plot-chart-data-mock.ts | 24 +
.../box-plot-chart.component.html | 29 +-
.../box-plot-chart.component.spec.ts | 40 +
.../box-plot-chart.component.spec.ts.off | 73 --
.../box-plot-chart.component.stories.ts | 41 +
.../box-plot-chart.component.ts | 187 +---
.../charts/src/lib/box-plot-chart/box-plot.ts | 955 ------------------
libs/agora/charts/src/test-setup.ts | 20 +
.../gene-evidence-metabolomics.component.html | 2 +-
.../gene-evidence-proteomics.component.html | 6 +-
.../gene-evidence-rna.component.html | 2 +-
.../gene-evidence-rna.component.ts | 1 -
.../news/src/lib/news.component.stories.ts | 24 +
libs/agora/news/tsconfig.lib.json | 8 +-
libs/agora/storybook/.eslintrc.json | 33 +
libs/agora/storybook/.storybook/main.ts | 16 +
libs/agora/storybook/.storybook/preview.ts | 0
libs/agora/storybook/.storybook/tsconfig.json | 18 +
libs/agora/storybook/README.md | 3 +
libs/agora/storybook/project.json | 64 ++
libs/agora/storybook/src/index.ts | 0
libs/agora/storybook/tsconfig.json | 29 +
libs/agora/storybook/tsconfig.lib.json | 18 +
.../charts-angular/src/test-setup.ts | 6 +
.../src/lib/boxplot-chart/boxplot-chart.ts | 8 +-
.../charts/src/lib/models/boxplot.ts | 2 +
.../charts/src/lib/utils/chart-utils.ts | 16 +-
.../x-axis-label-tooltips.ts | 4 +-
.../typescript/charts/tsconfig.lib.json | 3 +-
.../typescript/charts/tsconfig.spec.json | 3 +-
tsconfig.base.json | 1 +
36 files changed, 456 insertions(+), 1225 deletions(-)
create mode 100644 libs/agora/about/src/lib/about.component.stories.ts
create mode 100644 libs/agora/charts/src/lib/box-plot-chart/box-plot-chart-data-mock.ts
create mode 100644 libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts
delete mode 100644 libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts.off
create mode 100644 libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.stories.ts
delete mode 100644 libs/agora/charts/src/lib/box-plot-chart/box-plot.ts
create mode 100644 libs/agora/news/src/lib/news.component.stories.ts
create mode 100644 libs/agora/storybook/.eslintrc.json
create mode 100644 libs/agora/storybook/.storybook/main.ts
create mode 100644 libs/agora/storybook/.storybook/preview.ts
create mode 100644 libs/agora/storybook/.storybook/tsconfig.json
create mode 100644 libs/agora/storybook/README.md
create mode 100644 libs/agora/storybook/project.json
create mode 100644 libs/agora/storybook/src/index.ts
create mode 100644 libs/agora/storybook/tsconfig.json
create mode 100644 libs/agora/storybook/tsconfig.lib.json
diff --git a/apps/agora/app/project.json b/apps/agora/app/project.json
index eb02af7d4a..655ec44f49 100644
--- a/apps/agora/app/project.json
+++ b/apps/agora/app/project.json
@@ -57,7 +57,7 @@
{
"type": "initial",
"maximumWarning": "1mb",
- "maximumError": "3mb"
+ "maximumError": "4mb"
},
{
"type": "anyComponentStyle",
diff --git a/libs/agora/about/src/lib/about.component.html b/libs/agora/about/src/lib/about.component.html
index e984ba0699..3ad6a7f7bd 100644
--- a/libs/agora/about/src/lib/about.component.html
+++ b/libs/agora/about/src/lib/about.component.html
@@ -13,11 +13,3 @@
About
-
-
-
diff --git a/libs/agora/about/src/lib/about.component.stories.ts b/libs/agora/about/src/lib/about.component.stories.ts
new file mode 100644
index 0000000000..f61a21c4cd
--- /dev/null
+++ b/libs/agora/about/src/lib/about.component.stories.ts
@@ -0,0 +1,24 @@
+import { CommonModule } from '@angular/common';
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
+import { WikiComponent } from '@sagebionetworks/agora/shared';
+import { applicationConfig, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
+import { AboutComponent } from './about.component';
+
+const meta: Meta = {
+ component: AboutComponent,
+ title: 'About',
+ decorators: [
+ applicationConfig({
+ providers: [provideHttpClient(withInterceptorsFromDi())],
+ }),
+ moduleMetadata({
+ imports: [CommonModule, WikiComponent],
+ }),
+ ],
+};
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/libs/agora/about/src/lib/about.component.ts b/libs/agora/about/src/lib/about.component.ts
index 02aac33380..28b0c59e9c 100644
--- a/libs/agora/about/src/lib/about.component.ts
+++ b/libs/agora/about/src/lib/about.component.ts
@@ -1,11 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
-import { ScoreBarChartComponent } from '@sagebionetworks/agora/charts';
import { WikiComponent } from '@sagebionetworks/agora/shared';
@Component({
selector: 'agora-about',
- imports: [CommonModule, WikiComponent, ScoreBarChartComponent],
+ imports: [CommonModule, WikiComponent],
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss'],
})
diff --git a/libs/agora/about/tsconfig.lib.json b/libs/agora/about/tsconfig.lib.json
index b228a1a081..2403478901 100644
--- a/libs/agora/about/tsconfig.lib.json
+++ b/libs/agora/about/tsconfig.lib.json
@@ -7,6 +7,12 @@
"inlineSources": true,
"types": []
},
- "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
+ "exclude": [
+ "src/test-setup.ts",
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "jest.config.ts",
+ "**/*.stories.ts"
+ ],
"include": ["**/*.ts"]
}
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart-data-mock.ts b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart-data-mock.ts
new file mode 100644
index 0000000000..428390de56
--- /dev/null
+++ b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart-data-mock.ts
@@ -0,0 +1,24 @@
+import { boxPlotChartItem } from '@sagebionetworks/agora/models';
+
+export const boxPlotChartItemsMock: boxPlotChartItem[] = [
+ {
+ key: 'CBE',
+ value: [-0.5, -0.08, 0.5],
+ circle: {
+ value: -0.0752,
+ tooltip:
+ 'MSN is not significantly differentially expressed in CBE with a log fold change value of -0.0752 and an adjusted p-value of 0.531.',
+ },
+ quartiles: [-0.1, -0.08, 0.1],
+ },
+ {
+ key: 'ACC',
+ value: [-0.256, -0.0036, 0.2503],
+ circle: {
+ value: -0.0144,
+ tooltip:
+ 'MSN is not significantly differentially expressed in ACC with a log fold change value of -0.0145 and an adjusted p-value of 0.893.',
+ },
+ quartiles: [-0.0661, -0.0036, 0.0604],
+ },
+];
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.html b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.html
index 42a9d8c172..a9758ba87a 100644
--- a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.html
+++ b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.html
@@ -1,31 +1,12 @@
-
-
-
+>
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts
new file mode 100644
index 0000000000..388be61337
--- /dev/null
+++ b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts
@@ -0,0 +1,40 @@
+import { HelperService } from '@sagebionetworks/agora/services';
+import { BoxplotDirective } from '@sagebionetworks/shared/charts-angular';
+import { render, screen } from '@testing-library/angular';
+import { boxPlotChartItemsMock } from './box-plot-chart-data-mock';
+import { BoxPlotComponent } from './box-plot-chart.component';
+
+class MockHelperService {
+ getGCTColumnTooltipText(column: string): string {
+ return column + '-mock-text';
+ }
+}
+
+describe('Component: Chart - Box Plot', () => {
+ it('should render chart with data', async () => {
+ await render(BoxPlotComponent, {
+ componentProperties: {
+ data: boxPlotChartItemsMock,
+ },
+ imports: [BoxplotDirective],
+ providers: [{ provide: HelperService, useClass: MockHelperService }],
+ });
+
+ // keys are sorted alphabetically
+ const tooltipTextRegExp = new RegExp(
+ `${boxPlotChartItemsMock[1].circle.tooltip}.*${boxPlotChartItemsMock[0].circle.tooltip}`,
+ );
+ expect(screen.getByLabelText(tooltipTextRegExp)).toBeVisible();
+ });
+
+ it('should render no data placeholder when no data is passed', async () => {
+ await render(BoxPlotComponent, {
+ componentProperties: {
+ data: undefined,
+ },
+ imports: [BoxplotDirective],
+ providers: [{ provide: HelperService, useClass: MockHelperService }],
+ });
+ expect(screen.getByLabelText('No data is currently available.')).toBeVisible();
+ });
+});
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts.off b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts.off
deleted file mode 100644
index 7099ed2cab..0000000000
--- a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.spec.ts.off
+++ /dev/null
@@ -1,73 +0,0 @@
-// -------------------------------------------------------------------------- //
-// External
-// -------------------------------------------------------------------------- //
-import { TestBed, ComponentFixture } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-// -------------------------------------------------------------------------- //
-// Internal
-// -------------------------------------------------------------------------- //
-import { BoxPlotComponent } from './box-plot-chart.component';
-import { HelperService } from '@sagebionetworks/agora/services';
-import { boxPlotChartItemMock } from '@sagebionetworks/agora/testing';
-
-// -------------------------------------------------------------------------- //
-// Tests
-// -------------------------------------------------------------------------- //
-describe('Component: Chart - Box Plot', () => {
- let fixture: ComponentFixture;
- let component: BoxPlotComponent;
- let element: HTMLElement;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [BoxPlotComponent],
- imports: [RouterTestingModule],
- providers: [HelperService],
- }).compileComponents();
- });
-
- beforeEach(async () => {
- fixture = TestBed.createComponent(BoxPlotComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- element = fixture.nativeElement;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should display message if not data', () => {
- expect(component.data?.length).toEqual(0);
- expect(element.querySelector('.chart-no-data')).toBeTruthy();
- });
-
- it('should render the chart', () => {
- const idSpy = spyOn(component, 'initData').and.callThrough();
- const icSpy = spyOn(component, 'initChart').and.callThrough();
-
- component.data = [boxPlotChartItemMock];
- fixture.detectChanges();
-
- expect(idSpy).toHaveBeenCalled();
- expect(icSpy).toHaveBeenCalled();
- expect(element.querySelector('svg')).toBeTruthy();
- });
-
- it('should have circle', () => {
- component.data = [boxPlotChartItemMock];
- component.renderCircles();
- fixture.detectChanges();
- expect(element.querySelectorAll('svg circle')?.length).not.toEqual(0);
- });
-
- it('should have tooltips', () => {
- component.data = [boxPlotChartItemMock];
- component.renderCircles();
- component.addXAxisTooltips();
- fixture.detectChanges();
- expect(document.querySelector('.box-plot-chart-x-axis-tooltip')).toBeTruthy();
- expect(document.querySelector('.box-plot-chart-value-tooltip')).toBeTruthy();
- });
-});
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.stories.ts b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.stories.ts
new file mode 100644
index 0000000000..3d5c1d51ab
--- /dev/null
+++ b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.stories.ts
@@ -0,0 +1,41 @@
+import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
+import { BoxplotDirective } from '@sagebionetworks/shared/charts-angular';
+import { applicationConfig, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
+import { boxPlotChartItemsMock } from './box-plot-chart-data-mock';
+import { BoxPlotComponent } from './box-plot-chart.component';
+
+const meta: Meta = {
+ component: BoxPlotComponent,
+ title: 'Charts/BoxPlot',
+ decorators: [
+ applicationConfig({
+ providers: [provideHttpClient(withInterceptorsFromDi())],
+ }),
+ moduleMetadata({
+ imports: [BoxplotDirective],
+ }),
+ ],
+};
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ heading: 'AD Diagnosis (males and females)',
+ data: boxPlotChartItemsMock,
+ xAxisLabel: 'Brain Region',
+ yAxisLabel: 'LOG 2 FOLD CHANGE',
+ yAxisMin: -1,
+ yAxisMax: 1,
+ },
+};
+
+export const NoData: Story = {
+ args: {
+ heading: 'AD Diagnosis (males and females)',
+ xAxisLabel: 'Brain Region',
+ yAxisLabel: 'LOG 2 FOLD CHANGE',
+ yAxisMin: -1,
+ yAxisMax: 1,
+ },
+};
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.ts b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.ts
index 5c08d188ac..04270657c3 100644
--- a/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.ts
+++ b/libs/agora/charts/src/lib/box-plot-chart/box-plot-chart.component.ts
@@ -1,18 +1,14 @@
-/* eslint-disable @typescript-eslint/no-empty-function */
-/* eslint-disable @typescript-eslint/no-this-alias */
// -------------------------------------------------------------------------- //
// External
// -------------------------------------------------------------------------- //
import { Component, inject, Input } from '@angular/core';
-import * as d3 from 'd3';
+import { HelperService } from '@sagebionetworks/agora/services';
// -------------------------------------------------------------------------- //
// Internal
// -------------------------------------------------------------------------- //
-// import { agoraBoxPlot } from './box-plot';
import { boxPlotChartItem } from '@sagebionetworks/agora/models';
-import { HelperService } from '@sagebionetworks/agora/services';
-import { BaseChartComponent } from '../base-chart/base-chart.component';
+import { CategoryBoxplotSummary, CategoryPoint } from '@sagebionetworks/shared/charts';
import { BoxplotDirective } from '@sagebionetworks/shared/charts-angular';
// -------------------------------------------------------------------------- //
@@ -25,10 +21,16 @@ import { BoxplotDirective } from '@sagebionetworks/shared/charts-angular';
templateUrl: './box-plot-chart.component.html',
styleUrls: ['./box-plot-chart.component.scss'],
})
-export class BoxPlotComponent extends BaseChartComponent {
+export class BoxPlotComponent {
helperService = inject(HelperService);
_data: boxPlotChartItem[] = [];
+ points: CategoryPoint[] = [];
+ summaries: CategoryBoxplotSummary[] = [];
+ xAxisCategoryToTooltipText: Record | undefined = {};
+ isInitialized = false;
+ pointTooltipFormatter: ((pt: CategoryPoint) => string) | undefined;
+
get data(): boxPlotChartItem[] {
return this._data;
}
@@ -37,161 +39,60 @@ export class BoxPlotComponent extends BaseChartComponent {
this.init();
}
+ @Input() heading = '';
@Input() xAxisLabel = '';
@Input() yAxisLabel = 'LOG 2 FOLD CHANGE';
@Input() yAxisMin: number | undefined;
@Input() yAxisMax: number | undefined;
- @Input() yAxisPadding = 0.2;
- @Input() rcRadius = 9;
- @Input() rcColor = this.helperService.getColor('secondary');
- override name = 'box-plot-chart';
- dimension: any;
- group: any;
- min = 0;
- max = 0;
-
- override init() {
- if (!this._data?.length || !this.chartContainer?.nativeElement) {
+ init() {
+ if (!this._data?.length) {
return;
}
this.initData();
-
- if (!this.chart) {
- this.initChart();
- } else {
- this.hideCircles();
- this.chart.redraw();
- }
-
this.isInitialized = true;
}
initData() {
- const self = this;
-
- this.group = {
- all: () => {
- return self._data;
- },
- order: () => {},
- top: () => {},
- };
-
- this.dimension = {
- filter: () => {},
- filterAll: () => {},
- };
- }
-
- initChart() {
- const self = this;
-
- // this.chart = agoraBoxPlot(this.chartContainer.nativeElement, null, {
- // yAxisMin: this.yAxisMin ? this.yAxisMin - this.yAxisPadding : undefined,
- // yAxisMax: this.yAxisMax ? this.yAxisMax + this.yAxisPadding : undefined,
- // });
-
- this.chart.group(this.group).dimension(this.dimension);
-
- this.chart.elasticX(true).xAxis().tickSizeOuter([0]);
-
- this.chart
- .elasticY(true)
- .yAxisLabel(this.yAxisLabel, 20)
- .yRangePadding(this.rcRadius * 1.5)
- .yAxis()
- .ticks(8);
- //.tickSizeOuter([0]);
-
- this.chart
- .renderTitle(false)
- .showOutliers(0)
- .dataWidthPortion(0.1)
- .dataOpacity(0)
- .colors('transparent')
- .tickFormat(() => '');
-
- this.chart.margins({
- left: 90,
- right: 0,
- bottom: 50,
- top: 10,
- });
-
- this.chart.on('renderlet', function () {
- self.renderCircles();
- self.addXAxisTooltips();
- });
-
- this.chart.filter = () => '';
- this.chart.render();
- }
-
- renderCircles() {
- const self = this;
- const tooltip = this.getTooltip('internal', 'chart-value-tooltip box-plot-chart-value-tooltip');
-
- const height = this.chartContainer.nativeElement.offsetHeight;
- const lineCenter = this.chart.selectAll('line.center');
- const yDomainLength = Math.abs(this.chart.yAxisMax() - this.chart.yAxisMin());
- const mult = (height - 60) / yDomainLength;
-
- this.chart.selectAll('circle').remove();
-
- this.chart.selectAll('g.box').each(function (this: HTMLElement, el: any, i: number) {
- if (!self.data[i]['circle']) {
- return;
+ this._data.forEach((item) => {
+ const xAxisCategory = item.key;
+
+ const point: CategoryPoint = {
+ xAxisCategory: xAxisCategory,
+ value: item.circle.value,
+ text: item.circle.tooltip,
+ };
+ this.points.push(point);
+
+ const summary: CategoryBoxplotSummary = {
+ xAxisCategory: xAxisCategory,
+ min: item.value[0],
+ firstQuartile: item.quartiles[0],
+ median: item.value[1],
+ thirdQuartile: item.quartiles[2],
+ max: item.value[2],
+ };
+ this.summaries.push(summary);
+
+ if (this.xAxisCategoryToTooltipText) {
+ const tooltipText = this.getXAxisTooltipText(xAxisCategory);
+ if (tooltipText !== '') {
+ this.xAxisCategoryToTooltipText[xAxisCategory] = tooltipText;
+ }
}
- const data = self.data[i]['circle'];
- const cy = Math.abs(self.chart.y().domain()[1] - data['value']) * mult;
- const circle = d3.select(this).insert('circle', ':last-child');
-
- circle
- .attr('fill', self.rcColor)
- .attr('r', self.rcRadius)
- .attr('cx', lineCenter.attr('x1'))
- .attr('cy', isNaN(cy) ? 0.0 : cy)
- .style('stroke-width', 0)
- .style('opacity', 0)
- .style('transition', 'all .3s');
-
- circle
- .on('mouseover', function () {
- if (!data['tooltip']) {
- return;
- }
-
- const offset = self.helperService.getOffset(this);
-
- tooltip
- .html(data['tooltip'])
- .style('left', (offset?.left || 0) + 'px')
- .style('top', (offset?.top || 0) + 'px');
-
- self.showTooltip('internal');
- })
- .on('mouseout', function () {
- self.hideTooltip('internal');
- });
+ this.pointTooltipFormatter = (pt: CategoryPoint) => {
+ return pt.text ?? pt.value.toString();
+ };
});
- setTimeout(() => {
- self.showCircles();
- }, 1);
- }
-
- hideCircles() {
- this.chart.selectAll('circle').style('opacity', 0);
- }
-
- showCircles() {
- this.chart.selectAll('circle').style('opacity', 1);
+ this.points.sort((a, b) => {
+ return a.xAxisCategory.localeCompare(b.xAxisCategory);
+ });
}
- override getXAxisTooltipText(text: string) {
+ getXAxisTooltipText(text: string) {
return this.helperService.getGCTColumnTooltipText(text);
}
}
diff --git a/libs/agora/charts/src/lib/box-plot-chart/box-plot.ts b/libs/agora/charts/src/lib/box-plot-chart/box-plot.ts
deleted file mode 100644
index 06d25cf77d..0000000000
--- a/libs/agora/charts/src/lib/box-plot-chart/box-plot.ts
+++ /dev/null
@@ -1,955 +0,0 @@
-/* eslint-disable @typescript-eslint/no-this-alias */
-/* eslint-disable @typescript-eslint/ban-ts-comment */
-// @ts-nocheck
-
-import { scaleBand, scaleLinear } from 'd3';
-import { select } from 'd3';
-import { min, max, ascending, quantile, range } from 'd3';
-import { timerFlush } from 'd3';
-
-import { CoordinateGridMixin } from 'dc';
-import { transition } from 'dc';
-import { units } from 'dc';
-import { utils } from 'dc';
-import { d3compat } from 'dc/src/core/config';
-
-export const d3Box = function () {
- let width = 1;
- let height = 1;
- let duration = 0;
- const delay = 0;
- let domain = null;
- let value = Number;
- let whiskers = boxWhiskers;
- let quartiles = boxQuartiles;
- let tickFormat = null;
-
- // Enhanced attributes
- let renderDataPoints = false;
- const dataRadius = 3;
- let dataOpacity = 0.3;
- let dataWidthPortion = 0.8;
- let renderTitle = false;
- let showOutliers = true;
- let boldOutlier = false;
-
- // For each small multiple…
- function box(g) {
- g.each(function (_data, index) {
- const data = _data.map(value).sort(ascending);
- const _g = select(this);
- const n = data.length;
- let min;
- let max;
-
- // Leave if there are no items.
- if (data.length === 0) {
- return;
- }
-
- // Compute quartiles. Must return exactly 3 elements.
- // const quartileData = (data.quartiles = quartiles(data));
- // ** Agora custom code
- data.quartiles = quartiles(data);
- const quartileData = _data.quartiles ? _data.quartiles : data.quartiles;
- // Agora custom code **
-
- // Compute whiskers. Must return exactly 2 elements, or null.
- const whiskerIndices = whiskers && whiskers.call(this, data, index),
- whiskerData = whiskerIndices && whiskerIndices.map((_i) => data[_i]);
-
- // Compute outliers. If no whiskers are specified, all data are 'outliers'.
- // We compute the outliers as indices, so that we can join across transitions!
- const outlierIndices = whiskerIndices
- ? range(0, whiskerIndices[0]).concat(range(whiskerIndices[1] + 1, n))
- : range(n);
-
- // Determine the maximum value based on if outliers are shown
- if (showOutliers) {
- min = data[0];
- max = data[n - 1];
- } else {
- min = data[whiskerIndices[0]];
- max = data[whiskerIndices[1]];
- }
- const pointIndices = range(whiskerIndices[0], whiskerIndices[1] + 1);
-
- // Compute the new x-scale.
- const x1 = scaleLinear()
- .domain((domain && domain.call(this, data, index)) || [min, max])
- .range([height, 0]);
-
- // Retrieve the old x-scale, if this is an update.
- const x0 = this.__chart__ || scaleLinear().domain([0, Infinity]).range(x1.range());
-
- // Stash the new scale.
- this.__chart__ = x1;
-
- // Note: the box, median, and box tick elements are fixed in number,
- // so we only have to handle enter and update. In contrast, the outliers
- // and other elements are variable, so we need to exit them! Variable
- // elements also fade in and out.
-
- // Update center line: the vertical line spanning the whiskers.
- const center = _g.selectAll('line.center').data(whiskerData ? [whiskerData] : []);
-
- center
- .enter()
- .insert('line', 'rect')
- .attr('class', 'center')
- .attr('x1', width / 2)
- .attr('y1', (d) => x0(d[0]))
- .attr('x2', width / 2)
- .attr('y2', (d) => x0(d[1]))
- .style('opacity', 1e-6)
- .transition()
- .duration(duration)
- .delay(delay)
- .style('opacity', 1)
- .attr('y1', (d) => x1(d[0]))
- .attr('y2', (d) => x1(d[1]));
-
- center
- .transition()
- .duration(duration)
- .delay(delay)
- .style('opacity', 1)
- .attr('x1', width / 2)
- .attr('x2', width / 2)
- .attr('y1', (d) => x1(d[0]))
- .attr('y2', (d) => x1(d[1]));
-
- center
- .exit()
- .transition()
- .duration(duration)
- .delay(delay)
- .style('opacity', 1e-6)
- .attr('y1', (d) => x1(d[0]))
- .attr('y2', (d) => x1(d[1]))
- .remove();
-
- // Update innerquartile box.
- const _box = _g.selectAll('rect.box').data([quartileData]);
-
- _box
- .enter()
- .append('rect')
- .attr('class', 'box')
- .attr('x', 0)
- .attr('y', (d) => x0(d[2]))
- .attr('width', width)
- .attr('height', (d) => x0(d[0]) - x0(d[2]))
- // ** Agora custom code
- .attr('rx', 8)
- .style('fill-opacity', renderDataPoints ? 0.1 : 1)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y', (d) => x1(d[2]))
- .attr('height', (d) => x1(d[0]) - x1(d[2]));
-
- _box
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('width', width)
- .attr('y', (d) => x1(d[2]))
- .attr('height', (d) => x1(d[0]) - x1(d[2]));
-
- // Update median line.
- const medianLine = _g.selectAll('line.median').data([quartileData[1]]);
-
- medianLine
- .enter()
- .append('line')
- .attr('class', 'median')
- .attr('x1', 0)
- .attr('y1', x0)
- .attr('x2', width)
- .attr('y2', x0)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y1', x1)
- .attr('y2', x1);
-
- medianLine
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('x1', 0)
- .attr('x2', width)
- .attr('y1', x1)
- .attr('y2', x1);
-
- // Update whiskers.
- const whisker = _g.selectAll('line.whisker').data(whiskerData || []);
-
- whisker
- .enter()
- .insert('line', 'circle, text')
- .attr('class', 'whisker')
- .attr('x1', 0)
- .attr('y1', x0)
- .attr('x2', width)
- .attr('y2', x0)
- .style('opacity', 1e-6)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y1', x1)
- .attr('y2', x1)
- .style('opacity', 1);
-
- whisker
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('x1', 0)
- .attr('x2', width)
- .attr('y1', x1)
- .attr('y2', x1)
- .style('opacity', 1);
-
- whisker
- .exit()
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y1', x1)
- .attr('y2', x1)
- .style('opacity', 1e-6)
- .remove();
-
- // Update outliers.
- if (showOutliers) {
- const outlierClass = boldOutlier ? 'outlierBold' : 'outlier';
- const outlierSize = boldOutlier ? 3 : 5;
-
- let outlierX;
- if (boldOutlier) {
- outlierX = () => {
- return Math.floor(
- Math.random() * (width * dataWidthPortion) +
- 1 +
- (width - width * dataWidthPortion) / 2,
- );
- };
- } else {
- outlierX = () => {
- return width / 2;
- };
- }
-
- const outlier = _g.selectAll(`circle.${outlierClass}`).data(outlierIndices, Number);
-
- outlier
- .enter()
- .insert('circle', 'text')
- .attr('class', outlierClass)
- .attr('r', outlierSize)
- .attr('cx', outlierX)
- .attr('cy', (i) => x0(data[i]))
- .style('opacity', 1e-6)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cy', (i) => x1(data[i]))
- .style('opacity', 0.6);
-
- if (renderTitle) {
- outlier.selectAll('title').remove();
- outlier.append('title').text((i) => data[i]);
- }
-
- outlier
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cx', outlierX)
- .attr('cy', (i) => x1(data[i]))
- .style('opacity', 0.6);
-
- outlier
- .exit()
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cy', 0) //function (i) { return x1(d[i]); })
- .style('opacity', 1e-6)
- .remove();
- }
-
- // Update Values
- if (renderDataPoints) {
- const point = _g.selectAll('circle.data').data(pointIndices);
-
- point
- .enter()
- .insert('circle', 'text')
- .attr('class', 'data')
- .attr('r', dataRadius)
- .attr('cx', () =>
- Math.floor(
- Math.random() * (width * dataWidthPortion) +
- 1 +
- (width - width * dataWidthPortion) / 2,
- ),
- )
- .attr('cy', (i) => x0(data[i]))
- .style('opacity', 1e-6)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cy', (i) => x1(data[i]))
- .style('opacity', dataOpacity);
-
- if (renderTitle) {
- point.selectAll('title').remove();
- point.append('title').text((i) => data[i]);
- }
-
- point
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cx', () =>
- Math.floor(
- Math.random() * (width * dataWidthPortion) +
- 1 +
- (width - width * dataWidthPortion) / 2,
- ),
- )
- .attr('cy', (i) => x1(data[i]))
- .style('opacity', dataOpacity);
-
- point
- .exit()
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('cy', 0)
- .style('opacity', 1e-6)
- .remove();
- }
-
- // Compute the tick format.
- const format = tickFormat || x1.tickFormat(8);
-
- // Update box ticks.
- const boxTick = _g.selectAll('text.box').data(quartileData);
-
- boxTick
- .enter()
- .append('text')
- .attr('class', 'box')
- .attr('dy', '.3em')
- .attr('dx', (d, i) => (i & 1 ? 6 : -6))
- .attr('x', (d, i) => (i & 1 ? width : 0))
- .attr('y', x0)
- .attr('text-anchor', (d, i) => (i & 1 ? 'start' : 'end'))
- .text(format)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y', x1);
-
- boxTick
- .transition()
- .duration(duration)
- .delay(delay)
- .text(format)
- .attr('x', (d, i) => (i & 1 ? width : 0))
- .attr('y', x1);
-
- // Update whisker ticks. These are handled separately from the box
- // ticks because they may or may not exist, and we want don't want
- // to join box ticks pre-transition with whisker ticks post-.
- const whiskerTick = _g.selectAll('text.whisker').data(whiskerData || []);
-
- whiskerTick
- .enter()
- .append('text')
- .attr('class', 'whisker')
- .attr('dy', '.3em')
- .attr('dx', 6)
- .attr('x', width)
- .attr('y', x0)
- .text(format)
- .style('opacity', 1e-6)
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y', x1)
- .style('opacity', 1);
-
- whiskerTick
- .transition()
- .duration(duration)
- .delay(delay)
- .text(format)
- .attr('x', width)
- .attr('y', x1)
- .style('opacity', 1);
-
- whiskerTick
- .exit()
- .transition()
- .duration(duration)
- .delay(delay)
- .attr('y', x1)
- .style('opacity', 1e-6)
- .remove();
-
- // Remove temporary quartiles element from within data array.
- delete data.quartiles;
- });
- timerFlush();
- }
-
- box.width = function (x) {
- if (!arguments.length) {
- return width;
- }
- width = x;
- return box;
- };
-
- box.height = function (x) {
- if (!arguments.length) {
- return height;
- }
- height = x;
- return box;
- };
-
- box.tickFormat = function (x) {
- if (!arguments.length) {
- return tickFormat;
- }
- tickFormat = x;
- return box;
- };
-
- box.showOutliers = function (x) {
- if (!arguments.length) {
- return showOutliers;
- }
- showOutliers = x;
- return box;
- };
-
- box.boldOutlier = function (x) {
- if (!arguments.length) {
- return boldOutlier;
- }
- boldOutlier = x;
- return box;
- };
-
- box.renderDataPoints = function (x) {
- if (!arguments.length) {
- return renderDataPoints;
- }
- renderDataPoints = x;
- return box;
- };
-
- box.renderTitle = function (x) {
- if (!arguments.length) {
- return renderTitle;
- }
- renderTitle = x;
- return box;
- };
-
- box.dataOpacity = function (x) {
- if (!arguments.length) {
- return dataOpacity;
- }
- dataOpacity = x;
- return box;
- };
-
- box.dataWidthPortion = function (x) {
- if (!arguments.length) {
- return dataWidthPortion;
- }
- dataWidthPortion = x;
- return box;
- };
-
- box.duration = function (x) {
- if (!arguments.length) {
- return duration;
- }
- duration = x;
- return box;
- };
-
- box.domain = function (x) {
- if (!arguments.length) {
- return domain;
- }
- domain = x === null ? x : typeof x === 'function' ? x : utils.constant(x);
- return box;
- };
-
- box.value = function (x) {
- if (!arguments.length) {
- return value;
- }
- value = x;
- return box;
- };
-
- box.whiskers = function (x) {
- if (!arguments.length) {
- return whiskers;
- }
- whiskers = x;
- return box;
- };
-
- box.quartiles = function (x) {
- if (!arguments.length) {
- return quartiles;
- }
- quartiles = x;
- return box;
- };
-
- return box;
-};
-
-function boxWhiskers(d) {
- return [0, d.length - 1];
-}
-
-function boxQuartiles(d) {
- return [quantile(d, 0.25), quantile(d, 0.5), quantile(d, 0.75)];
-}
-
-// Returns a function to compute the interquartile range.
-function defaultWhiskersIQR(k) {
- return (d) => {
- const q1 = d.quartiles[0];
- const q3 = d.quartiles[2];
- const iqr = (q3 - q1) * k;
-
- let i = -1;
- let j = d.length;
-
- do {
- ++i;
- } while (d[i] < q1 - iqr);
-
- do {
- --j;
- } while (d[j] > q3 + iqr);
-
- return [i, j];
- };
-}
-
-/**
- * A box plot is a chart that depicts numerical data via their quartile ranges.
- *
- * Examples:
- * - {@link http://dc-js.github.io/dc.js/examples/boxplot-basic.html Boxplot Basic example}
- * - {@link http://dc-js.github.io/dc.js/examples/boxplot-enhanced.html Boxplot Enhanced example}
- * - {@link http://dc-js.github.io/dc.js/examples/boxplot-render-data.html Boxplot Render Data example}
- * - {@link http://dc-js.github.io/dc.js/examples/boxplot-time.html Boxplot time example}
- * @mixes CoordinateGridMixin
- */
-export class AgoraBoxPlot extends CoordinateGridMixin {
- /**
- * Create a Box Plot.
- *
- * @example
- * // create a box plot under #chart-container1 element using the default global chart group
- * var boxPlot1 = new BoxPlot('#chart-container1');
- * // create a box plot under #chart-container2 element using chart group A
- * var boxPlot2 = new BoxPlot('#chart-container2', 'chartGroupA');
- * @param {String|node|d3.selection} parent - Any valid
- * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
- * a dom block element such as a div; or a dom element or d3 selection.
- * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
- * Interaction with a chart will only trigger events and redraws within the chart's group.
- */
- constructor(parent, chartGroup?, options?) {
- super();
-
- this._whiskerIqrFactor = 1.5;
- this._whiskersIqr = defaultWhiskersIQR;
- this._whiskers = this._whiskersIqr(this._whiskerIqrFactor);
-
- this._box = d3Box();
- this._tickFormat = null;
- this._renderDataPoints = false;
- this._dataOpacity = 0.3;
- this._dataWidthPortion = 0.8;
- this._showOutliers = true;
- this._boldOutlier = false;
-
- // Used in yAxisMin and yAxisMax to add padding in pixel coordinates
- // so the min and max data points/whiskers are within the chart
- this._yRangePadding = 8;
-
- this._yAxisMin = options?.yAxisMin || undefined;
- this._yAxisMax = options?.yAxisMax || undefined;
-
- this._boxWidth = (innerChartWidth, xUnits) => {
- if (this.isOrdinal()) {
- return this.x().bandwidth();
- } else {
- return innerChartWidth / (1 + this.boxPadding()) / xUnits;
- }
- };
-
- // default to ordinal
- this.x(scaleBand());
- this.xUnits(units.ordinal);
-
- // valueAccessor should return an array of values that can be coerced into numbers
- // or if data is overloaded for a static array of arrays, it should be `Number`.
- // Empty arrays are not included.
- this.data((group) =>
- group
- .all()
- .map((d) => {
- d.map = (accessor) => accessor.call(d, d);
- return d;
- })
- .filter((d) => {
- const values = this.valueAccessor()(d);
- return values.length !== 0;
- }),
- );
-
- this.boxPadding(0.8);
- this.outerPadding(0.5);
-
- this.anchor(parent, chartGroup);
- }
-
- /**
- * Get or set the spacing between boxes as a fraction of box size. Valid values are within 0-1.
- * See the {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3 docs}
- * for a visual description of how the padding is applied.
- * @see {@link https://github.com/d3/d3-scale/blob/master/README.md#scaleBand d3.scaleBand}
- * @param {Number} [padding=0.8]
- * @returns {Number|BoxPlot}
- */
- boxPadding(padding) {
- if (!arguments.length) {
- return this._rangeBandPadding();
- }
- return this._rangeBandPadding(padding);
- }
-
- /**
- * Get or set the outer padding on an ordinal box chart. This setting has no effect on non-ordinal charts
- * or on charts with a custom {@link BoxPlot#boxWidth .boxWidth}. Will pad the width by
- * `padding * barWidth` on each side of the chart.
- * @param {Number} [padding=0.5]
- * @returns {Number|BoxPlot}
- */
- outerPadding(padding) {
- if (!arguments.length) {
- return this._outerRangeBandPadding();
- }
- return this._outerRangeBandPadding(padding);
- }
-
- /**
- * Get or set the numerical width of the boxplot box. The width may also be a function taking as
- * parameters the chart width excluding the right and left margins, as well as the number of x
- * units.
- * @example
- * // Using numerical parameter
- * chart.boxWidth(10);
- * // Using function
- * chart.boxWidth((innerChartWidth, xUnits) { ... });
- * @param {Number|Function} [boxWidth=0.5]
- * @returns {Number|Function|BoxPlot}
- */
- boxWidth(boxWidth) {
- if (!arguments.length) {
- return this._boxWidth;
- }
- this._boxWidth = typeof boxWidth === 'function' ? boxWidth : utils.constant(boxWidth);
- return this;
- }
-
- _boxTransform(d, i) {
- const xOffset = this.x()(this.keyAccessor()(d, i));
- return `translate(${xOffset}, 0)`;
- }
-
- _preprocessData() {
- if (this.elasticX()) {
- this.x().domain([]);
- }
- }
-
- plotData() {
- this._calculatedBoxWidth = this._boxWidth(this.effectiveWidth(), this.xUnitCount());
-
- this._box
- .whiskers(this._whiskers)
- .width(this._calculatedBoxWidth)
- .height(this.effectiveHeight())
- .value(this.valueAccessor())
- .domain(this.y().domain())
- .duration(this.transitionDuration())
- .tickFormat(this._tickFormat)
- .renderDataPoints(this._renderDataPoints)
- .dataOpacity(this._dataOpacity)
- .dataWidthPortion(this._dataWidthPortion)
- .renderTitle(this.renderTitle())
- .showOutliers(this._showOutliers)
- .boldOutlier(this._boldOutlier);
-
- const boxesG = this.chartBodyG().selectAll('g.box').data(this.data(), this.keyAccessor());
-
- const boxesGEnterUpdate = this._renderBoxes(boxesG);
- this._updateBoxes(boxesGEnterUpdate);
- this._removeBoxes(boxesG);
-
- this.fadeDeselectedArea(this.filter());
- }
-
- _renderBoxes(boxesG) {
- const boxesGEnter = boxesG.enter().append('g');
-
- boxesGEnter
- .attr('class', 'box')
- .classed('dc-tabbable', this._keyboardAccessible)
- .attr('transform', (d, i) => this._boxTransform(d, i))
- .call(this._box)
- .on(
- 'click',
- d3compat.eventHandler((d) => {
- this.filter(this.keyAccessor()(d));
- this.redrawGroup();
- }),
- )
- .selectAll('circle')
- .classed('dc-tabbable', this._keyboardAccessible);
-
- if (this._keyboardAccessible) {
- this._makeKeyboardAccessible(this.onClick);
- }
-
- return boxesGEnter.merge(boxesG);
- }
-
- _updateBoxes(boxesG) {
- const chart = this;
- transition(boxesG, this.transitionDuration(), this.transitionDelay())
- .attr('transform', (d, i) => this._boxTransform(d, i))
- .call(this._box)
- .each(function (d) {
- const color = chart.getColor(d, 0);
- select(this).select('rect.box').attr('fill', color);
- select(this).selectAll('circle.data').attr('fill', color);
- });
- }
-
- _removeBoxes(boxesG) {
- boxesG.exit().remove().call(this._box);
- }
-
- _minDataValue() {
- return min(this.data(), (e) => min(this.valueAccessor()(e)));
- }
-
- _maxDataValue() {
- return max(this.data(), (e) => max(this.valueAccessor()(e)));
- }
-
- _yAxisRangeRatio() {
- return (this._maxDataValue() - this._minDataValue()) / this.effectiveHeight();
- }
-
- onClick(d) {
- this.filter(this.keyAccessor()(d));
- this.redrawGroup();
- }
-
- fadeDeselectedArea(brushSelection) {
- const chart = this;
- if (this.hasFilter()) {
- if (this.isOrdinal()) {
- this.g()
- .selectAll('g.box')
- .each(function (d) {
- if (chart.isSelectedNode(d)) {
- chart.highlightSelected(this);
- } else {
- chart.fadeDeselected(this);
- }
- });
- } else {
- if (!(this.brushOn() || this.parentBrushOn())) {
- return;
- }
- const start = brushSelection[0];
- const end = brushSelection[1];
- this.g()
- .selectAll('g.box')
- .each(function (d) {
- const key = chart.keyAccessor()(d);
- if (key < start || key >= end) {
- chart.fadeDeselected(this);
- } else {
- chart.highlightSelected(this);
- }
- });
- }
- } else {
- this.g()
- .selectAll('g.box')
- .each(function () {
- chart.resetHighlight(this);
- });
- }
- }
-
- isSelectedNode(d) {
- return this.hasFilter(this.keyAccessor()(d));
- }
-
- yAxisMin() {
- const padding = this._yRangePadding * this._yAxisRangeRatio();
- let min = this._minDataValue();
- min = this._yAxisMin && this._yAxisMin < min ? this._yAxisMin : min - 0.2;
- return utils.subtract(min - padding, this.yAxisPadding());
- }
-
- yAxisMax() {
- const padding = this._yRangePadding * this._yAxisRangeRatio();
- let max = this._maxDataValue();
- max = this._yAxisMax && this._yAxisMax > max ? this._yAxisMax : max + 0.2;
- return utils.add(max + padding, this.yAxisPadding());
- }
-
- /**
- * Get or set the numerical format of the boxplot median, whiskers and quartile labels. Defaults
- * to integer formatting.
- * @example
- * // format ticks to 2 decimal places
- * chart.tickFormat(d3.format('.2f'));
- * @param {Function} [tickFormat]
- * @returns {Number|Function|BoxPlot}
- */
- tickFormat(tickFormat) {
- if (!arguments.length) {
- return this._tickFormat;
- }
- this._tickFormat = tickFormat;
- return this;
- }
-
- /**
- * Get or set the amount of padding to add, in pixel coordinates, to the top and
- * bottom of the chart to accommodate box/whisker labels.
- * @example
- * // allow more space for a bigger whisker font
- * chart.yRangePadding(12);
- * @param {Function} [yRangePadding = 8]
- * @returns {Number|Function|BoxPlot}
- */
- yRangePadding(yRangePadding) {
- if (!arguments.length) {
- return this._yRangePadding;
- }
- this._yRangePadding = yRangePadding;
- return this;
- }
-
- /**
- * Get or set whether individual data points will be rendered.
- * @example
- * // Enable rendering of individual data points
- * chart.renderDataPoints(true);
- * @param {Boolean} [show=false]
- * @returns {Boolean|BoxPlot}
- */
- renderDataPoints(show) {
- if (!arguments.length) {
- return this._renderDataPoints;
- }
- this._renderDataPoints = show;
- return this;
- }
-
- /**
- * Get or set the opacity when rendering data.
- * @example
- * // If individual data points are rendered increase the opacity.
- * chart.dataOpacity(70%);
- * @param {Number} [opacity=0.3]
- * @returns {Number|BoxPlot}
- */
- dataOpacity(opacity) {
- if (!arguments.length) {
- return this._dataOpacity;
- }
- this._dataOpacity = opacity;
- return this;
- }
-
- /**
- * Get or set the portion of the width of the box to show data points.
- * @example
- * // If individual data points are rendered increase the data box.
- * chart.dataWidthPortion(0.9);
- * @param {Number} [percentage=0.8]
- * @returns {Number|BoxPlot}
- */
- dataWidthPortion(percentage) {
- if (!arguments.length) {
- return this._dataWidthPortion;
- }
- this._dataWidthPortion = percentage;
- return this;
- }
-
- /**
- * Get or set whether outliers will be rendered.
- * @example
- * // Disable rendering of outliers
- * chart.showOutliers(false);
- * @param {Boolean} [show=true]
- * @returns {Boolean|BoxPlot}
- */
- showOutliers(show) {
- if (!arguments.length) {
- return this._showOutliers;
- }
- this._showOutliers = show;
- return this;
- }
-
- /**
- * Get or set whether outliers will be drawn bold.
- * @example
- * // If outliers are rendered display as bold
- * chart.boldOutlier(true);
- * @param {Boolean} [show=false]
- * @returns {Boolean|BoxPlot}
- */
- boldOutlier(show) {
- if (!arguments.length) {
- return this._boldOutlier;
- }
- this._boldOutlier = show;
- return this;
- }
-}
-
-export const agoraBoxPlot = (parent, chartGroup?, options?) =>
- new AgoraBoxPlot(parent, chartGroup, options);
diff --git a/libs/agora/charts/src/test-setup.ts b/libs/agora/charts/src/test-setup.ts
index 1100b3e8a6..cf0030b47c 100644
--- a/libs/agora/charts/src/test-setup.ts
+++ b/libs/agora/charts/src/test-setup.ts
@@ -1 +1,21 @@
+import '@testing-library/jest-dom';
import 'jest-preset-angular/setup-jest';
+
+/**
+ * Mock clientHeight and clientWidth -- Apache Echarts expects a non-zero value to be returned, but
+ * jsdom will always return 0. See https://github.com/jsdom/jsdom/issues/2342 and
+ * https://github.com/jsdom/jsdom/issues/2310. */
+Object.defineProperty(window.HTMLElement.prototype, 'clientHeight', {
+ configurable: true,
+ value: jest.fn(),
+});
+Object.defineProperty(window.HTMLElement.prototype, 'clientWidth', {
+ configurable: true,
+ value: jest.fn(),
+});
+
+global.ResizeObserver = jest.fn().mockImplementation(() => ({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+}));
diff --git a/libs/agora/genes/src/lib/components/gene-evidence-metabolomics/gene-evidence-metabolomics.component.html b/libs/agora/genes/src/lib/components/gene-evidence-metabolomics/gene-evidence-metabolomics.component.html
index 427e112aca..83a860f5a4 100644
--- a/libs/agora/genes/src/lib/components/gene-evidence-metabolomics/gene-evidence-metabolomics.component.html
+++ b/libs/agora/genes/src/lib/components/gene-evidence-metabolomics/gene-evidence-metabolomics.component.html
@@ -80,7 +80,7 @@
diff --git a/libs/agora/genes/src/lib/components/gene-evidence-proteomics/gene-evidence-proteomics.component.html b/libs/agora/genes/src/lib/components/gene-evidence-proteomics/gene-evidence-proteomics.component.html
index 5bbf578bd4..2097dc3b2f 100644
--- a/libs/agora/genes/src/lib/components/gene-evidence-proteomics/gene-evidence-proteomics.component.html
+++ b/libs/agora/genes/src/lib/components/gene-evidence-proteomics/gene-evidence-proteomics.component.html
@@ -53,7 +53,7 @@
= {
+ component: NewsComponent,
+ title: 'News',
+ decorators: [
+ applicationConfig({
+ providers: [provideHttpClient(withInterceptorsFromDi())],
+ }),
+ moduleMetadata({
+ imports: [CommonModule, WikiComponent],
+ }),
+ ],
+};
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};
diff --git a/libs/agora/news/tsconfig.lib.json b/libs/agora/news/tsconfig.lib.json
index b228a1a081..2403478901 100644
--- a/libs/agora/news/tsconfig.lib.json
+++ b/libs/agora/news/tsconfig.lib.json
@@ -7,6 +7,12 @@
"inlineSources": true,
"types": []
},
- "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"],
+ "exclude": [
+ "src/test-setup.ts",
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "jest.config.ts",
+ "**/*.stories.ts"
+ ],
"include": ["**/*.ts"]
}
diff --git a/libs/agora/storybook/.eslintrc.json b/libs/agora/storybook/.eslintrc.json
new file mode 100644
index 0000000000..a4991f945f
--- /dev/null
+++ b/libs/agora/storybook/.eslintrc.json
@@ -0,0 +1,33 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*", "storybook-static"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "lib",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "lib",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/agora/storybook/.storybook/main.ts b/libs/agora/storybook/.storybook/main.ts
new file mode 100644
index 0000000000..60ef283d1b
--- /dev/null
+++ b/libs/agora/storybook/.storybook/main.ts
@@ -0,0 +1,16 @@
+import type { StorybookConfig } from '@storybook/angular';
+
+const config: StorybookConfig = {
+ stories: ['../../**/src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
+ addons: ['@storybook/addon-essentials'],
+ framework: {
+ name: '@storybook/angular',
+ options: {},
+ },
+};
+
+export default config;
+
+// To customize your webpack configuration you can use the webpackFinal field.
+// Check https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config
+// and https://nx.dev/recipes/storybook/custom-builder-configs
diff --git a/libs/agora/storybook/.storybook/preview.ts b/libs/agora/storybook/.storybook/preview.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/agora/storybook/.storybook/tsconfig.json b/libs/agora/storybook/.storybook/tsconfig.json
new file mode 100644
index 0000000000..ba371db18d
--- /dev/null
+++ b/libs/agora/storybook/.storybook/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "emitDecoratorMetadata": true
+ },
+ "exclude": ["../**/*.spec.ts"],
+ "include": [
+ "../src/**/*.stories.ts",
+ "../src/**/*.stories.js",
+ "../src/**/*.stories.jsx",
+ "../src/**/*.stories.tsx",
+ "../src/**/*.stories.mdx",
+ "*.js",
+ "*.ts",
+ "../../**/src/lib/**/*.stories.ts",
+ "../../../../apps/agora/app/src/types/*.d.ts"
+ ]
+}
diff --git a/libs/agora/storybook/README.md b/libs/agora/storybook/README.md
new file mode 100644
index 0000000000..89e0274448
--- /dev/null
+++ b/libs/agora/storybook/README.md
@@ -0,0 +1,3 @@
+# agora-storybook
+
+This library was generated with [Nx](https://nx.dev).
diff --git a/libs/agora/storybook/project.json b/libs/agora/storybook/project.json
new file mode 100644
index 0000000000..6f56bdbc94
--- /dev/null
+++ b/libs/agora/storybook/project.json
@@ -0,0 +1,64 @@
+{
+ "name": "agora-storybook",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/agora/storybook/src",
+ "prefix": "agora",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "storybook": {
+ "executor": "@storybook/angular:start-storybook",
+ "options": {
+ "port": 4400,
+ "configDir": "libs/agora/storybook/.storybook",
+ "browserTarget": "agora-storybook:build-storybook",
+ "compodoc": false
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true
+ }
+ }
+ },
+ "build-storybook": {
+ "executor": "@storybook/angular:build-storybook",
+ "outputs": ["{options.outputDir}"],
+ "options": {
+ "outputDir": "dist/storybook/agora-storybook",
+ "configDir": "libs/agora/storybook/.storybook",
+ "browserTarget": "agora-storybook:build-storybook",
+ "compodoc": false,
+ "styles": [
+ "apps/agora/app/src/styles.scss",
+ "node_modules/primeicons/primeicons.css",
+ "node_modules/primeng/resources/primeng.min.css"
+ ],
+ "stylePreprocessorOptions": {
+ "includePaths": ["libs/agora/styles/src/lib", "libs/agora/themes/src/lib"]
+ }
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true
+ }
+ }
+ },
+ "static-storybook": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build-storybook"],
+ "options": {
+ "buildTarget": "agora-storybook:build-storybook",
+ "staticFilePath": "dist/storybook/agora-storybook",
+ "spa": true
+ },
+ "configurations": {
+ "ci": {
+ "buildTarget": "agora-storybook:build-storybook:ci"
+ }
+ }
+ }
+ }
+}
diff --git a/libs/agora/storybook/src/index.ts b/libs/agora/storybook/src/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/agora/storybook/tsconfig.json b/libs/agora/storybook/tsconfig.json
new file mode 100644
index 0000000000..7f5fdd4cd3
--- /dev/null
+++ b/libs/agora/storybook/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./.storybook/tsconfig.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/agora/storybook/tsconfig.lib.json b/libs/agora/storybook/tsconfig.lib.json
new file mode 100644
index 0000000000..3bf331d9ba
--- /dev/null
+++ b/libs/agora/storybook/tsconfig.lib.json
@@ -0,0 +1,18 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "**/*.stories.ts",
+ "**/*.stories.js"
+ ],
+ "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
+}
diff --git a/libs/shared/typescript/charts-angular/src/test-setup.ts b/libs/shared/typescript/charts-angular/src/test-setup.ts
index 89605951a8..cf0030b47c 100644
--- a/libs/shared/typescript/charts-angular/src/test-setup.ts
+++ b/libs/shared/typescript/charts-angular/src/test-setup.ts
@@ -13,3 +13,9 @@ Object.defineProperty(window.HTMLElement.prototype, 'clientWidth', {
configurable: true,
value: jest.fn(),
});
+
+global.ResizeObserver = jest.fn().mockImplementation(() => ({
+ observe: jest.fn(),
+ unobserve: jest.fn(),
+ disconnect: jest.fn(),
+}));
diff --git a/libs/shared/typescript/charts/src/lib/boxplot-chart/boxplot-chart.ts b/libs/shared/typescript/charts/src/lib/boxplot-chart/boxplot-chart.ts
index 934c85aadb..4e26b79c5d 100644
--- a/libs/shared/typescript/charts/src/lib/boxplot-chart/boxplot-chart.ts
+++ b/libs/shared/typescript/charts/src/lib/boxplot-chart/boxplot-chart.ts
@@ -182,6 +182,7 @@ export class BoxplotChart {
{
text: title,
left: 'center',
+ top: 'top',
textStyle: titleTextStyle,
},
// Add x-axis title as a title rather than xAxis.name, because
@@ -260,18 +261,21 @@ export class BoxplotChart {
max: yAxisMax ? yAxisMax + yAxisPadding : undefined,
},
tooltip: {
+ confine: true,
position: 'top',
backgroundColor: '#63676C',
borderColor: 'none',
textStyle: {
color: 'white',
},
- extraCssText: 'opacity: 0.9',
+ extraCssText:
+ 'opacity: 0.9; width: auto; max-width: 300px; white-space: pre-wrap; text-align: center;',
},
series: seriesOpts,
};
- this.chart.setOption(option);
+ // notMerge must be set to true to override any existing options set on the chart
+ this.chart.setOption(option, true);
this.setXAxisLabelTooltips(xAxisCategoryToTooltipText);
}
}
diff --git a/libs/shared/typescript/charts/src/lib/models/boxplot.ts b/libs/shared/typescript/charts/src/lib/models/boxplot.ts
index bd78f3c240..1cc49f3644 100644
--- a/libs/shared/typescript/charts/src/lib/models/boxplot.ts
+++ b/libs/shared/typescript/charts/src/lib/models/boxplot.ts
@@ -7,6 +7,8 @@ export type CategoryPoint = {
gridCategory?: string;
// if defined, will use a different shape and color for each pointCategory.
pointCategory?: string;
+ // additional text about this point
+ text?: string;
};
export type CategoryAsValuePoint = CategoryPoint & {
diff --git a/libs/shared/typescript/charts/src/lib/utils/chart-utils.ts b/libs/shared/typescript/charts/src/lib/utils/chart-utils.ts
index 9d9235cce2..8140684123 100644
--- a/libs/shared/typescript/charts/src/lib/utils/chart-utils.ts
+++ b/libs/shared/typescript/charts/src/lib/utils/chart-utils.ts
@@ -13,16 +13,20 @@ export function ensureChartDomHasHeight(chartDom: HTMLDivElement | HTMLCanvasEle
export function initChart(chartDom: HTMLDivElement | HTMLCanvasElement) {
ensureChartDomHasHeight(chartDom);
const chart = echarts.init(chartDom);
- resizeChartOnWindowResize(chart);
+ resizeChartOnWindowResize(chartDom, chart);
return chart;
}
-// ensure chart resizes -- may need to update to use ResizeObserver to handle non-window resize events
-// See ngx-echarts: https://github.com/xieziyu/ngx-echarts/blob/master/projects/ngx-echarts/src/lib/ngx-echarts.directive.ts
-export function resizeChartOnWindowResize(chart: ECharts) {
- window.onresize = function () {
+// ensure chart resizes -- see https://github.com/apache/echarts/issues/17428#issuecomment-1723693844
+// if there are issues in the future, check ngx-echarts: https://github.com/xieziyu/ngx-echarts/blob/master/projects/ngx-echarts/src/lib/ngx-echarts.directive.ts
+export function resizeChartOnWindowResize(
+ chartDom: HTMLDivElement | HTMLCanvasElement,
+ chart: ECharts,
+) {
+ const resizeObserver = new ResizeObserver(() => {
chart.resize();
- };
+ });
+ resizeObserver.observe(chartDom);
}
export function setNoDataOption(chart: ECharts) {
diff --git a/libs/shared/typescript/charts/src/lib/x-axis-label-tooltips/x-axis-label-tooltips.ts b/libs/shared/typescript/charts/src/lib/x-axis-label-tooltips/x-axis-label-tooltips.ts
index 4d6931d7a5..a86b2b0753 100644
--- a/libs/shared/typescript/charts/src/lib/x-axis-label-tooltips/x-axis-label-tooltips.ts
+++ b/libs/shared/typescript/charts/src/lib/x-axis-label-tooltips/x-axis-label-tooltips.ts
@@ -38,8 +38,10 @@ export class XAxisLabelTooltips {
const currLabel = e.event?.target;
if (!currLabel) return;
- // show tooltip
const fullText = this.xAxisCategoryToTooltipText[e.value as string];
+ if (!fullText) return;
+
+ // show tooltip
axisTooltipContent.innerText = fullText;
axisTooltipStyle.left = currLabel.transform[4] - axisTooltipDOM.offsetWidth / 2 + 'px';
axisTooltipStyle.top = currLabel.transform[5] - axisTooltipDOM.offsetHeight - 15 + 'px';
diff --git a/libs/shared/typescript/charts/tsconfig.lib.json b/libs/shared/typescript/charts/tsconfig.lib.json
index 18f2d37a19..ca380ba9fa 100644
--- a/libs/shared/typescript/charts/tsconfig.lib.json
+++ b/libs/shared/typescript/charts/tsconfig.lib.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
- "types": ["node"]
+ "types": ["node"],
+ "composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
diff --git a/libs/shared/typescript/charts/tsconfig.spec.json b/libs/shared/typescript/charts/tsconfig.spec.json
index 56497b8178..b0558b50f9 100644
--- a/libs/shared/typescript/charts/tsconfig.spec.json
+++ b/libs/shared/typescript/charts/tsconfig.spec.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
- "types": ["jest", "node"]
+ "types": ["jest", "node"],
+ "composite": true
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 1201276f64..34f50f1981 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -33,6 +33,7 @@
"@sagebionetworks/agora/not-found": ["libs/agora/not-found/src/index.ts"],
"@sagebionetworks/agora/services": ["libs/agora/services/src/index.ts"],
"@sagebionetworks/agora/shared": ["libs/agora/shared/src/index.ts"],
+ "@sagebionetworks/agora/storybook": ["libs/agora/storybook/src/index.ts"],
"@sagebionetworks/agora/teams": ["libs/agora/teams/src/index.ts"],
"@sagebionetworks/agora/testing": ["libs/agora/testing/src/index.ts"],
"@sagebionetworks/agora/ui": ["libs/agora/ui/src/index.ts"],