Skip to content

Commit 6ac5373

Browse files
authored
Merge pull request #440 from DigitalSlideArchive/distribution-plots
Add a distributions plot mode
2 parents 7324b0b + a358582 commit 6ac5373

File tree

2 files changed

+105
-9
lines changed

2 files changed

+105
-9
lines changed

histomicsui/web_client/panels/MetadataPlot.js

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ import '../stylesheets/panels/metadataPlot.styl';
1515

1616
const sessionId = uuidv4();
1717

18+
function mean(arr) {
19+
if (!arr.length) {
20+
return 0;
21+
}
22+
return arr.reduce((a, b) => a + b) / arr.length;
23+
}
24+
25+
function stddev(arr) {
26+
if (arr.length <= 1) {
27+
return 0;
28+
}
29+
const m = mean(arr);
30+
return Math.sqrt(arr.map((x) => Math.pow(x - m, 2)).reduce((a, b) => a + b) / arr.length);
31+
}
32+
1833
var MetadataPlot = Panel.extend({
1934
events: _.extend(Panel.prototype.events, {
2035
'click .g-widget-metadata-plot-settings': function (event) {
@@ -457,7 +472,7 @@ var MetadataPlot = Panel.extend({
457472
colorScale = window.d3.scale.linear().domain(viridis.map((_, i) => i / (viridis.length - 1) * ((plotData.series.c.max - plotData.series.c.min) || 0) + plotData.series.c.min)).range(viridis);
458473
}
459474
const plotlyData = {
460-
x: plotData.data.map((d) => d[plotData.series.x.index]),
475+
x: plotData.series.x ? plotData.data.map((d) => d[plotData.series.x.index]) : 0,
461476
y: plotData.data.map((d) => d[plotData.series.y.index]),
462477
hovertext: plotData.data.map((d) => this._hoverText(d, plotData)),
463478
hoverinfo: 'text',
@@ -518,6 +533,86 @@ var MetadataPlot = Panel.extend({
518533
}];
519534
}
520535
}
536+
if (plotData.format === 'distrib') {
537+
const maxs = plotData.series.s && plotData.series.s.distinct ? plotData.series.s.distinctcount : 1;
538+
const pv = {
539+
x: [],
540+
y: [],
541+
c: [],
542+
r: [],
543+
xe: [],
544+
ye: [],
545+
dd: Array(maxs),
546+
xd: Array.from(Array(maxs), () => []),
547+
yd: Array.from(Array(maxs), () => []),
548+
cd: Array.from(Array(maxs), () => []),
549+
rd: Array.from(Array(maxs), () => [])
550+
};
551+
plotData.data.forEach((d) => {
552+
const s = plotData.series.s && plotData.series.s.distinct ? plotData.series.s.distinct.indexOf(d[plotData.series.s.index]) : 0;
553+
if (pv.dd[s] === undefined) {
554+
pv.dd[s] = d;
555+
}
556+
if (plotData.series.x) {
557+
pv.xd[s].push(d[plotData.series.x.index]);
558+
}
559+
pv.yd[s].push(d[plotData.series.y.index]);
560+
if (plotData.series.c) {
561+
pv.cd[s].push(d[plotData.series.c.index]);
562+
}
563+
if (plotData.series.r) {
564+
pv.rd[s].push(d[plotData.series.r.index]);
565+
}
566+
});
567+
for (let s = 0; s < maxs; s += 1) {
568+
pv.x[s] = mean(pv.xd[s]);
569+
pv.xe[s] = stddev(pv.xd[s]);
570+
pv.y[s] = mean(pv.yd[s]);
571+
pv.ye[s] = stddev(pv.yd[s]);
572+
pv.r[s] = plotData.series.r && plotData.series.r !== 'number' && pv.rd[s].length ? pv.rd[s][0] : mean(pv.rd[s]);
573+
pv.c[s] = plotData.series.c && plotData.series.c !== 'number' && pv.cd[s].length ? pv.cd[s][0] : mean(pv.cd[s]);
574+
}
575+
plotlyData.x = [...Array(maxs).keys()];
576+
if (plotData.series.x) {
577+
plotlyData.error_x = {
578+
type: 'data',
579+
array: pv.xe.map((v) => v / Math.max.apply(Math, pv.xe)),
580+
color: '#0000C0',
581+
// width: 0,
582+
visible: true
583+
};
584+
}
585+
plotlyData.y = pv.y;
586+
plotlyData.mode = 'lines+markers';
587+
plotlyData.error_y = {
588+
type: 'data',
589+
array: pv.ye,
590+
color: '#C00000',
591+
// width: 0,
592+
visible: true
593+
};
594+
plotlyData.hovertext = pv.dd.map((d) => this._hoverText(d, plotData));
595+
plotlyData.marker.symbol = 0;
596+
if (plotData.series.r) {
597+
plotlyData.marker.size = plotData.series.r && (plotData.series.r.type === 'number' || plotData.series.r.distinctcount)
598+
? (
599+
plotData.series.r.type === 'number'
600+
? pv.r.map((r) => (r - plotData.series.r.min) / (plotData.series.r.max - plotData.series.r.min) * 10 + 5)
601+
: pv.dd.map((d) => plotData.series.r.distinct.indexOf(d[plotData.series.r.index]) / plotData.series.r.distinctcount * 10 + 5)
602+
)
603+
: 10;
604+
}
605+
if (plotData.series.c) {
606+
plotlyData.marker.color = plotData.series.c
607+
? (
608+
!plotData.series.c.distinctcount
609+
? pv.c.map((c) => colorScale(c))
610+
: pv.dd.map((d) => colorBrewerPaired12[plotData.series.c.distinct.indexOf(d[plotData.series.c.index])] || '#000000')
611+
)
612+
: '#000000';
613+
}
614+
plotlyData.type = 'scatter';
615+
}
521616
return [plotlyData];
522617
},
523618

@@ -548,7 +643,7 @@ var MetadataPlot = Panel.extend({
548643
this.lastPlotData = plotData;
549644
this.$el.html(metadataPlotTemplate({}));
550645
const elem = this.$el.find('.h-metadata-plot-area');
551-
if (!plotData || (plotData.format !== 'violin' && !plotData.series.x) || !plotData.series.y || plotData.data.length < 2) {
646+
if (!plotData || (plotData.format !== 'violin' && plotData.format !== 'distrib' && !plotData.series.x) || !plotData.series.y || plotData.data.length < 2) {
552647
elem.html('');
553648
return;
554649
}
@@ -560,7 +655,7 @@ var MetadataPlot = Panel.extend({
560655
if (maximized) {
561656
plotOptions.margin.l += 20;
562657
plotOptions.margin.b += 40;
563-
plotOptions.xaxis = {title: {text: plotData.format !== 'violin' ? `${plotData.series.x.title}` : `${plotData.series.s ? plotData.series.s.title : ''}`}};
658+
plotOptions.xaxis = {title: {text: plotData.format !== 'violin' && plotData.format !== 'distrib' ? `${plotData.series.x.title}` : `${plotData.series.s ? plotData.series.s.title : ''}`}};
564659
plotOptions.yaxis = {title: {text: `${plotData.series.y.title}`}};
565660
}
566661
this._plotlyNode = elem;

histomicsui/web_client/templates/dialogs/metadataPlot.pug

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@
1111
-
1212
var plotFormats = [
1313
{key: 'scatter', label: 'Scatter'},
14-
{key: 'violin', label: 'Violin'}]
14+
{key: 'violin', label: 'Violin'},
15+
{key: 'distrib', label: 'Distributions'}]
1516
select#h-plot-format.form-control
1617
each opt in plotFormats
1718
option(value=opt.key, selected=plotConfig.format === opt.key) #{opt.label}
1819
-
1920
var seriesList = [
2021
{key: 'x', label: 'x-axis', number: true, comment: 'Not used in violin plots'},
21-
{key: 'y', label: 'y-axis', number: true},
22+
{key: 'y', label: 'y-axis', number: true, noNone: true},
2223
{key: 'r', label: 'Radius'},
2324
{key: 'c', label: 'Color'},
24-
{key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin plots'},
25-
{key: 'u', label: 'Dimension Reducation', number: true, multiple: true}]
25+
{key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin and distribution plots'},
26+
{key: 'u', label: 'Dimension Reduction', number: true, multiple: true}]
2627
-
2728
var numNumbers = 0;
2829
var numIndex = [];
@@ -33,12 +34,12 @@
3334
if series.comment
3435
p.g-hui-description #{series.comment}
3536
select.h-plot-select.form-control(id='h-plot-series-' + series.key, multiple=series.multiple)
36-
if !series.number
37+
if !series.noNone
3738
option(value='_none_', selected=plotConfig[series.key] === undefined) None
3839
each opt, optidx in plotOptions
3940
if (!series.number || opt.type === 'number') && (!series.string || opt.type === 'string' || opt.distinct)
4041
- var selected = plotConfig[series.key] === opt.key
41-
- if (plotConfig[series.key] === undefined && series.number === true && numIndex[optidx] === seriesidx) { selected = true; }
42+
- if (plotConfig[series.key] === undefined && series.number === true && numIndex[optidx] === seriesidx && (!plotConfig.format || plotConfig.format === 'scatter')) { selected = true; }
4243
- if (series.multiple) { selected = plotConfig[series.key] ? plotConfig[series.key].includes(opt.key) : false; }
4344
option(value=opt.key, selected=selected) #{opt.title}#{opt.count > 1 && opt.distinctcount !== 1 ? ' *' : ''}
4445
.form-group

0 commit comments

Comments
 (0)