UNPKG

@jbrowse/plugin-wiggle

Version:

JBrowse 2 wiggle adapters, tracks, etc.

539 lines (538 loc) 21.5 kB
import { lazy } from 'react'; import { fromNewick } from '@gmod/hclust'; import { getConf } from '@jbrowse/core/configuration'; import { set1 as colors } from '@jbrowse/core/ui/colors'; import { getContainingView, getSession, max, measureText, } from '@jbrowse/core/util'; import { stopStopToken } from '@jbrowse/core/util/stopToken'; import { cast, isAlive, types } from '@jbrowse/mobx-state-tree'; import EqualizerIcon from '@mui/icons-material/Equalizer'; import VisibilityIcon from '@mui/icons-material/Visibility'; import { ascending } from '@mui/x-charts-vendor/d3-array'; import deepEqual from 'fast-deep-equal'; import { cluster, hierarchy } from "../d3-hierarchy2/index.js"; import SharedWiggleMixin from "../shared/SharedWiggleMixin.js"; import axisPropsFromTickScale from "../shared/axisPropsFromTickScale.js"; import { YSCALEBAR_LABEL_OFFSET, getScale } from "../util.js"; const randomColor = () => '#000000'.replaceAll('0', () => (~~(Math.random() * 16)).toString(16)); const Tooltip = lazy(() => import("./components/Tooltip.js")); const SetColorDialog = lazy(() => import("./components/SetColorDialog.js")); const WiggleClusterDialog = lazy(() => import("./components/WiggleClusterDialog/WiggleClusterDialog.js")); const rendererTypes = new Map([ ['xyplot', 'MultiXYPlotRenderer'], ['multirowxy', 'MultiRowXYPlotRenderer'], ['multirowdensity', 'MultiDensityRenderer'], ['multiline', 'MultiLineRenderer'], ['multirowline', 'MultiRowLineRenderer'], ]); export function stateModelFactory(_pluginManager, configSchema) { return types .compose('MultiLinearWiggleDisplay', SharedWiggleMixin(configSchema), types.model({ type: types.literal('MultiLinearWiggleDisplay'), layout: types.optional(types.frozen(), []), showSidebar: true, clusterTree: types.maybe(types.string), treeAreaWidth: types.optional(types.number, 80), showTreeSetting: types.maybe(types.boolean), subtreeFilter: types.maybe(types.array(types.string)), })) .volatile(() => ({ sourcesLoadingStopToken: undefined, featureUnderMouseVolatile: undefined, sourcesVolatile: undefined, hoveredTreeNode: undefined, treeCanvas: undefined, mouseoverCanvas: undefined, })) .actions(self => ({ setShowSidebar(arg) { self.showSidebar = arg; }, setSourcesLoading(str) { if (self.sourcesLoadingStopToken) { stopStopToken(self.sourcesLoadingStopToken); } self.sourcesLoadingStopToken = str; }, setLayout(layout, clearTree = true) { const orderChanged = clearTree && self.clusterTree && self.layout.length === layout.length && self.layout.some((source, idx) => source.name !== layout[idx]?.name); self.layout = layout; if (orderChanged) { self.clusterTree = undefined; } }, clearLayout() { self.layout = []; self.clusterTree = undefined; }, setClusterTree(tree) { self.clusterTree = tree; }, setTreeAreaWidth(width) { self.treeAreaWidth = width; }, setShowTree(arg) { self.showTreeSetting = arg; }, setSubtreeFilter(names) { self.subtreeFilter = names ? cast(names) : undefined; }, setHoveredTreeNode(node) { self.hoveredTreeNode = node; }, setTreeCanvasRef(ref) { self.treeCanvas = ref || undefined; }, setMouseoverCanvasRef(ref) { self.mouseoverCanvas = ref || undefined; }, setSources(sources) { if (!deepEqual(sources, self.sourcesVolatile)) { self.sourcesVolatile = sources; } }, setFeatureUnderMouse(f) { self.featureUnderMouseVolatile = f; }, })) .views(self => ({ get featureUnderMouse() { return self.featureUnderMouseVolatile; }, get TooltipComponent() { return Tooltip; }, get rendererTypeName() { const name = self.rendererTypeNameSimple; const rendererType = rendererTypes.get(name); if (!rendererType) { throw new Error(`unknown renderer ${name}`); } return rendererType; }, })) .views(self => ({ get graphType() { return (self.rendererTypeName === 'MultiXYPlotRenderer' || self.rendererTypeName === 'MultiRowXYPlotRenderer' || self.rendererTypeName === 'MultiLineRenderer' || self.rendererTypeName === 'MultiRowLineRenderer'); }, get needsFullHeightScalebar() { return (self.rendererTypeName === 'MultiXYPlotRenderer' || self.rendererTypeName === 'MultiLineRenderer'); }, get isMultiRow() { return (self.rendererTypeName === 'MultiRowXYPlotRenderer' || self.rendererTypeName === 'MultiRowLineRenderer' || self.rendererTypeName === 'MultiDensityRenderer'); }, get canHaveFill() { return (self.rendererTypeName === 'MultiXYPlotRenderer' || self.rendererTypeName === 'MultiRowXYPlotRenderer'); }, get needsCustomLegend() { return self.rendererTypeName === 'MultiDensityRenderer'; }, get renderColorBoxes() { return !(self.rendererTypeName === 'MultiRowLineRenderer' || self.rendererTypeName === 'MultiRowXYPlotRenderer'); }, get prefersOffset() { return this.isMultiRow; }, get sourcesWithoutLayout() { const sources = Object.fromEntries(self.sourcesVolatile?.map(s => [s.name, s]) || []); const iter = self.sourcesVolatile; return iter ?.map(s => ({ ...sources[s.name], ...s, })) .map((s, i) => ({ ...s, color: s.color || (!this.isMultiRow ? colors[i] || randomColor() : 'blue'), })); }, get sources() { const sources = Object.fromEntries(self.sourcesVolatile?.map(s => [s.name, s]) || []); const iter = self.layout.length ? self.layout : self.sourcesVolatile; let result = iter ?.map(s => ({ ...sources[s.name], ...s, })) .map((s, i) => ({ ...s, color: s.color || (!this.isMultiRow ? colors[i] || randomColor() : 'blue'), })); if (result && self.subtreeFilter?.length) { const filterSet = new Set(self.subtreeFilter); result = result.filter(s => filterSet.has(s.name)); } return result; }, get quantitativeStatsReady() { const view = getContainingView(self); return (view.initialized && self.featureDensityStatsReadyAndRegionNotTooLarge && !self.error); }, })) .views(self => ({ get showTree() { return self.showTreeSetting ?? true; }, get rowHeight() { const { sources, height, isMultiRow } = self; return isMultiRow ? height / (sources?.length || 1) : height; }, get rowHeightTooSmallForScalebar() { return this.rowHeight < 70; }, get useMinimalTicks() { return (getConf(self, 'minimalTicks') || this.rowHeightTooSmallForScalebar); }, get root() { const newick = self.clusterTree; if (!newick) { return undefined; } const tree = fromNewick(newick); let root = hierarchy(tree, (d) => d.children) .sum((d) => (d.children ? 0 : 1)) .sort((a, b) => ascending(a.data.height || 1, b.data.height || 1)); if (self.subtreeFilter?.length) { const filterSet = new Set(self.subtreeFilter); const getLeafNames = (node) => { if (!node.children?.length) { return [node.data.name]; } return node.children.flatMap(child => getLeafNames(child)); }; const findSubtree = (node) => { const leafNames = getLeafNames(node); if (leafNames.length === filterSet.size && leafNames.every(name => filterSet.has(name))) { return node; } if (node.children) { for (const child of node.children) { const found = findSubtree(child); if (found) { return found; } } } return undefined; }; const subtree = findSubtree(root); if (subtree) { root = subtree; } } return root; }, })) .views(self => ({ get totalHeight() { return self.rowHeight * (self.sources?.length || 1); }, get hierarchy() { const r = self.root; if (!r || !self.sources?.length) { return undefined; } const clust = cluster(); clust.size([self.rowHeight * self.sources.length, self.treeAreaWidth]); clust.separation(() => 1); clust(r); return r; }, })) .views(self => { const { renderProps: superRenderProps } = self; return { adapterProps() { const superProps = superRenderProps(); return { ...superProps, config: self.rendererConfig, filters: self.filters, resolution: self.resolution, sources: self.sources, }; }, get ticks() { const { scaleType, domain, isMultiRow, rowHeight, useMinimalTicks } = self; if (!domain) { return undefined; } const offset = isMultiRow ? 0 : YSCALEBAR_LABEL_OFFSET; const ticks = axisPropsFromTickScale(getScale({ scaleType, domain, range: [rowHeight - offset, offset], inverted: getConf(self, 'inverted'), }), 4); return useMinimalTicks ? { ...ticks, values: domain } : ticks; }, get colors() { return [ 'red', 'blue', 'green', 'orange', 'purple', 'cyan', 'pink', 'darkblue', 'darkred', 'pink', ]; }, get quantitativeStatsRelevantToCurrentZoom() { const view = getContainingView(self); return self.stats?.currStatsBpPerPx === view.bpPerPx; }, }; }) .views(self => ({ get legendFontSize() { return Math.min(self.rowHeight, 8); }, get canDisplayLegendLabels() { return self.rowHeight > 7; }, get labelWidth() { const minWidth = 20; return max(self.sources ?.map(s => measureText(s.name, this.legendFontSize)) .map(width => (this.canDisplayLegendLabels ? width : minWidth)) || []); }, renderProps() { const superProps = self.adapterProps(); return { ...superProps, notReady: superProps.notReady || !self.sources || !self.stats, displayCrossHatches: self.displayCrossHatches, height: self.height, ticks: self.ticks, stats: self.stats, scaleOpts: self.scaleOpts, offset: self.isMultiRow ? 0 : YSCALEBAR_LABEL_OFFSET, }; }, renderingProps() { return { displayModel: self, }; }, get hasResolution() { return self.adapterCapabilities.includes('hasResolution'); }, get hasGlobalStats() { return self.adapterCapabilities.includes('hasGlobalStats'); }, get fillSetting() { if (self.filled) { return 0; } else if (self.minSize === 1) { return 1; } else { return 2; } }, })) .views(self => { const { trackMenuItems: superTrackMenuItems } = self; const hasRenderings = getConf(self, 'defaultRendering'); return { trackMenuItems() { return [ ...superTrackMenuItems(), { label: 'Show...', icon: VisibilityIcon, subMenu: [ { label: 'Show tooltips', type: 'checkbox', checked: self.showTooltipsEnabled, onClick: () => { self.setShowTooltips(!self.showTooltipsEnabled); }, }, { label: 'Show sidebar', type: 'checkbox', checked: self.showSidebar, onClick: () => { self.setShowSidebar(!self.showSidebar); }, }, ...(self.isMultiRow ? [ { label: `Show tree${!self.clusterTree ? ' (run clustering first)' : ''}`, type: 'checkbox', checked: self.showTree, disabled: !self.clusterTree, onClick: () => { self.setShowTree(!self.showTree); }, }, ...(self.subtreeFilter?.length ? [ { label: 'Clear subtree filter', onClick: () => { self.setSubtreeFilter(undefined); }, }, ] : []), ] : []), ...(self.graphType ? [ { type: 'checkbox', label: 'Show cross hatches', checked: self.displayCrossHatchesSetting, onClick: () => { self.toggleCrossHatches(); }, }, ] : []), ], }, { label: 'Score', icon: EqualizerIcon, subMenu: self.scoreTrackMenuItems(), }, ...(self.canHaveFill ? [ { label: 'Fill mode', subMenu: ['filled', 'no fill', 'no fill w/ emphasis'].map((elt, idx) => ({ label: elt, type: 'radio', checked: self.fillSetting === idx, onClick: () => { self.setFill(idx); }, })), }, ] : []), ...(hasRenderings ? [ { label: 'Renderer type', subMenu: [ 'xyplot', 'multirowxy', 'multirowdensity', 'multiline', 'multirowline', ].map(key => ({ label: key, type: 'radio', checked: self.rendererTypeNameSimple === key, onClick: () => { self.setRendererType(key); }, })), }, ] : []), ...(self.isMultiRow ? [ { label: 'Cluster rows by score', onClick: () => { getSession(self).queueDialog(handleClose => [ WiggleClusterDialog, { model: self, handleClose, }, ]); }, }, ] : []), { label: 'Edit colors/arrangement...', onClick: () => { getSession(self).queueDialog(handleClose => [ SetColorDialog, { model: self, handleClose, }, ]); }, }, ]; }, }; }) .actions(self => { const { renderSvg: superRenderSvg } = self; return { afterAttach() { ; (async () => { try { const [{ getMultiWiggleSourcesAutorun }, { getQuantitativeStatsAutorun }, { setupTreeDrawingAutorun },] = await Promise.all([ import("../getMultiWiggleSourcesAutorun.js"), import("../getQuantitativeStatsAutorun.js"), import("./treeDrawingAutorun.js"), ]); getQuantitativeStatsAutorun(self); getMultiWiggleSourcesAutorun(self); setupTreeDrawingAutorun(self); } catch (e) { if (isAlive(self)) { console.error(e); getSession(self).notifyError(`${e}`, e); } } })(); }, async renderSvg(opts) { const { renderSvg } = await import("./renderSvg.js"); return renderSvg(self, opts, superRenderSvg); }, }; }) .postProcessSnapshot(snap => { if (!snap) { return snap; } const { layout, showSidebar, clusterTree, treeAreaWidth, showTreeSetting, subtreeFilter, ...rest } = snap; return { ...rest, ...(layout?.length ? { layout } : {}), ...(!showSidebar ? { showSidebar } : {}), ...(clusterTree !== undefined ? { clusterTree } : {}), ...(treeAreaWidth !== 80 ? { treeAreaWidth } : {}), ...(showTreeSetting !== undefined ? { showTreeSetting } : {}), ...(subtreeFilter?.length ? { subtreeFilter } : {}), }; }); } export default stateModelFactory;