Preferred viz rough sketch

This commit is contained in:
Dannon Baker
2025-05-14 08:50:33 -04:00
parent 3da3ed02ed
commit a1ce48a282
12 changed files with 771 additions and 6 deletions
+71
View File
@@ -0,0 +1,71 @@
import axios from "axios";
import { withPrefix } from "@/utils/redirect";
import { rethrowSimple } from "@/utils/simple-error";
/**
* Interface for datatype to visualization mapping
*/
export interface DatatypeVisualization {
datatype: string; // Datatype extension (e.g., "h5", "tabular")
visualization: string; // Visualization plugin name
defaultParams?: Record<string, string>; // Optional default parameters for the visualization
}
/**
* Fetches the list of datatype to visualization mappings
*/
export async function fetchDatatypeVisualizations(): Promise<DatatypeVisualization[]> {
try {
const { data } = await axios.get(withPrefix("/api/datatypes/visualizations"));
return data;
} catch (error) {
rethrowSimple(error);
}
}
/**
* Updates a datatype visualization mapping
*/
export async function updateDatatypeVisualization(mapping: DatatypeVisualization): Promise<DatatypeVisualization> {
try {
const { data } = await axios.put(withPrefix(`/api/datatypes/visualizations/${mapping.datatype}`), mapping);
return data;
} catch (error) {
rethrowSimple(error);
}
}
/**
* Deletes a datatype visualization mapping
*/
export async function deleteDatatypeVisualization(datatype: string): Promise<void> {
try {
await axios.delete(withPrefix(`/api/datatypes/visualizations/${datatype}`));
} catch (error) {
rethrowSimple(error);
}
}
/**
* Gets the preferred visualization for a specific datatype
*/
export async function getPreferredVisualization(datatype: string): Promise<DatatypeVisualization | null> {
try {
const url = withPrefix(`/api/datatypes/visualizations/${datatype}`);
const { data } = await axios.get(url);
// If the API returns an array with one item, return that item
if (Array.isArray(data) && data.length === 1) {
return data[0];
}
return data;
} catch (error) {
// Return null if no preferred visualization is found
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
rethrowSimple(error);
}
}
@@ -7,6 +7,23 @@ import VueRouter from "vue-router";
import DatasetView from "./DatasetView.vue";
// Mock the datatypeVisualizationsStore
jest.mock("@/stores/datatypeVisualizationsStore", () => ({
useDatatypeVisualizationsStore: jest.fn(() => ({
getPreferredVisualizationForDatatype: jest.fn().mockImplementation((datatype) => {
// Only return a preferred visualization for a specific test datatype
if (datatype === "h5") {
return Promise.resolve({
datatype: "h5",
visualization: "h5web",
defaultParams: { param1: "value1" },
});
}
return Promise.resolve(null);
}),
})),
}));
const DATASET_ID = "dataset_id";
const localVue = getLocalVue();
localVue.use(VueRouter);
@@ -28,6 +45,8 @@ const failedMetadataDataset = { ...mockDataset, state: "failed_metadata" };
const uploadingDataset = { ...mockDataset, state: "upload" };
const runningDataset = { ...mockDataset, state: "running" };
const pausedDataset = { ...mockDataset, state: "paused" };
// Dataset with preferred visualization
const h5Dataset = { ...mockDataset, file_ext: "h5" };
/**
* Mount the DatasetView component with the specified tab and dataset options
@@ -83,6 +102,11 @@ async function mountDatasetView(tab = "preview", options = {}) {
VisualizationsList: true,
DatasetAttributes: true,
DatasetError: true,
// Use a stub for the VisualizationFrame component
VisualizationFrame: {
template: '<div class="viz-frame"></div>',
props: ["datasetId", "visualization", "visualizationParams"],
},
},
mocks: {
$store: {
@@ -268,6 +292,32 @@ describe("DatasetView", () => {
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
expect(wrapper.vm.$props.tab).toBe("preview");
});
it("uses preferred visualization for supported datatypes", async () => {
const wrapper = await mountDatasetView("preview", { dataset: h5Dataset });
await flushPromises(); // Wait for preferred visualization check
// Check that the preferredVisualization was set
expect(wrapper.vm.preferredVisualization).toBe("h5web");
expect(wrapper.vm.preferredVisualizationParams).toEqual({ param1: "value1" });
// Check that we're using the VisualizationFrame for the preferred visualization
expect(wrapper.findComponent({ name: "VisualizationFrame" }).exists()).toBe(true);
expect(wrapper.find("iframe").exists()).toBe(false);
});
it("falls back to default preview for unsupported datatypes", async () => {
const wrapper = await mountDatasetView("preview");
await flushPromises(); // Wait for preferred visualization check
// No preferred visualization should be set
expect(wrapper.vm.preferredVisualization).toBeNull();
// Check that we're using the default iframe
expect(wrapper.findComponent({ name: "VisualizationFrame" }).exists()).toBe(false);
expect(wrapper.find("iframe").exists()).toBe(true);
expect(wrapper.find("iframe").attributes("src")).toBe(`/datasets/${DATASET_ID}/display/?preview=true`);
});
});
describe("URL and routing", () => {
+60 -4
View File
@@ -3,12 +3,13 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BLink, BTab, BTabs } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router/composables";
import { STATES } from "@/components/History/Content/model/states";
import { usePersistentToggle } from "@/composables/persistentToggle";
import { useDatasetStore } from "@/stores/datasetStore";
import { useDatatypeVisualizationsStore } from "@/stores/datatypeVisualizationsStore";
import Heading from "../Common/Heading.vue";
import DatasetError from "../DatasetInformation/DatasetError.vue";
@@ -16,10 +17,12 @@ import LoadingSpan from "../LoadingSpan.vue";
import DatasetAttributes from "@/components/DatasetInformation/DatasetAttributes.vue";
import DatasetDetails from "@/components/DatasetInformation/DatasetDetails.vue";
import VisualizationsList from "@/components/Visualizations/Index.vue";
import VisualizationFrame from "@/components/Visualizations/VisualizationFrame.vue";
library.add(faChevronUp, faChevronDown);
const datasetStore = useDatasetStore();
const datatypeVisualizationsStore = useDatatypeVisualizationsStore();
const router = useRouter();
const iframeLoading = ref(true);
@@ -50,10 +53,23 @@ type TabName = (typeof TABS)[keyof typeof TABS];
const TAB_VALUES = Object.values(TABS) as TabName[];
const activeTab = ref<TabName>(TABS.PREVIEW);
const preferredVisualization = ref<string | null>(null);
const preferredVisualizationParams = ref<Record<string, string> | null>(null);
const dataset = computed(() => datasetStore.getDataset(props.datasetId));
const isLoading = computed(() => datasetStore.isLoadingDataset(props.datasetId));
const displayUrl = computed(() => `/datasets/${props.datasetId}/display/?preview=true`);
// Compute display URL based on whether we have a preferred visualization
const displayUrl = computed(() => {
// Standard preview display URL
if (!preferredVisualization.value) {
return `/datasets/${props.datasetId}/display/?preview=true`;
}
// Use preferred visualization
return null;
});
const showError = computed(
() => dataset.value && (dataset.value.state === "error" || dataset.value.state === "failed_metadata")
);
@@ -72,6 +88,28 @@ function toggleHeader() {
toggleHeaderCollapse();
}
// Check if the dataset has a preferred visualization by datatype
async function checkPreferredVisualization() {
if (!dataset.value || !dataset.value.file_ext) {
preferredVisualization.value = null;
return;
}
try {
const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype(dataset.value.file_ext);
if (mapping) {
preferredVisualization.value = mapping.visualization;
preferredVisualizationParams.value = mapping.defaultParams || null;
} else {
preferredVisualization.value = null;
preferredVisualizationParams.value = null;
}
} catch (error) {
preferredVisualization.value = null;
preferredVisualizationParams.value = null;
}
}
function onTabChange(tabName: TabName) {
if (!TAB_VALUES.includes(tabName)) {
console.error("Invalid tab name received:", tabName);
@@ -130,6 +168,16 @@ watch(
}
}
);
// Watch for changes to the dataset to check for preferred visualizations
watch(() => dataset.value?.file_ext, checkPreferredVisualization);
onMounted(() => {
// Check for preferred visualization when component is mounted
if (dataset.value) {
checkPreferredVisualization();
}
});
</script>
<template>
<div v-if="dataset && !isLoading" class="dataset-view d-flex flex-column">
@@ -189,9 +237,17 @@ watch(
<LoadingSpan message="Loading preview" />
</div>
<!-- Iframe for dataset preview -->
<!-- Show preferred visualization if available -->
<template v-if="preferredVisualization">
<VisualizationFrame
:dataset-id="datasetId"
:visualization="preferredVisualization"
:visualization-params="preferredVisualizationParams" />
</template>
<!-- Default iframe preview otherwise -->
<iframe
:src="displayUrl"
v-else
:src="displayUrl"
title="galaxy dataset display frame"
class="dataset-preview-iframe"
frameborder="0"
@@ -0,0 +1,230 @@
<template>
<div class="datatype-visualizations-manager">
<h2>Datatype Visualization Mappings</h2>
<p class="mb-4">
Configure preferred visualizations for datatypes. These visualizations will be used as the default preview
when viewing datasets of the specified type.
</p>
<!-- Loading state -->
<div v-if="isLoading" class="text-center my-5">
<font-awesome-icon icon="spinner" pulse size="2x" />
<p class="mt-3">Loading datatype visualization mappings...</p>
</div>
<!-- Error message -->
<BAlert v-if="error" show variant="danger" class="mb-4"> <strong>Error:</strong> {{ error }} </BAlert>
<!-- Main content -->
<div v-if="!isLoading && !error">
<!-- Create new mapping form -->
<BCard class="mb-4" header="Add New Mapping">
<BForm @submit.prevent="addNewMapping">
<BFormGroup label="Datatype" label-for="new-datatype">
<BFormSelect
id="new-datatype"
v-model="newMapping.datatype"
:options="availableDatatypes"
required>
<template v-slot:first>
<BFormSelectOption :value="null">-- Select Datatype --</BFormSelectOption>
</template>
</BFormSelect>
</BFormGroup>
<BFormGroup label="Visualization" label-for="new-visualization">
<BFormSelect
id="new-visualization"
v-model="newMapping.visualization"
:options="availableVisualizations"
required>
<template v-slot:first>
<BFormSelectOption :value="null">-- Select Visualization --</BFormSelectOption>
</template>
</BFormSelect>
</BFormGroup>
<!-- Parameters section, can be expanded in the future -->
<BButton type="submit" variant="primary" :disabled="!isFormValid">Add Mapping</BButton>
</BForm>
</BCard>
<!-- Existing mappings table -->
<BCard header="Existing Mappings">
<BTable
striped
hover
:items="visualizationMappings"
:fields="fields"
empty-text="No datatype visualization mappings defined."
class="mb-0">
<template v-slot:cell(actions)="row">
<BButton
size="sm"
variant="danger"
:disabled="isDeleting[row.item.datatype]"
@click="confirmDelete(row.item)">
<font-awesome-icon v-if="isDeleting[row.item.datatype]" icon="spinner" pulse />
<font-awesome-icon v-else icon="trash" />
Delete
</BButton>
</template>
</BTable>
</BCard>
</div>
<!-- Confirmation modal -->
<BModal
v-model="showDeleteModal"
title="Confirm Deletion"
ok-variant="danger"
ok-title="Delete"
@ok="deleteMapping">
<p>
Are you sure you want to delete the visualization mapping for datatype
<strong>{{ selectedMapping?.datatype }}</strong
>?
</p>
</BModal>
</div>
</template>
<script setup lang="ts">
import {
BAlert,
BButton,
BCard,
BForm,
BFormGroup,
BFormSelect,
BFormSelectOption,
BModal,
BTable,
} from "bootstrap-vue";
import { computed, onMounted, ref } from "vue";
import { type DatatypeVisualization } from "@/api/datatypeVisualizations";
import { fetchPlugins } from "@/api/plugins";
import { useDatatypeStore } from "@/stores/datatypeStore";
import { useDatatypeVisualizationsStore } from "@/stores/datatypeVisualizationsStore";
const datatypeVisualizationsStore = useDatatypeVisualizationsStore();
const datatypeStore = useDatatypeStore();
// State
const isLoading = ref(true);
const error = ref("");
const availableDatatypes = ref<Array<{ value: string; text: string }>>([]);
const availableVisualizations = ref<Array<{ value: string; text: string }>>([]);
const newMapping = ref<DatatypeVisualization>({
datatype: "",
visualization: "",
});
const showDeleteModal = ref(false);
const selectedMapping = ref<DatatypeVisualization | null>(null);
const isDeleting = ref<Record<string, boolean>>({});
// Table configuration
const fields = [
{ key: "datatype", label: "Datatype", sortable: true },
{ key: "visualization", label: "Visualization", sortable: true },
{ key: "actions", label: "Actions" },
];
// Computed properties
const visualizationMappings = computed(() => {
return datatypeVisualizationsStore.visualizationMappings;
});
const isFormValid = computed(() => {
return newMapping.value.datatype && newMapping.value.visualization;
});
// Initialize component
async function initialize() {
isLoading.value = true;
error.value = "";
try {
// Load datatypes
await datatypeStore.loadDatatypes();
// Convert to select options
availableDatatypes.value = Object.entries(datatypeStore.datatypes)
.map(([ext, info]) => ({
value: ext,
text: `${ext} - ${info.description || ext}`,
}))
.sort((a, b) => a.text.localeCompare(b.text));
// Load visualizations
const plugins = await fetchPlugins();
availableVisualizations.value = plugins
.map((plugin) => ({
value: plugin.name,
text: plugin.name,
}))
.sort((a, b) => a.text.localeCompare(b.text));
// Load existing mappings
await datatypeVisualizationsStore.loadAllMappings();
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
} finally {
isLoading.value = false;
}
}
// Add a new mapping
async function addNewMapping() {
if (!isFormValid.value) {
return;
}
try {
await datatypeVisualizationsStore.updateMapping(newMapping.value);
// Reset form
newMapping.value = {
datatype: "",
visualization: "",
};
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
}
}
// Confirm deletion of a mapping
function confirmDelete(mapping: DatatypeVisualization) {
selectedMapping.value = mapping;
showDeleteModal.value = true;
}
// Delete a mapping
async function deleteMapping() {
if (!selectedMapping.value) {
return;
}
const datatype = selectedMapping.value.datatype;
isDeleting.value[datatype] = true;
try {
await datatypeVisualizationsStore.deleteMapping(datatype);
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
} finally {
isDeleting.value[datatype] = false;
selectedMapping.value = null;
}
}
// Load data when component is mounted
onMounted(initialize);
</script>
<style scoped>
.datatype-visualizations-manager {
padding: 1rem;
}
</style>
+6
View File
@@ -23,6 +23,12 @@
</strong>
- See all data tables available in this Galaxy.
</li>
<li>
<strong>
<router-link to="/admin/datatype_visualizations">Datatype Visualizations</router-link>
</strong>
- Configure preferred visualizations for datasets based on their datatype.
</li>
<li>
<strong>
<router-link to="/admin/display_applications">Display Applications</router-link>
@@ -6,6 +6,7 @@ import DataManagerTable from "components/admin/DataManager/DataManagerTable";
import DataManagerView from "components/admin/DataManager/DataManagerView";
import DataTables from "components/admin/DataTables";
import DataTypes from "components/admin/DataTypes";
import DatatypeVisualizations from "components/admin/DatatypeVisualizations";
import ToolboxDependencies from "components/admin/Dependencies/Landing";
import DisplayApplications from "components/admin/DisplayApplications";
import ErrorStack from "components/admin/ErrorStack";
@@ -47,6 +48,7 @@ export default [
},
{ path: "data_tables", component: DataTables },
{ path: "data_types", component: DataTypes },
{ path: "datatype_visualizations", component: DatatypeVisualizations },
{ path: "display_applications", component: DisplayApplications },
{ path: "error_stack", component: ErrorStack },
{
@@ -0,0 +1,129 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import type { DatatypeVisualization } from "@/api/datatypeVisualizations";
import {
deleteDatatypeVisualization,
fetchDatatypeVisualizations,
getPreferredVisualization,
updateDatatypeVisualization,
} from "@/api/datatypeVisualizations";
export const useDatatypeVisualizationsStore = defineStore("datatypeVisualizations", () => {
const visualizationMappings = ref<DatatypeVisualization[]>([]);
const loadingStates = ref<Record<string, boolean>>({});
const errorStates = ref<Record<string, string>>({});
// Computed maps for quick lookups
const mappingByDatatype = computed(() => {
const result: Record<string, DatatypeVisualization> = {};
visualizationMappings.value.forEach((mapping) => {
result[mapping.datatype] = mapping;
});
return result;
});
/**
* Load all datatype visualization mappings
*/
async function loadAllMappings() {
loadingStates.value["all"] = true;
errorStates.value["all"] = "";
try {
visualizationMappings.value = await fetchDatatypeVisualizations();
} catch (error) {
errorStates.value["all"] = error instanceof Error ? error.message : String(error);
} finally {
loadingStates.value["all"] = false;
}
}
/**
* Get the preferred visualization for a specific datatype
*/
async function getPreferredVisualizationForDatatype(datatype: string): Promise<DatatypeVisualization | null> {
// First check our store
if (mappingByDatatype.value[datatype]) {
return mappingByDatatype.value[datatype];
}
// If not in store, query the API
loadingStates.value[datatype] = true;
errorStates.value[datatype] = "";
try {
const result = await getPreferredVisualization(datatype);
// If found, add to our store
if (result) {
visualizationMappings.value.push(result);
}
return result;
} catch (error) {
errorStates.value[datatype] = error instanceof Error ? error.message : String(error);
return null;
} finally {
loadingStates.value[datatype] = false;
}
}
/**
* Update a visualization mapping
*/
async function updateMapping(mapping: DatatypeVisualization) {
loadingStates.value[mapping.datatype] = true;
errorStates.value[mapping.datatype] = "";
try {
const updatedMapping = await updateDatatypeVisualization(mapping);
// Update in our store
const index = visualizationMappings.value.findIndex((m) => m.datatype === mapping.datatype);
if (index >= 0) {
visualizationMappings.value[index] = updatedMapping;
} else {
visualizationMappings.value.push(updatedMapping);
}
return updatedMapping;
} catch (error) {
errorStates.value[mapping.datatype] = error instanceof Error ? error.message : String(error);
throw error;
} finally {
loadingStates.value[mapping.datatype] = false;
}
}
/**
* Delete a visualization mapping
*/
async function deleteMapping(datatype: string) {
loadingStates.value[datatype] = true;
errorStates.value[datatype] = "";
try {
await deleteDatatypeVisualization(datatype);
// Remove from our store
visualizationMappings.value = visualizationMappings.value.filter((m) => m.datatype !== datatype);
} catch (error) {
errorStates.value[datatype] = error instanceof Error ? error.message : String(error);
throw error;
} finally {
loadingStates.value[datatype] = false;
}
}
return {
visualizationMappings,
loadingStates,
errorStates,
mappingByDatatype,
loadAllMappings,
getPreferredVisualizationForDatatype,
updateMapping,
deleteMapping,
};
});
@@ -1148,6 +1148,23 @@
<!-- rdeval types -->
<datatype extension="rd" type="galaxy.datatypes.binary:Binary" mimetype="application/octet-stream" display_in_upload="true" subclass="true" description="Rdeval read sketch"/>
</registration>
<visualization_mappings>
<!--
This section maps datatypes to their preferred visualization plugins.
These visualizations will be used as the default preview when viewing datasets of the specified type.
Attributes:
- datatype: The file extension/datatype to map
- visualization: The visualization plugin to use
- default_params: Optional JSON-formatted parameters to pass to the visualization
-->
<visualization_mapping datatype="h5" visualization="vitessce" />
<visualization_mapping datatype="bam" visualization="igv" />
<visualization_mapping datatype="vcf" visualization="jbrowse" />
<!-- Add more mappings as needed -->
</visualization_mappings>
<sniffers>
<!--
The order in which Galaxy attempts to determine data types is
+40
View File
@@ -11,6 +11,19 @@ from pydantic import (
RootModel,
)
__all__ = [
"CompositeFileInfo",
"DatatypeDetails",
"DatatypesMap",
"DatatypesCombinedMap",
"DatatypeConverter",
"DatatypeConverterList",
"DatatypeEDAMDetails",
"DatatypesEDAMDetailsDict",
"DatatypeVisualizationMapping",
"DatatypeVisualizationMappingsList",
]
class CompositeFileInfo(BaseModel):
name: str = Field(..., title="Name", description="The name of this composite file") # Mark this field as required
@@ -130,3 +143,30 @@ class DatatypesEDAMDetailsDict(RootModel):
title="Dict of EDAM details for formats",
default={},
)
class DatatypeVisualizationMapping(BaseModel):
datatype: str = Field(
..., # Mark this field as required
title="Datatype",
description="The datatype extension this visualization applies to",
examples=["bam", "h5", "vcf"],
)
visualization: str = Field(
..., # Mark this field as required
title="Visualization",
description="The visualization plugin to use",
examples=["igv", "trackster", "vitessce"],
)
default_params: Optional[Dict[str, str]] = Field(
default=None,
title="Default Parameters",
description="Optional parameters to pass to the visualization plugin",
)
class DatatypeVisualizationMappingsList(RootModel):
root: List[DatatypeVisualizationMapping] = Field(
title="List of datatype visualization mappings",
default=[],
)
+84
View File
@@ -103,6 +103,8 @@ class Registry:
self._edam_formats_mapping = None
self._edam_data_mapping = None
self._converters_by_datatype = {}
# Datatype visualization mappings
self.visualization_mappings: Dict[str, Dict[str, Any]] = {}
# Build sites
self.build_sites = {}
self.display_sites = {}
@@ -417,6 +419,9 @@ class Registry:
# Load build sites
if use_build_sites:
self._load_build_sites(root)
# Load visualization mappings
self._load_visualization_mappings(root)
self.set_default_values()
def append_to_sniff_order() -> None:
@@ -436,6 +441,60 @@ class Registry:
append_to_sniff_order()
def _load_visualization_mappings(self, root):
"""
Load datatype to visualization mappings from the datatypes_conf.xml file.
Administrators can define preferred visualizations for datatypes in the
datatypes_conf.xml configuration file using the visualization_mappings section.
For example:
<visualization_mappings>
<visualization_mapping datatype="bam" visualization="igv" />
<visualization_mapping datatype="vcf" visualization="jbrowse" />
<visualization_mapping datatype="h5" visualization="vitessce" default_params='{"layer":"primary"}' />
</visualization_mappings>
These mappings are read-only and must be manually edited in the configuration file.
"""
# Clear existing mappings
self.visualization_mappings = {}
# Find the visualization_mappings element
mappings_elem = root.find("visualization_mappings")
if mappings_elem is None:
# No mappings defined, that's okay
return
for mapping_elem in mappings_elem.findall("visualization_mapping"):
datatype = mapping_elem.get("datatype", None)
visualization = mapping_elem.get("visualization", None)
default_params_str = mapping_elem.get("default_params", None)
# Skip incomplete mappings
if not datatype or not visualization:
self.log.warning("Incomplete visualization mapping (missing datatype or visualization)")
continue
# Parse default parameters if provided
default_params = None
if default_params_str:
try:
default_params = galaxy.util.safe_loads(default_params_str)
except Exception:
self.log.exception("Error parsing default_params for visualization mapping: %s -> %s",
datatype, visualization)
continue
# Store the mapping
self.visualization_mappings[datatype] = {
"visualization": visualization,
"default_params": default_params
}
self.log.debug("Loaded visualization mapping: %s -> %s", datatype, visualization)
def _load_build_sites(self, root):
def load_build_site(build_site_config):
# Take in either an XML element or simple dictionary from YAML and add build site for this.
@@ -489,6 +548,31 @@ class Registry:
def get_display_sites(self, site_type):
return self.display_sites.get(site_type, [])
def get_preferred_visualization(self, datatype_extension):
"""
Get the preferred visualization mapping for a specific datatype extension.
Returns a dictionary with 'visualization' and 'default_params' keys, or None if no mapping exists.
Preferred visualizations are defined in the datatypes_conf.xml configuration file using
the visualization_mappings section. These mappings determine which visualization plugin
should be used by default when viewing datasets of a specific type.
Example configuration:
<visualization_mappings>
<visualization_mapping datatype="bam" visualization="igv" />
</visualization_mappings>
"""
return self.visualization_mappings.get(datatype_extension)
def get_all_visualization_mappings(self):
"""
Get all datatype to visualization mappings.
Returns a dictionary where keys are datatype extensions and values are mapping configurations.
Mappings are defined in the datatypes_conf.xml configuration file and are read-only.
"""
return self.visualization_mappings
def load_datatype_sniffers(
self,
+45 -1
View File
@@ -11,8 +11,10 @@ from galaxy.datatypes._schema import (
DatatypeConverterList,
DatatypeDetails,
DatatypesCombinedMap,
DatatypesEDAMDetailsDict,
DatatypesEDAMDetailsDict,
DatatypesMap,
DatatypeVisualizationMapping,
DatatypeVisualizationMappingsList,
)
from galaxy.datatypes.data import Data
from galaxy.datatypes.registry import Registry
@@ -122,15 +124,57 @@ def view_edam_data(
return datatypes_registry.edam_data
def view_visualization_mappings(datatypes_registry: Registry, datatype: Optional[str] = None) -> DatatypeVisualizationMappingsList:
"""
Get datatype visualization mappings from the registry.
Args:
datatypes_registry: The datatypes registry
datatype: If provided, return only the mapping for this datatype extension
Returns:
A list of datatype visualization mappings
"""
mappings = []
# Get all mappings
all_mappings = datatypes_registry.get_all_visualization_mappings()
# Filter for a specific datatype if requested
if datatype and datatype in all_mappings:
mapping_info = all_mappings[datatype]
mappings.append({
"datatype": datatype,
"visualization": mapping_info["visualization"],
"default_params": mapping_info.get("default_params")
})
elif not datatype:
for dt, mapping_info in all_mappings.items():
mappings.append({
"datatype": dt,
"visualization": mapping_info["visualization"],
"default_params": mapping_info.get("default_params")
})
return parse_obj_as(DatatypeVisualizationMappingsList, mappings)
__all__ = (
"DatatypeConverterList",
"DatatypeDetails",
"DatatypesCombinedMap",
"DatatypesEDAMDetailsDict",
"DatatypesMap",
"DatatypeVisualizationMapping",
"DatatypeVisualizationMappingsList",
"view_index",
"view_mapping",
"view_types_and_mapping",
"view_sniffers",
"view_converters",
"view_edam_formats",
"view_edam_data",
"view_visualization_mappings",
)
+37 -1
View File
@@ -11,7 +11,7 @@ from typing import (
Union,
)
from fastapi import Query
from fastapi import Path, Query
from galaxy.datatypes.registry import Registry
from galaxy.managers.datatypes import (
@@ -20,12 +20,15 @@ from galaxy.managers.datatypes import (
DatatypesCombinedMap,
DatatypesEDAMDetailsDict,
DatatypesMap,
DatatypeVisualizationMapping,
DatatypeVisualizationMappingsList,
view_converters,
view_edam_data,
view_edam_formats,
view_index,
view_mapping,
view_sniffers,
view_visualization_mappings,
)
from . import (
depends,
@@ -165,3 +168,36 @@ class FastAPIDatatypes:
"""Gets a map of datatypes and their corresponding EDAM data.
EDAM data contains the EDAM iri, label, and definition."""
return view_edam_data(self.datatypes_registry, True)
@router.get(
"/api/datatypes/visualizations",
public=True,
summary="Returns a list of all datatype visualization mappings",
response_description="List of all datatype visualization mappings",
response_model=DatatypeVisualizationMappingsList,
)
async def visualizations(self) -> DatatypeVisualizationMappingsList:
"""Gets a list of all datatype visualization mappings."""
return view_visualization_mappings(self.datatypes_registry)
@router.get(
"/api/datatypes/visualizations/{datatype}",
public=True,
summary="Returns the visualization mapping for a specific datatype",
response_description="Visualization mapping for the specified datatype",
response_model=DatatypeVisualizationMappingsList,
)
async def visualization_for_datatype(
self,
datatype: str = Path(
...,
title="Datatype",
description="Datatype extension to get visualization mapping for",
examples=["bam", "h5"],
),
) -> DatatypeVisualizationMappingsList:
"""Gets the visualization mapping for a specific datatype.
Mappings are defined in the datatypes_conf.xml configuration file.
"""
return view_visualization_mappings(self.datatypes_registry, datatype)