diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Histogram.tsx | 151 |
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} |