Histogram2DPlotlyChartBuilder

Source

HistXYZ.js
function affine(inMin, val, inMax, outMin, outMax) {
return ((val - inMin) / (inMax - inMin)) * (outMax - outMin) + outMin;
}

export default function HistXYZ(chartState, histogram, chartType) {
if (!histogram) return null;

const nBins = histogram.numberOfBins;
const z = [];
const x = [];
const y = [];
for (let i = 0; i < nBins; ++i) {
const row = [];
for (let j = 0; j < nBins; ++j) {
row.push(0);
}
z.push(row);
// x and y make sure the axes reflect the real extent of the binned data.
x.push(
affine(0, i, nBins - 1, histogram.x.extent[0], histogram.x.extent[1])
);
y.push(
affine(0, i, nBins - 1, histogram.y.extent[0], histogram.y.extent[1])
);
}

histogram.bins.forEach((bin) => {
const xIndex = Math.floor(
affine(histogram.x.extent[0], bin.x, histogram.x.extent[1], 0, nBins - 1)
);
const yIndex = Math.floor(
affine(histogram.y.extent[0], bin.y, histogram.y.extent[1], 0, nBins - 1)
);

z[yIndex][xIndex] = bin.count;
});

return {
nonsense: 'unknown value',
forceNewPlot: chartState.forceNewPlot,
traces: [
{
x,
y,
z,
type: chartType,
colorscale: chartState.colormap,
reversescale: chartState.reversescale,
},
],
// redundant axis titles for 2D and 3D plots.
layout: {
margin: {
t: 40,
},
xaxis: {
title: histogram.x.name,
},
yaxis: {
title: histogram.y.name,
},
scene: {
xaxis: {
title: histogram.x.name,
},
yaxis: {
title: histogram.y.name,
},
zaxis: {
title: 'Count',
},
},
},
config: {
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
showLink: false,
modeBarButtonsToRemove: ['sendDataToCloud'],
},
};
}
Scatter.js
export default function Scatter(chartState, histogram, chartType) {
if (!histogram) return null;

const x = [];
const y = [];
const color = [];

histogram.bins.forEach((bin) => {
x.push(bin.x);
y.push(bin.y);
color.push(bin.count);
});

// 'text' shows up in the hover popup.
return {
forceNewPlot: chartState.forceNewPlot,
traces: [
{
x,
y,
text: color.map((count) => `Count: ${count}`),
type: chartType,
mode: 'markers',
marker: {
size: 12,
// size: color,
color,
colorscale: chartState.colormap, // Viridis
showscale: true,
reversescale: chartState.reversescale,
},
},
],
layout: {
hovermode: 'closest',
margin: {
t: 40,
},
xaxis: {
title: histogram.x.name,
},
yaxis: {
title: histogram.y.name,
},
},
config: {
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
showLink: false,
modeBarButtonsToRemove: ['sendDataToCloud'],
},
};
}
ScatterXY.js
export default function ScatterXY(chartState, scatter, chartType) {
if (!scatter) return null;

const x = scatter[0].data;
const y = scatter[1].data;
const trace2 = {
x,
y,
type: chartType,
colorscale: chartState.colormap,
reversescale: chartState.reversescale,
};

return {
forceNewPlot: chartState.forceNewPlot,
traces: [
{
x,
y,
type: 'scatter',
mode: 'markers',
marker: {
color: 'rgba(156, 165, 196, 0.65)',
line: {
color: 'rgba(106, 115, 146, 1.0)',
width: 1,
},
symbol: 'circle',
size: 8,
},
},
].concat(chartType !== 'scatter' ? trace2 : []),
layout: {
hovermode: 'closest',
margin: {
t: 40,
},
xaxis: {
title: scatter[0].name,
},
yaxis: {
title: scatter[1].name,
},
},
config: {
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
showLink: false,
modeBarButtonsToRemove: ['sendDataToCloud'],
},
};
}
index.js
import Monologue from 'monologue.js';
import HistXYZ from './HistXYZ';
import Scatter from './Scatter';
import ScatterXY from './ScatterXY';

import '../../../React/CollapsibleControls/CollapsibleControlFactory/QueryDataModelWidget';

const defaultConfig = {
scrollZoom: true,
displayModeBar: true,
displaylogo: false,
showLink: false,
modeBarButtonsToRemove: ['sendDataToCloud'],
};
const chartFactory = {
Contour: { builder: HistXYZ, type: 'contour', data: 'histogram' },
Heatmap: { builder: HistXYZ, type: 'heatmap', data: 'histogram' },
Scatter: { builder: Scatter, type: 'scatter', data: 'histogram' },
ScatterXY: { builder: ScatterXY, type: 'scatter', data: 'scatter' },
ScatterXYContour: {
builder: ScatterXY,
type: 'histogram2dcontour',
data: 'scatter',
},
Surface3D: { builder: HistXYZ, type: 'surface', data: 'histogram' },
Trend: {
builder: (chartState, data) => {
if (data) data.config = data.config || defaultConfig;
return data;
},
type: 'custom',
data: 'plot',
},
};

const DATA_READY_TOPIC = 'data-ready';

export default class Histogram2DPlotlyChartBuilder {
constructor(queryDataModel) {
this.queryDataModel = queryDataModel;
this.availableChartTypes = Object.keys(chartFactory);
this.chartState = {
chartType: this.availableChartTypes[1],
colormap: 'Portland',
};

// Handle data fetching
if (this.queryDataModel) {
this.queryDataModel.onDataChange((data, envelope) => {
this.histogram = data.histogram2D.data;

this.updateState();
});
this.queryDataModel.onStateChange((event) => {
if (event.name === 'chartType') {
this.chartState.chartType = event.value;
this.chartState.forceNewPlot = true;
this.updateState();
}
});
}

this.controlWidgets = [];
if (this.queryDataModel) {
this.controlWidgets.push({
name: 'QueryDataModelWidget',
queryDataModel,
});
}
}

// ------------------------------------------------------------------------

buildChart() {
if (this.chartState.chartType && (this.histogram || this.scatter)) {
const builder = chartFactory[this.chartState.chartType].builder;
const typeString = chartFactory[this.chartState.chartType].type;
const dataType = chartFactory[this.chartState.chartType].data;
const plotData = builder(this.chartState, this[dataType], typeString);
this.chartState.forceNewPlot = false;
if (plotData) {
this.dataReady(plotData);
}
}
}

// ------------------------------------------------------------------------

updateState(state, forceNewPlot) {
if (state) {
this.chartState = Object.assign(this.chartState, state);
if (forceNewPlot) this.chartState.forceNewPlot = true;
}

this.buildChart();
}

// ------------------------------------------------------------------------

getState() {
return this.chartState;
}

// ------------------------------------------------------------------------
getHistogram() {
return this.histogram;
}

getScatter() {
return this.scatter;
}

setHistogram(histogram) {
// we need a new plot if the axes change, as opposed to just the data.
if (
!this.histogram ||
this.histogram.x.name !== histogram.x.name ||
this.histogram.x.extent !== histogram.x.extent ||
this.histogram.y.name !== histogram.y.name ||
this.histogram.y.extent !== histogram.y.extent
) {
this.chartState.forceNewPlot = true;
}
this.histogram = histogram;
this.buildChart();
}

setScatter(scatter) {
// we need a new plot if the axes change, as opposed to just the data.
if (
!this.scatter ||
this.scatter[0].name !== scatter[0].name ||
this.scatter[0].extent !== scatter[0].extent ||
this.scatter[1].name !== scatter[1].name ||
this.scatter[1].extent !== scatter[1].extent
) {
this.chartState.forceNewPlot = true;
}
this.scatter = scatter;
this.buildChart();
}

setPlot(plot) {
this.chartState.forceNewPlot = true;
this.plot = plot;
this.buildChart();
}

// ------------------------------------------------------------------------

onDataReady(callback) {
return this.on(DATA_READY_TOPIC, callback);
}

// ------------------------------------------------------------------------

dataReady(readyData) {
this.emit(DATA_READY_TOPIC, readyData);
}

// ------------------------------------------------------------------------

// Method meant to be used with the WidgetFactory
getControlWidgets() {
return this.controlWidgets;
}

// ------------------------------------------------------------------------

getControlModels() {
return {
queryDataModel: this.queryDataModel,
};
}

getAvailableChartTypes() {
return this.availableChartTypes;
}

getChartType() {
return this.chartState.chartType;
}

getDataType() {
return chartFactory[this.chartState.chartType].data;
}
}

// Add Observer pattern using Monologue.js
Monologue.mixInto(Histogram2DPlotlyChartBuilder);