From 447137a4d42af5f0fd5c52532c5c5b5c4b34f4ab Mon Sep 17 00:00:00 2001 From: Robbie Court Date: Tue, 30 Jun 2026 16:58:05 +0000 Subject: [PATCH 1/3] get_instances: one row per instance with a multi-template thumbnail carousel The registration MATCH returns one row per (instance, template), so a multi-template instance appeared as several rows each with a single thumbnail. Group by instance and join every alignment's thumbnail into the '; '-joined multi-image carousel the V2 Images column renders, so the frontend can bring the loaded template's thumbnail to the front. Mirrors the existing groupby collapse in TransgeneExpressionHere and the multi-thumbnail carousel the shared _owlery_query_to_results helper already emits. --- src/vfbquery/vfb_queries.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index de38f0d..f00b2cf 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -2461,6 +2461,17 @@ def get_instances(short_form: str, return_dataframe=True, limit: int = -1): # Convert the results to a DataFrame df = pd.DataFrame.from_records(get_dict_cursor()(results)) + # The registration MATCH yields one row per (instance, template). Collapse + # to one row per instance, joining every alignment's thumbnail into the + # "; "-joined multi-image carousel the V2 Images column renders (the + # frontend then brings the loaded template's thumbnail to the front). Other + # columns take the first (representative) value; the ORDER BY id is kept. + if not df.empty and 'id' in df.columns and 'thumbnail' in df.columns: + other_cols = [c for c in df.columns if c not in ('id', 'thumbnail')] + agg = {c: 'first' for c in other_cols} + agg['thumbnail'] = lambda s: '; '.join(t for t in s if isinstance(t, str) and t) + df = df.groupby('id', as_index=False, sort=False).agg(agg) + columns_to_encode = ['label', 'parent', 'source', 'source_id', 'template', 'dataset', 'license', 'thumbnail'] df = encode_markdown_links(df, columns_to_encode) From 327f0d1880b72155468e36478957e1bab60ffdcd Mon Sep 17 00:00:00 2001 From: Robbie Court Date: Tue, 30 Jun 2026 17:12:01 +0000 Subject: [PATCH 2/3] Carousel + dedupe straggler query images; collapse multi-dataset rows - get_instances: collapse the per-(template, dataset) rows the registration + has_source MATCH multiplies into one row per instance: thumbnails join into a DISTINCT '; ' carousel (a template's thumbnail repeats across datasets), and datasets join into a distinct ', ' list (fixes the duplicate/triple rows for images sourced from several datasets, e.g. larval mushroom body output neuron). - get_neuron_neuron_connectivity, get_similar_neurons: replace the single- alignment LIMIT 1 subquery with collect + apoc.text.join so multi-template neurons return all alignments' thumbnails as a carousel (representative template/ technique kept for those columns). Validated against pdb.virtualflybrain.org. Frontend (geppetto-vfb slideshowImageComponent) brings the loaded template's thumbnail to the front of the carousel. --- src/vfbquery/vfb_queries.py | 62 ++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index f00b2cf..8f3ad49 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -2461,15 +2461,26 @@ def get_instances(short_form: str, return_dataframe=True, limit: int = -1): # Convert the results to a DataFrame df = pd.DataFrame.from_records(get_dict_cursor()(results)) - # The registration MATCH yields one row per (instance, template). Collapse - # to one row per instance, joining every alignment's thumbnail into the - # "; "-joined multi-image carousel the V2 Images column renders (the - # frontend then brings the loaded template's thumbnail to the front). Other - # columns take the first (representative) value; the ORDER BY id is kept. + # The registration + has_source MATCH yields one row per + # (instance, template, dataset). Collapse to one row per instance: + # - thumbnail -> the "; "-joined multi-image carousel the V2 Images column + # renders, DISTINCT (a template's thumbnail is identical across the + # instance's datasets, so it must not repeat), so the frontend can bring + # the loaded template's thumbnail to the front; + # - dataset -> a distinct ", "-joined list (an image from several + # datasets is one row listing them, not one row per dataset); + # - other columns take the first (representative) value; ORDER BY id kept. if not df.empty and 'id' in df.columns and 'thumbnail' in df.columns: - other_cols = [c for c in df.columns if c not in ('id', 'thumbnail')] - agg = {c: 'first' for c in other_cols} - agg['thumbnail'] = lambda s: '; '.join(t for t in s if isinstance(t, str) and t) + def _join_distinct(series, sep): + seen = [] + for v in series: + if isinstance(v, str) and v and v not in seen: + seen.append(v) + return sep.join(seen) + agg = {c: 'first' for c in df.columns if c not in ('id', 'thumbnail', 'dataset')} + agg['thumbnail'] = lambda s: _join_distinct(s, '; ') + if 'dataset' in df.columns: + agg['dataset'] = lambda s: _join_distinct(s, ', ') df = df.groupby('id', as_index=False, sort=False).agg(agg) columns_to_encode = ['label', 'parent', 'source', 'source_id', 'template', 'dataset', 'license', 'thumbnail'] @@ -2909,8 +2920,19 @@ def get_similar_neurons(neuron, similarity_score='NBLAST_score', return_datafram WITH n2 OPTIONAL MATCH (n2)<-[:depicts]-(channel:Individual)-[ri:in_register_with]->(:Template)-[:depicts]->(templ:Template) OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) - WITH ri, templ, technique LIMIT 1 - RETURN ri, templ, technique + WITH n2, collect({{ri: ri, templ: templ, technique: technique}}) AS aligns + WITH n2, [a IN aligns WHERE a.templ IS NOT NULL] AS va + RETURN + CASE WHEN size(va)=0 THEN null ELSE head(va).templ END AS templ, + CASE WHEN size(va)=0 THEN null ELSE head(va).technique END AS technique, + apoc.text.join([a IN va | + apoc.text.format("[![%s](%s '%s')](%s)", [ + n2.label + " aligned to " + (CASE WHEN a.templ.symbol[0] <> '' THEN a.templ.symbol[0] ELSE a.templ.label END), + REPLACE(COALESCE(a.ri.thumbnail[0],''),'thumbnailT.png','thumbnail.png'), + n2.label + " aligned to " + (CASE WHEN a.templ.symbol[0] <> '' THEN a.templ.symbol[0] ELSE a.templ.label END), + a.templ.short_form + "," + n2.short_form + ]) + ], '; ') AS thumbnails }} RETURN n2.short_form as id, apoc.text.format("[%s](%s)", [n2.label, n2.short_form]) AS name, @@ -2921,7 +2943,7 @@ def get_similar_neurons(neuron, similarity_score='NBLAST_score', return_datafram CASE WHEN site:Deprecated THEN COALESCE(rx.accession[0],'') ELSE REPLACE(apoc.text.format("[%s](%s)",[rx.accession[0], (site.link_base[0] + rx.accession[0])]), '[null](null)', '') END AS source_id, REPLACE(apoc.text.format("[%s](%s)",[CASE WHEN templ.symbol[0] <> '' THEN templ.symbol[0] ELSE templ.label END,templ.short_form]), '[null](null)', '') AS template, coalesce(technique.label, '') AS technique, - REPLACE(apoc.text.format("[![%s](%s '%s')](%s)",[n2.label + " aligned to " + CASE WHEN templ.symbol[0] <> '' THEN templ.symbol[0] ELSE templ.label END, REPLACE(COALESCE(ri.thumbnail[0],""),"thumbnailT.png","thumbnail.png"), n2.label + " aligned to " + CASE WHEN templ.symbol[0] <> '' THEN templ.symbol[0] ELSE templ.label END, templ.short_form + "," + n2.short_form]), "[![null]( 'null')](null)", "") as thumbnail + thumbnails as thumbnail ORDER BY score DESC""" if limit != -1: @@ -3586,9 +3608,19 @@ def get_neuron_neuron_connectivity(short_form: str, return_dataframe=True, limit WITH oi OPTIONAL MATCH (oi)<-[:depicts]-(channel:Individual)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) OPTIONAL MATCH (channel)-[:is_specified_output_of]->(technique:Class) - WITH channel, template, template_anat, technique, irw - LIMIT 1 - RETURN channel, template, template_anat, technique, irw + WITH oi, collect({{irw: irw, template_anat: template_anat, technique: technique}}) AS aligns + WITH oi, [a IN aligns WHERE a.template_anat IS NOT NULL] AS va + RETURN + CASE WHEN size(va)=0 THEN null ELSE head(va).template_anat END AS template_anat, + CASE WHEN size(va)=0 THEN null ELSE head(va).technique END AS technique, + apoc.text.join([a IN va | + apoc.text.format("[![%s](%s '%s')](%s)", [ + coalesce(oi.label,'image') + " aligned to " + (CASE WHEN a.template_anat.symbol[0] <> '' THEN a.template_anat.symbol[0] ELSE a.template_anat.label END), + REPLACE(COALESCE(a.irw.thumbnail[0],''),'thumbnailT.png','thumbnail.png'), + coalesce(oi.label,'image') + " aligned to " + (CASE WHEN a.template_anat.symbol[0] <> '' THEN a.template_anat.symbol[0] ELSE a.template_anat.label END), + a.template_anat.short_form + "," + oi.short_form + ]) + ], '; ') AS thumbnails }} RETURN oi.short_form AS id, @@ -3599,7 +3631,7 @@ def get_neuron_neuron_connectivity(short_form: str, return_dataframe=True, limit apoc.text.join(coalesce(oi.uniqueFacets, []), '|') AS tags, REPLACE(apoc.text.format("[%s](%s)", [CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, template_anat.short_form]), '[null](null)', '') AS template, coalesce(technique.label, '') AS technique, - REPLACE(apoc.text.format("[![%s](%s '%s')](%s)", [coalesce(oi.label, 'image') + " aligned to " + CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, REPLACE(COALESCE(irw.thumbnail[0], ''), 'thumbnailT.png', 'thumbnail.png'), coalesce(oi.label, 'image') + " aligned to " + CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, template_anat.short_form + "," + oi.short_form]), "[![null]( 'null')](null)", "") AS thumbnail + thumbnails AS thumbnail """ results = vc.nc.commit_list([main_cypher]) From 9e56149b70ddc32d466c91a578d95b2c2e2f88d4 Mon Sep 17 00:00:00 2001 From: Robbie Court Date: Tue, 30 Jun 2026 17:19:22 +0000 Subject: [PATCH 3/3] get_individual_neuron_inputs: dedupe the thumbnail carousel + datasets The Images/Template_Space collects had no DISTINCT, so the cross-product with INSTANCEOF types / imaging techniques repeated each alignment's thumbnail (e.g. 2 templates -> 6 thumbnails, 1 -> 3). Use collect(DISTINCT ...) so it is one thumbnail per template, and align the thumbnail markdown with the other query functions (template,imageId ref + 'aligned to' alt + '; ' separator) so the frontend reorders by the loaded template. Validated against pdb (nThumbs == nTemplates per neuron). --- src/vfbquery/vfb_queries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 8f3ad49..7198cee 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -3042,9 +3042,9 @@ def get_individual_neuron_inputs(neuron_short_form: str, return_dataframe=True, apoc.text.format("[%s](%s)", [b.label, b.short_form]) as Name, apoc.text.format("[%s](%s)", [neuronType.label, neuronType.short_form]) as Type, apoc.text.join(b.uniqueFacets, '|') as Gross_Type, - apoc.text.join(collect(apoc.text.format("[%s](%s)", [templ.label, templ.short_form])), ', ') as Template_Space, + apoc.text.join(collect(DISTINCT apoc.text.format("[%s](%s)", [templ.label, templ.short_form])), ', ') as Template_Space, apoc.text.format("[%s](%s)", [imagingTechnique.label, imagingTechnique.short_form]) as Imaging_Technique, - apoc.text.join(collect(REPLACE(apoc.text.format("[![%s](%s '%s')](%s)",[b.label, REPLACE(COALESCE(image.thumbnail[0],""),"thumbnailT.png","thumbnail.png"), b.label, b.short_form]), "[![null]( 'null')](null)", "")), ' | ') as Images + apoc.text.join(collect(DISTINCT REPLACE(apoc.text.format("[![%s](%s '%s')](%s)",[b.label + " aligned to " + (CASE WHEN templ.symbol[0] <> '' THEN templ.symbol[0] ELSE templ.label END), REPLACE(COALESCE(image.thumbnail[0],""),"thumbnailT.png","thumbnail.png"), b.label + " aligned to " + (CASE WHEN templ.symbol[0] <> '' THEN templ.symbol[0] ELSE templ.label END), templ.short_form + "," + b.short_form]), "[![null]( 'null')](null)", "")), '; ') as Images ORDER BY Weight Desc """