aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/Histogram.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx151
1 files changed, 104 insertions, 47 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 79b3e9541..14d7e9bf6 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components';
import * as d3 from 'd3';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaFillDrip } from 'react-icons/fa';
@@ -37,14 +37,14 @@ export interface HistogramProps {
@observer
export class Histogram extends ObservableReactComponent<HistogramProps> {
private _disposers: { [key: string]: IReactionDisposer } = {};
- private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _histogramRef: HTMLDivElement | null = null;
private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis
private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency
private maxBins = 15; // maximum number of bins that is readable on a normal sized doc
@observable _currSelected: any | undefined = undefined; // Object of selected bar
- private curBarSelected: any = undefined; // histogram bin of selected bar
- private selectedData: any = undefined; // Selection of selected bar
+ private curBarSelected: any = undefined; // histogram bin of selected bar for when just one bar is selected
+ private selectedData: any[] = []; // array of selected bars
private hoverOverData: any = undefined; // Selection of bar being hovered over
constructor(props: any) {
@@ -103,14 +103,24 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
}
componentDidMount() {
- this._disposers.chartData = reaction(
- () => ({ dataSet: this._histogramData, w: this.width, h: this.height }),
- ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h),
- { fireImmediately: true }
- );
+ // restore selected bars
+ const svg = this._histogramSvg;
+ if (svg) {
+ const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_histogram_selectedData);
+ svg.selectAll('rect').attr('class', (d: any) => {
+ let selected = false;
+ selectedDataBars.forEach(eachSelectedData => {
+ if (d[0] === eachSelectedData) selected = true;
+ });
+ if (selected) {
+ this.selectedData.push(d);
+ return 'histogram-bar hover';
+ }
+ return 'histogram-bar';
+ });
+ }
}
- restoreView = () => {};
// create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
getAnchor = (pinProps?: PinProps) => {
const anchor = Docs.Create.ConfigDocument({
@@ -130,7 +140,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
// cleans data by converting numerical data to numbers and taking out empty cells
data = (dataSet: any) => {
- const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
+ const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any)));
const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
return !field
? []
@@ -143,14 +153,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
// outlines the bar selected / hovered over
highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => {
- let sameAsCurrent: boolean;
let barCounter = -1;
const selected = svg.selectAll('.histogram-bar').filter((d: any) => {
barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
- if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) {
+ if (d.length && barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) {
let showSelected = this.numericalYData
- ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0]
- : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/$/g, '').replace(/%/g, '').replace(/</g, '') === d[0])[0];
+ ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]
+ : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0];
if (this.numericalXData) {
// calculating frequency
if (d[0] && d[1] && d[0] !== d[1]) {
@@ -159,24 +168,59 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
}
if (changeSelectedVariables) {
// for when a bar is selected - not just hovered over
- sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] === this._currSelected![xAxisTitle] && showSelected[yAxisTitle] === this._currSelected![yAxisTitle] : false;
- this._currSelected = sameAsCurrent ? undefined : showSelected;
- this.selectedData = sameAsCurrent ? undefined : d;
+ let sameAsAny = false;
+ const selectedDataBars = Cast(this._props.layoutDoc.dataViz_histogram_selectedData, listSpec('number'), null);
+ this.selectedData.forEach(eachData => {
+ if (!sameAsAny) {
+ let match = true;
+ Object.keys(d).forEach(key => {
+ if (d[key] !== eachData[key]) match = false;
+ });
+ if (match) {
+ sameAsAny = true;
+ const index = this.selectedData.indexOf(eachData);
+ this.selectedData.splice(index, 1);
+ selectedDataBars.splice(index, 1);
+ this._currSelected = undefined;
+ }
+ }
+ });
+ if (!sameAsAny) {
+ this.selectedData.push(d);
+ selectedDataBars.push(d[0]);
+ this._currSelected = this.selectedData.length > 1 ? undefined : showSelected;
+ }
+
+ // for filtering child dataviz docs
+ if (this._props.layoutDoc.dataViz_filterSelection) {
+ const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null);
+ this._tableDataIds.forEach(rowID => {
+ let match = false;
+ for (let i = 0; i < d.length; i++) {
+ console.log('Compare: ' + this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + ' = ' + d[i]);
+ if (this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[i]) match = true;
+ }
+ if (match && !selectedRows?.includes(rowID))
+ selectedRows?.push(rowID); // adding to filtered rows
+ else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows
+ });
+ }
} else this.hoverOverData = d;
return true;
}
return false;
});
if (changeSelectedVariables) {
- if (sameAsCurrent!) this.curBarSelected = undefined;
- else this.curBarSelected = selected;
+ if (this._currSelected) this.curBarSelected = selected;
+ else this.curBarSelected = undefined;
}
};
// draws the histogram
drawChart = (dataSet: any, width: number, height: number) => {
- d3.select(this._histogramRef.current).select('svg').remove();
- d3.select(this._histogramRef.current).select('.tooltip').remove();
+ if (dataSet?.length <= 0) return;
+ d3.select(this._histogramRef).select('svg').remove();
+ d3.select(this._histogramRef).select('.tooltip').remove();
const data = this.data(dataSet);
const xAxisTitle = Object.keys(dataSet[0])[0];
@@ -189,7 +233,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins;
// converts data into Objects
- let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key])));
+ let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any)));
if (!this.numericalXData) {
const histStringDataSet: { [x: string]: unknown }[] = [];
if (this.numericalYData) {
@@ -201,8 +245,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] });
}
for (let i = 0; i < data.length; i++) {
- const barData = histStringDataSet.filter(each => each[xAxisTitle] === data[i]);
- histStringDataSet.filter(each => each[xAxisTitle] === data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
+ const barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]);
+ histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
}
}
histDataSet = histStringDataSet;
@@ -210,7 +254,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
// initial graph and binning data for histogram
const svg = (this._histogramSvg = d3
- .select(this._histogramRef.current)
+ .select(this._histogramRef)
.append('svg')
.attr('class', 'graph')
.attr('width', width + this._props.margin.right + this._props.margin.left)
@@ -242,7 +286,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
for (let i = 0; i < data.length; i++) {
let index = 0;
for (let j = 0; j < uniqueArr.length; j++) {
- if (uniqueArr[j] === data[i]) {
+ if (uniqueArr[j] == data[i]) {
index = j;
}
}
@@ -315,8 +359,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
updateHighlights();
});
const updateHighlights = () => {
- const { hoverOverData: hoverOverBar, selectedData } = this;
- svg.selectAll('rect').attr('class', (d: any) => ((hoverOverBar && hoverOverBar[0] === d[0]) || (selectedData && selectedData[0] === d[0]) ? 'histogram-bar hover' : 'histogram-bar'));
+ const hoverOverBar = this.hoverOverData;
+ const { selectedData } = this;
+ svg.selectAll('rect').attr('class', (d: any) => {
+ let selected = false;
+ selectedData.forEach(eachSelectedData => {
+ if (d[0] === eachSelectedData[0]) selected = true;
+ });
+ return (hoverOverBar && hoverOverBar[0] == d[0]) || selected ? 'histogram-bar hover' : 'histogram-bar';
+ });
};
svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut);
@@ -343,9 +394,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
'transform',
this.numericalYData
? d => {
- const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]);
- const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
- return 'translate(' + x(d.x0!) + ',' + y(length) + ')';
+ const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]);
+ const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
+ return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')';
}
: d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')'
)
@@ -353,20 +404,20 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
'height',
this.numericalYData
? d => {
- const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] === d[0]);
- const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
- return height - y(length);
+ const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]);
+ const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
+ return height - y(Number(length));
}
: d => height - y(d.length)
)
.attr('width', eachRectWidth)
- .attr('class', selected ? d => (selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar')
+ .attr('class', selected ? d => (selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar')
.attr('fill', d => {
let barColor;
const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
barColors.forEach(each => {
// eslint-disable-next-line prefer-destructuring
- if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1];
+ if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
else {
const range = StrCast(each[0]).split(' to ');
// eslint-disable-next-line prefer-destructuring
@@ -394,15 +445,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1));
};
- updateBarColors = () => {
+ // reloads the bar colors and selected bars
+ updateSavedUI = () => {
const svg = this._histogramSvg;
- if (svg)
+ if (svg) {
+ // bar color
svg.selectAll('rect').attr('fill', (d: any) => {
let barColor;
const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
barColors.forEach(each => {
// eslint-disable-next-line prefer-destructuring
- if (d[0] && d[0].toString() && each[0] === d[0].toString()) barColor = each[1];
+ if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
else {
const range = StrCast(each[0]).split(' to ');
// eslint-disable-next-line prefer-destructuring
@@ -411,10 +464,11 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
});
return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor);
});
+ }
};
render() {
- this.updateBarColors();
+ this.updateSavedUI();
this._histogramData;
let curSelectedBarName = '';
let titleAccessor: any = 'dataViz_histogram_title';
@@ -423,6 +477,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2';
if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>();
+ if (!this._props.layoutDoc.dataViz_histogram_selectedData) this._props.layoutDoc.dataViz_histogram_selectedData = new List<string>();
let selected = 'none';
if (this._currSelected) {
curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''));
@@ -483,7 +538,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
size={Size.XSMALL}
/>
</div>
- <div ref={this._histogramRef} />
+ <div
+ ref={r => {
+ this._histogramRef = r;
+ r && this.drawChart(this._histogramData, this.width, this.height);
+ }}
+ />
{selected !== 'none' ? (
<div className="selected-data">
Selected: {selected}
@@ -503,11 +563,8 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
size={Size.XSMALL}
color="black"
type={Type.SEC}
- tooltip="Revert to the default bar color"
- onClick={undoable(
- action(() => this.eraseSelectedColor()),
- 'Change Selected Bar Color'
- )}
+ tooltip="Revert to the default bar color" //
+ onClick={undoable(this.eraseSelectedColor, 'Change Selected Bar Color')}
/>
</div>
) : null}