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"],