Skip to content

External project details readsets tab - #1181

Closed
nabalma wants to merge 26 commits into
external-project-details-submissions-tabfrom
external-project-details-readsets-tab
Closed

External project details readsets tab#1181
nabalma wants to merge 26 commits into
external-project-details-submissions-tabfrom
external-project-details-readsets-tab

Conversation

@nabalma

@nabalma nabalma commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Note :

Please review PR #1180 first, if you haven't already.

This PR adds the Readsets tab. It follows the Project Submissions tab PR.

@nafiz1001

nafiz1001 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

You might have forgotten to add a dependency to package.json

ERROR in ./src/components/projectOverview/ExternalIDReadSetDashboard.tsx 17:0-44
Module not found: Error: Can't resolve '@ant-design/charts' in '/home/user/projects/freezeman/frontend/src/components/projectOverview'

Fixed with npm install @ant-design/charts

Comment on lines +5 to +21
ACTIVE_PARENT_PROJECT_READSET_FILTERS = {
"deleted": False,
"dataset__deleted": False,
"dataset__project__deleted": False,
"dataset__project__parent_project__deleted": False,
"dataset__experiment_run__deleted": False,
"dataset__experiment_run__run_type__deleted": False,
"dataset__experiment_run__run_type__platform__deleted": False,
"derived_sample__deleted": False,
"derived_sample__biosample__deleted": False,
"derived_sample__biosample__individual__deleted": False,
"derived_sample__library__deleted": False,
"derived_sample__library__library_type__deleted": False,
"derived_sample__derived_by_samples__deleted": False,
"derived_sample__derived_by_samples__sample__deleted": False,
"derived_sample__derived_by_samples__sample__container__deleted": False,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would call this DEFAULT_PARENT_PROJECT_READSET_FILTERS

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

And I also believe this should be moved inside get_parent_project_readsets_queryset

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A small note : The deleted flag is typically used to save a last version in reversion before deleting an object for good. This means there should be no "deleted" objects in the DB except in the reversion tables. These filter will likely slow the query without much to offer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks. I removed the redundant deleted=False filters ... Fixed.

Comment on lines +23 to +54
PARENT_PROJECT_READSET_ORDERING = [
"dataset__experiment_run__start_date",
"dataset__experiment_run__name",
"dataset__lane",
"id",
]

PARENT_PROJECT_READSET_VALUE_FIELDS = [
"id",
"name",
"average_quality",
"pf_reads_aligned",
"duplicate_aligned",
"readset_file_paths",
"readset_file_sizes",
"barcodes",
"validation_status",
]

PARENT_PROJECT_READSET_VALUE_ALIASES = {
"readset_sample_name": F("sample_name"),
"external_id": F("dataset__project__parent_project__external_id"),
"run_name": F("dataset__experiment_run__name"),
"lane": F("dataset__lane"),
"reference_genome_id": F("derived_sample__biosample__individual__reference_genome_id"),
"sequencing_index_name": F("derived_sample__library__index__name"),
"run_start_date": F("dataset__experiment_run__start_date"),
"alias": F("derived_sample__biosample__alias"),
"cohort": F("derived_sample__biosample__individual__cohort"),
"library_type": F("derived_sample__library__library_type__name"),
"number_of_reads": F("production_data__reads"),
}

@nafiz1001 nafiz1001 Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These should be moved inside get_parent_project_readsets_queryset unless they have a use outside the function.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed ..

@nafiz1001 nafiz1001 Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Having codes in backend/fms_core/queries/ is stepping outside the usual pattern for creating queryset in Freezeman.

@nafiz1001 nafiz1001 Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

They are normally achieved with services or viewsets or filters.py or serializers.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nafiz is right, but i honestly find your solution tempting ... The main downside i find to it is that these queries are likely not going to be reused outside a specific viewset and in the end it would just force code reader to move back and forth between code sections (one of the things i dislike the most about our frontend ;). I would tend to recommend we move these query within the viewset itself.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. I moved the parent project readsets queryset logic into ParentProjectViewSet as a private helper, since it is specific to this action.

@nafiz1001 nafiz1001 Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Brand new submodules shouldn't be introduced without master's permission @UlysseFG's agreement.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks ..
Addressed by removing the new queries package and moving the action-specific logic into ParentProjectViewSet.

Comment on lines +17 to +18
const total = items.length || 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's strange to always be needing at least total of 1.

Comment on lines +32 to +37
return {
complete: Math.round((complete / total) * 100),
incomplete: Math.round((incomplete / total) * 100),
completeCount: complete,
incompleteCount: incomplete,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, that's why you wanted total of at least 1. I feel like it would be better instead to do something like

        complete: Math.round((complete / Math.max(total, 1)) * 100),
        incomplete: Math.round((incomplete / Math.max(total, 1)) * 100),

or

        complete: total == 0 ? 0 : Math.round((complete / total) * 100),
        incomplete: total == 0 ? 0 : Math.round((incomplete / total) * 100),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I support this approach it is less risky if we ever decide to use total elsewhere. 0 is also a very valid value when not used as a denominator... All projects will have 0 readset at some point.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated to keep the actual total and protect the percentage calculations when total is 0.

Comment on lines +42 to +43
const total = readsets.length || 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar suggestion I've made for frontend/src/components/projectOverview/ExternalIDReadSetDashboard.tsx

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Also fixed ....

@nafiz1001 nafiz1001 Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is corrections that were supposed to be done on the PR that introduced those code.

Comment on lines +331 to +334

const projectOverviewReadsetColumns = getProjectOverviewReadsetColumns(libraryTypeFilters)

if (isLoading) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

getProjectOverviewReadsetColumns result should be memoized.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed ...

totalReadsets: readsets.length,
totalReads: readsets.reduce((sum, x) => sum + Number(x.number_of_reads || 0), 0),
totalRuns: new Set(readsets.map((x) => x.run_name)).size,
totalSamples: new Set(readsets.map((x) => x.readset_sample_name)).size,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sample name is not unique. You should not trust it for counting independant samples. You would be better to count biosample_id. It is likely to be mostly the same but there might be case where this will make a difference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks a lot ... This is fixed ....

totalReads: readsets.reduce((sum, x) => sum + Number(x.number_of_reads || 0), 0),
totalRuns: new Set(readsets.map((x) => x.run_name)).size,
totalSamples: new Set(readsets.map((x) => x.readset_sample_name)).size,
totalCohorts: new Set(readsets.map((x) => x.cohort)).size,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a metric that will likely not be very useful. Projects usually don't have many cohorts and the number of cohort is not an indicator for much of anything. You can leave it, but keep in mind if you ever need to free some space on the UI that this is a good candidate for replacement.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Noted ...

"files__file_path",
filter=Q(files__deleted=False),distinct=True,default=Value([]),
),
readset_file_sizes=ArrayAgg(

@UlysseFG UlysseFG Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be better to combine each file with its size in a single cell instead of having to rely on matching position in a list. I admit this would not render well in a CSV but the list are likely to break the regularity of the structure.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated to return and render readset files as a single list of objects containing both file_path and size,

filter=Q(files__deleted=False),distinct=True,default=Value([]),
),
barcodes=ArrayAgg(
"derived_sample__derived_by_samples__sample__container__barcode",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think there might still be some confusion between sample barcodes and container barcodes ... I would be very surprised we have to report container barcode anywhere in the project overview unless it is run container barcodes... At the readset level i think the barcode you might have seen in the original report on Nanuq would map to the index (index is sometime called barcode by some people making this more confusing than it needs to be ;).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

you have the library on derived_sample__library__index__name and the sequences is available also at derived_sample__library__index__sequences_3prime and derived_sample__library__index__sequences_5prime

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seeing the container barcodes there might actually be of some use if there is someone investigating the readset to check if there was some mixup ... It would be important though to mention that this is container_barcodes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated to keep the barcode information and expose it as container_barcodes .



@action(detail=True, methods=["get"])
def readsets(self, request, pk=None):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe call this overview_readsets to reduce the chance people might confuse what is sent back to normal readsets.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

action renamed
image

Comment thread backend/fms_core/serializers.py Outdated
)
readset_file_paths = serializers.ListField(required=False)
readset_file_sizes = serializers.ListField(required=False)
validation_status = serializers.IntegerField(allow_null=True,)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe call this run_validation_status instead to make it clear it is the flag that is assigned when validating the run. The library tab and sample will likely get a validation/qc_status flag also that will be differ from this one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

const grouped = new Map<string, number>()

readsets.forEach((item) => {
const libraryType = item.library_type?.trim() || 'Non renseigné'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Might be better not to mix language in there. This is something that should essentially never happen (all sequenced sample should have a library_type... This may change but this would essentially be something wrong that would need investigating on our part.). For now you can use "unknown" or "sample without library".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fixed

totalRuns: new Set(readsets.map((x) => x.run_name)).size,
totalSamples: new Set(readsets.map((x) => x.readset_sample_name)).size,
totalCohorts: new Set(readsets.map((x) => x.cohort)).size,
avgQuality: readsets.reduce((sum, x) => sum + Number(x.average_quality), 0) / total,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Averaging averages based on a variable number of bases is likely to skew the value... The correct way would be to use a weighted average using the number of bases for each readset . The number of bases is typically stored on the yield metrics field. I think some run do not provide this information you can assume an equal number of bases in this case.

@nabalma nabalma Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. I updated the average quality calculation to weight each readset by its yield (number of bases). The calculation falls back to an unweighted average when that information is incomplete.

"alias": F("derived_sample__biosample__alias"),
"cohort": F("derived_sample__biosample__individual__cohort"),
"library_type": F("derived_sample__library__library_type__name"),
"number_of_reads": F("production_data__reads"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not the best place to fetch the reads ... The production_data is extracted only once a run is validated and is not live... It is aggregated daily making this a bad source unless you want the have the reads that passed validation only... In our case, the source should be the readset metric nb_reads.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. number_of_reads is now retrieved from the readset nb_reads metric instead of production_data.

totalSamples: new Set(readsets.map((x) => x.readset_sample_name)).size,
totalCohorts: new Set(readsets.map((x) => x.cohort)).size,
avgQuality: readsets.reduce((sum, x) => sum + Number(x.average_quality), 0) / total,
avgAlignment:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For alignment and duplication you are averaging averages again ... ideally we would do a weighted average using the number of reads of the readset.

@nabalma nabalma Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. I updated both the alignment and duplication calculations to use weighted averages based on each readset’s number of reads.


<Col xs={24} sm={12} lg={6} xl={3}>
<Card size="small" styles={{ body: { padding: '8px 12px' } }}>
<Statistic title="Cohortes" value={metrics.totalCohorts} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cohorts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

size="small"
bordered
scroll={{ x: 'max-content', y: 400 }}
pagination={{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be great to have the number of hits listed in the pagination.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed...

image

onHeaderCell: compactHeaderCell,
filters: libraryTypeFilters,
filterIcon: (filtered) => <FilterOutlined style={{ color: filtered ? '#1677ff' : undefined }} />,
onFilter: (value, record) => record.library_type === value,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice touch that filter.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks!

render: (name: string) => <Text strong>{name}</Text>,
},
{
title: 'Sample',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would be great to have a filter on that column too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added

<ProjectOverviewExportButton data={exportButtonData} />
</div>
)}
{!isLoading && isActive && <ExternalIDReadSetDashboard readsets={projectOverviewReadsets} />}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would like to test having the dashboard at the very top of the tab with the export button just above the table... This would allow for a clear filters button to be inserted also next to the export button.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated. The dashboard is now displayed at the top of the tab, with the export button above the table.

image

.includes(String(value).toLowerCase()),
},
{
title: 'Run Start',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A filter here might be nice...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Date range filter added to the Run Start column.

image


const exportButtonData: ProjectOverviewExportButtonData = {
exportType: 'Project Readsets',
exportFunction: generateCsvContent,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same comment as for the project... we typically send a request to the backend to export only the currently select items... Your csv builder is much more efficient than the package we are using ... I tested the big project MOH-YR5_CRCHUM ... The initial load time is quite large but the frontend filtering and export are much faster that way ... I am afraid that this might lead to some projects being undisplayable because the request times out... We will be adding more metrics to the query and this will slow down the call and some projects can run for years with potentially thousands of readsets ... Maybe it would be better to consider the overview_readsets to have its own endpoint to be able to use a similar behaviour as elsewhere. I am however curious if we could not improve our csv generation on the backend side.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense. As discussed, could we address this in a follow-up ?

<Row gutter={[16, 16]} style={{ margin: 8, borderRadius: 4 }}>
<Col xs={24} sm={12} lg={6} xl={3}>
<Card size="small" styles={{ body: { padding: '8px 12px' } }}>
<Statistic title="Readsets" value={metrics.totalReadsets} prefix={<ExperimentOutlined style={iconStyle('#1677ff', '#e6f4ff')}/>} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Numbers displayed should not use the , to separate thousands. You can use a space instead.

@nabalma nabalma Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed ...Numeric values now use spaces instead of commas as thousands separators in both the dashboard statistics and the readsets table.
image


<Col xs={24} sm={12} lg={6} xl={3}>
<Card size="small" styles={{ body: { padding: '8px 12px' } }}>
<Statistic title="Samples" value={metrics.totalSamples} prefix={<TeamOutlined style={iconStyle('#13c2c2', '#e6fffb')} />} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This will likely go to the thousands too so you should also replace the ,.

@nabalma nabalma Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed ...
image

"external_id": F("dataset__project__parent_project__external_id"),
"run_name": F("dataset__experiment_run__name"),
"lane": F("dataset__lane"),
"reference_genome_id": F("derived_sample__biosample__individual__reference_genome_id"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not really informative without getting the assembly_name. This would need to be fetched separately from the store on the frontend or directly sent back (derived_sample__biosample__individual__reference_genome__assembly_name).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated. The backend now returns the reference genome assembly name

@nafiz1001

nafiz1001 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

When I exported on http://localhost:9000/external-projects-overview/193#readsets, the readset_files column shows up like this in the csv:

duplicate_aligned readset_files run_validation_status
0.133361 [object Object],[object Object] 1
0.414306 [object Object],[object Object] 1
0.136845 [object Object],[object Object] 1
0.40548 [object Object],[object Object] 1

Thanks. Fixed now ...
image

@nabalma

nabalma commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this PR as it has been replaced by #1185, which consolidates the entire stack into a single PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants