Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entries plot #825

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
## Purpose

Describe the problem or feature in addition to a link to the issue(s), including context where needed.

## Approach

How does this change address the problem?

## Testing

What test(s) did you write to validate and verify your changes?

## Checklist

- [ ] My PR is scoped properly, and “does one thing only”
- [ ] I have reviewed my own code
- [ ] I have checked that linting checks pass and type safety is respected
- [ ] I have checked that tests pass and coverage has at least improved, and if not explained the reasons why
- [ ] If needed, the changes have been previewed (eg on wwwdev) by all interested parties.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
"core-js": "3.38.1",
"d3": "5.16.0",
"deep-freeze": "0.0.1",
"franklin-sites": "0.0.247",
"franklin-sites": "file:.yalc/franklin-sites",
aurel-l marked this conversation as resolved.
Show resolved Hide resolved
"front-matter": "4.0.2",
"history": "4.10.1",
"idb": "8.0.0",
Expand Down
18 changes: 18 additions & 0 deletions src/shared/utils/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
deepFindAllByKey,
addBlastLinksToFreeText,
keysToLowerCase,
defaultdict,
} from '../utils';

describe('Model Utils', () => {
Expand Down Expand Up @@ -119,3 +120,20 @@ describe('keysToLowerCase', () => {
expect(keysToLowerCase(undefined)).toEqual({});
});
});

describe('defaultdict', () => {
it('should return default value if not present', () => {
const dd = defaultdict(0);
expect(dd.foo).toEqual(0);
});
it('should increment value correctly', () => {
const dd = defaultdict(0);
dd.foo += 1;
expect(dd.foo).toEqual(1);
});
it('should return current value when assigned', () => {
const dd = defaultdict(0);
dd.foo = 100;
expect(dd.foo).toEqual(100);
});
});
10 changes: 10 additions & 0 deletions src/shared/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,13 @@ export function keysToLowerCase<T>(o: { [k: string]: T } = {}): {
Object.entries(o).map(([k, v]) => [k.toLowerCase(), v])
);
}

export function defaultdict<T>(defaultValue: T) {
return new Proxy<Record<string | symbol, T>>(
{},
{
get: (dict, key: string | symbol) =>
key in dict ? dict[key] : defaultValue,
}
);
}
143 changes: 143 additions & 0 deletions src/uniprotkb/components/statistics/HistoricalReleasesEntries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Card, Loader } from 'franklin-sites';
import LazyComponent from '../../../shared/components/LazyComponent';

import ErrorHandler from '../../../shared/components/error-pages/ErrorHandler';
import UniProtKBStatsTabs from './UniProtKBStatsTabs';
import HistoricalReleasesEntriesLinePlot, {
Bounds,
DateCount,
} from './HistoricalReleasesEntriesLinePlot';

import useDataApi from '../../../shared/hooks/useDataApi';

import { defaultdict } from '../../../shared/utils/utils';

import apiUrls from '../../config/apiUrls/apiUrls';

import styles from './styles/statistics-page.module.scss';

type DateToCount = Record<string, number>;

type Proccessed = {
UNIPROTKB: DateToCount;
REVIEWED: DateToCount;
UNREVIEWED: DateToCount;
};
type StatisticsType = 'UNIPROTKB' | 'REVIEWED' | 'UNREVIEWED';
type StatisticsTypeToDateCount = Record<StatisticsType, DateCount[]>;
type StatisticsTypeToBounds = Record<StatisticsType, Bounds>;

const sortByDate = (dateToCount: DateToCount) =>
Array.from(Object.entries(dateToCount))
.map(([date, count]): DateCount => [new Date(date), count])
.sort(([a], [b]) => a.getTime() - b.getTime());

const processResults = (results: Results): StatisticsTypeToDateCount => {
const processed: Proccessed = {
UNIPROTKB: defaultdict(0),
REVIEWED: defaultdict(0),
UNREVIEWED: defaultdict(0),
};
for (const { entryCount, releaseDate, statisticsType } of results) {
processed[statisticsType][releaseDate] += entryCount;
processed.UNIPROTKB[releaseDate] += entryCount;
}
return {
UNIPROTKB: sortByDate(processed.UNIPROTKB),
REVIEWED: sortByDate(processed.REVIEWED),
UNREVIEWED: sortByDate(processed.UNREVIEWED),
};
};

const getBounds = (
processed: StatisticsTypeToDateCount
): StatisticsTypeToBounds => {
const f = (dateCounts: DateCount[]): Bounds => ({
date: [dateCounts[0][0], dateCounts[dateCounts.length - 1][0]],
count: [0, Math.max(...dateCounts.map(([, count]) => count))],
});
return {
UNIPROTKB: f(processed.UNIPROTKB),
REVIEWED: f(processed.REVIEWED),
UNREVIEWED: f(processed.UNREVIEWED),
};
};

type Results = {
statisticsType: 'REVIEWED' | 'UNREVIEWED';
releaseName: string;
releaseDate: string;
valueCount: number;
entryCount: number;
}[];

type HistoricalReleasesEntriesPayload = {
results: Results;
};

const HistoricalReleaseEntryCounts = () => {
const { data, error, loading, status } =
useDataApi<HistoricalReleasesEntriesPayload>(apiUrls.statistics.history);
if (loading) {
return <Loader />;
}

if (error || !data) {
return <ErrorHandler status={status} error={error} fullPage />;
}

const processed = processResults(data.results);
const bounds = getBounds(processed);

return (
<Card id="amino-acid-composition">
<h2>Total number of entries per release over time</h2>
<UniProtKBStatsTabs>
<div className={styles['side-by-side']}>
<div className={styles.viz}>
<LazyComponent
// Keep the space with an empty visualisation
fallback={<HistoricalReleasesEntriesLinePlot />}
rootMargin="0px 0px"
>
<HistoricalReleasesEntriesLinePlot
dateCounts={processed.UNIPROTKB}
bounds={bounds.UNIPROTKB}
/>
</LazyComponent>
</div>
</div>
<div className={styles['side-by-side']}>
<div className={styles.viz}>
<LazyComponent
// Keep the space with an empty visualisation
fallback={<HistoricalReleasesEntriesLinePlot />}
rootMargin="0px 0px"
>
<HistoricalReleasesEntriesLinePlot
dateCounts={processed.REVIEWED}
bounds={bounds.REVIEWED}
/>
</LazyComponent>
</div>
</div>
<div className={styles['side-by-side']}>
<div className={styles.viz}>
<LazyComponent
// Keep the space with an empty visualisation
fallback={<HistoricalReleasesEntriesLinePlot />}
rootMargin="0px 0px"
>
<HistoricalReleasesEntriesLinePlot
dateCounts={processed.UNREVIEWED}
bounds={bounds.UNREVIEWED}
/>
</LazyComponent>
</div>
</div>
</UniProtKBStatsTabs>
</Card>
);
};

export default HistoricalReleaseEntryCounts;
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { memo, useCallback, useEffect, useRef } from 'react';
import {
select,
scaleLinear,
axisBottom,
axisLeft,
line,
scaleTime,
format,
timeFormat,
dlrice marked this conversation as resolved.
Show resolved Hide resolved
timeYear,
} from 'd3';

import styles from './styles/historical-release-entries-line-plot.module.scss';

// Specify the chart’s dimensions.
const width = 400;
const height = 400;
const margin = { top: 20, right: 60, bottom: 45, left: 80 };

export type DateCount = [Date, number];
export type Bounds = {
date: [Date, Date];
count: [number, number];
};

type Props = {
dateCounts?: DateCount[];
bounds?: Bounds;
};

const HistoricalReleasesEntriesLinePlot = ({ dateCounts, bounds }: Props) => {
const svgRef = useRef<SVGSVGElement>(null);

const renderPlot = useCallback((dateCounts: DateCount[], bounds: Bounds) => {
const chart = select(svgRef.current).select('g');
dlrice marked this conversation as resolved.
Show resolved Hide resolved

const startYear = new Date(bounds.date[0].getFullYear(), 0, 1);
const endYear = new Date(bounds.date[1].getFullYear() + 1, 0, 1);

// x-axis
const xScale = scaleTime()
.domain([startYear, endYear]) // units: Date
.range([0, width]); // units: pixels
chart
.select<SVGGElement>('.x-axis')
.transition()
.duration(1_000)
.call(
axisBottom(xScale)
.ticks(timeYear)
.tickFormat((d: Date | { valueOf(): number }) =>
timeFormat('%Y')(new Date(d.valueOf()))
)
);

// y-axis
const yScale = scaleLinear()
.domain(bounds.count) // units: count
.range([height, 0]) // units: pixels
.nice();

chart
.select<SVGGElement>('.y-axis')
.transition()
.duration(1_000)
.call(axisLeft(yScale).tickFormat(format('.2s')));

chart
.select(`.${styles.line}`)
.datum(dateCounts)
.transition()
.duration(1_000)
.attr('opacity', 1)
.attr(
'd',
line<DateCount>()
.x((d) => xScale(d[0]) || 0)
.y((d) => yScale(d[1]) || 0)
);
}, []);

useEffect(() => {
if (svgRef.current && dateCounts && bounds) {
renderPlot(dateCounts, bounds);
}
}, [bounds, dateCounts, renderPlot]);

return (
<svg
ref={svgRef}
width={width + margin.left + margin.right}
height={height + margin.top + margin.bottom}
className={styles['line-plot']}
>
<g transform={`translate(${margin.left}, ${margin.top})`}>
<g className="x-axis" transform={`translate(0, ${height})`} />
<text
textAnchor="middle"
x={width / 2}
y={height + 0.8 * margin.bottom}
>
Release date
</text>
<g className="y-axis" />
<text
textAnchor="middle"
x={-height / 2}
y={-0.7 * margin.left}
transform="rotate(-90)"
>
Number of entries
</text>
<path className={styles.line} opacity={0} />
</g>
</svg>
);
};

export default memo(HistoricalReleasesEntriesLinePlot);
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ const SequenceLengthLinePlot = ({ counts }: Props) => {
.x((_, index) => xScale(index) || 0)
.y((d) => yScale(d) || 0)
);
// Keeping an empty comment here otherwise somehow prettier messes things
}, []);

useEffect(() => {
Expand Down
3 changes: 3 additions & 0 deletions src/uniprotkb/components/statistics/StatisticsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import UniqueReferencesTable from './UniqueReferencesTable';
import AminoAcidCompositionTable from './AminoAcidCompositionTable';
import { ReviewedLabel, UnreviewedLabel } from './UniProtKBLabels';
import InPageNav from '../../../shared/components/InPageNav';
import HistoricalReleaseEntryCounts from './HistoricalReleasesEntries';

import useUniProtDataVersion from '../../../shared/hooks/useUniProtDataVersion';
import useDataApi from '../../../shared/hooks/useDataApi';
Expand Down Expand Up @@ -647,12 +648,14 @@ const StatisticsPage = () => {
query, a link has been provided. In some instances, due to the nature
of the statistic, no query link is possible.
</p>

<IntroductionEntriesTable
uniprotkbData={uniprotkbData.AUDIT}
reviewedData={reviewedData.AUDIT}
unreviewedData={unreviewedData.AUDIT}
releaseDate={release.releaseDate}
/>
<HistoricalReleaseEntryCounts />
<IntroductionSequenceTable
uniprotkbData={uniprotkbData.SEQUENCE_STATS}
reviewedData={reviewedData.SEQUENCE_STATS}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.line-plot {
path.line {
fill: none;
stroke: var(--fr--color-weldon-blue);
stroke-width: 4;
}
}
2 changes: 2 additions & 0 deletions src/uniprotkb/config/apiUrls/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export const statistics = (
type
? joinUrl(apiPrefix, 'statistics', 'releases', releaseNumber, type)
: joinUrl(apiPrefix, 'statistics', 'releases', releaseNumber);

export const history = joinUrl(apiPrefix, 'statistics', 'history', 'entry');