@jbrowse/plugin-wiggle
Version:
JBrowse 2 wiggle adapters, tracks, etc.
539 lines (538 loc) • 21.5 kB
JavaScript
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;