vitessce
Version:
Vitessce app and React component library
276 lines (261 loc) • 7.78 kB
JavaScript
import React from 'react';
import clamp from 'lodash/clamp';
import { VegaPlot, VEGA_THEMES, DATASET_NAME } from '../vega';
import { colorArrayToString } from './utils';
/**
* Gene expression histogram displayed as a bar chart,
* implemented with the VegaPlot component.
* @param {object} props
* @param {object[]} props.data The expression data, an array
* of objects with properties `value`, `gene`, and `set`.
* @param {number} props.domainMax The maximum gene expression value.
* @param {object[]} props.colors An object for each
* cell set, with properties `name` and `color`.
* @param {string} props.theme The name of the current Vitessce theme.
* @param {number} props.width The container width.
* @param {number} props.height The container height.
* @param {number} props.marginRight The size of the margin
* on the right side of the plot, to account for the vega menu button.
* By default, 90.
* @param {number} props.marginBottom The size of the margin
* on the bottom of the plot, to account for long x-axis labels.
* Default is allowing the component to automatically determine the margin.
* @param {string|null} props.featureValueTransformName A name
* for the feature value transformation function.
*/
export default function CellSetExpressionPlot(props) {
const {
domainMax = 100,
colors,
data,
theme,
width,
height,
marginRight = 90,
marginBottom,
featureValueTransformName,
} = props;
// Get the max characters in an axis label for autsizing the bottom margin.
const maxCharactersForLabel = data.reduce((acc, val) => {
// eslint-disable-next-line no-param-reassign
acc = acc === undefined || val.set.length > acc ? val.set.length : acc;
return acc;
}, 0);
// Use a square-root term because the angle of the labels is 45 degrees (see below)
// so the perpendicular distance to the bottom of the labels is proportional to the
// square root of the length of the labels along the imaginary hypotenuse.
// 30 is an estimate of the pixel size of a given character and seems to work well.
const autoMarginBottom = marginBottom
|| 30 + Math.sqrt(maxCharactersForLabel / 2) * 30;
// Manually set the color scale so that Vega-Lite does
// not choose the colors automatically.
const colorScale = {
domain: colors.map(d => d.name),
range: colors.map(d => colorArrayToString(d.color)),
};
const plotWidth = clamp(width - marginRight, 10, Infinity);
const plotHeight = clamp(height - autoMarginBottom, 10, Infinity);
const numBands = colors.length;
const bandWidth = plotWidth / numBands;
const rectColor = (theme === 'dark' ? 'white' : 'black');
const spec = {
$schema: 'https://vega.github.io/schema/vega/v5.json',
description: 'A violin plot showing distributions of expression levels for selected cell sets.',
width: plotWidth,
height: plotHeight,
config: {
...VEGA_THEMES[theme],
axisBand: {
bandPosition: 1,
tickExtra: true,
tickOffset: 0,
},
},
signals: [
{ name: 'bandWidth', value: bandWidth },
{ name: 'width', value: plotWidth },
{ name: 'height', value: plotHeight },
{ name: 'trim', value: true },
],
data: [
{
name: 'density',
source: DATASET_NAME,
transform: [
{
type: 'kde',
field: 'value',
groupby: ['set'],
bandwidth: 0,
extent: [0, domainMax],
},
],
},
{
name: 'stats',
source: DATASET_NAME,
transform: [
{
type: 'aggregate',
groupby: ['set'],
fields: ['value', 'value', 'value'],
ops: ['q1', 'median', 'q3'],
as: ['q1', 'median', 'q3'],
},
],
},
],
scales: [
{
name: 'layout',
type: 'band',
range: 'width',
domain: { data: DATASET_NAME, field: 'set' },
},
{
name: 'yscale',
type: 'linear',
range: 'height',
domain: [0, domainMax],
},
{
name: 'wscale',
type: 'linear',
range: [0, { signal: 'bandWidth' }],
domain: { data: 'density', field: 'density' },
},
{
name: 'wscaleReversed',
type: 'linear',
reverse: true,
range: [0, { signal: 'bandWidth' }],
domain: { data: 'density', field: 'density' },
},
{
name: 'color',
type: 'ordinal',
...colorScale,
},
],
axes: [
{
orient: 'left',
scale: 'yscale',
zindex: 1,
title: featureValueTransformName
? [`${featureValueTransformName}-Transformed`, 'Normalized Expression Values']
: 'Normalized Expression Values',
},
{
orient: 'bottom',
scale: 'layout',
tickCount: 5,
zindex: 1,
title: 'Cell Set',
labelAngle: -45,
labelAlign: 'right',
},
],
marks: [
{
type: 'group',
from: {
facet: {
data: 'density',
name: 'violin',
groupby: 'set',
},
},
encode: {
enter: {
xc: { scale: 'layout', field: 'set', band: 0.5 },
width: { signal: 'bandWidth' },
height: { signal: 'height' },
},
},
data: [
{
name: 'summary',
source: 'stats',
transform: [
{
type: 'filter',
expr: 'datum.set === parent.set',
},
],
},
],
marks: [
{
type: 'area',
orient: 'vertical',
from: { data: 'violin' },
encode: {
enter: {
fill: { scale: 'color', field: { parent: 'set' } },
},
update: {
width: { scale: 'wscale', field: 'density' },
xc: { signal: 'bandWidth / 2' },
y2: { scale: 'yscale', field: 'value' },
y: { scale: 'yscale', value: 0 },
},
},
},
{
type: 'area',
orient: 'vertical',
from: { data: 'violin' },
encode: {
enter: {
fill: { scale: 'color', field: { parent: 'set' } },
},
update: {
width: { scale: 'wscaleReversed', field: 'density' },
xc: { signal: 'bandWidth' },
y2: { scale: 'yscale', field: 'value' },
y: { scale: 'yscale', value: 0 },
},
},
},
{
type: 'rect',
from: { data: 'summary' },
encode: {
enter: {
fill: { value: rectColor },
width: { value: 2 },
},
update: {
y: { scale: 'yscale', field: 'q1' },
y2: { scale: 'yscale', field: 'q3' },
xc: { signal: 'bandWidth / 2' },
},
},
},
{
type: 'rect',
from: { data: 'summary' },
encode: {
enter: {
fill: { value: rectColor },
height: { value: 2 },
width: { value: 8 },
},
update: {
y: { scale: 'yscale', field: 'median' },
xc: { signal: 'bandWidth / 2' },
},
},
},
],
},
],
};
return (
<VegaPlot
data={data}
spec={spec}
/>
);
}