diff options
author | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
---|---|---|
committer | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
commit | 9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch) | |
tree | 14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/DataVizBox/components/PieChart.tsx | |
parent | 1be63695875c9242fba43d580465e8765cf3991d (diff) | |
parent | 202e994515392892676f8f080852db1e32b8dbd3 (diff) |
Merge branch 'master' into nathan-starter
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/PieChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/PieChart.tsx | 140 |
1 files changed, 102 insertions, 38 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index ef6d1d412..19ea8e4fa 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -1,7 +1,7 @@ import { Checkbox } from '@mui/material'; import { ColorPicker, EditableText, 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'; @@ -36,10 +36,10 @@ export interface PieChartProps { @observer export class PieChart extends ObservableReactComponent<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; - private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _piechartRef: HTMLDivElement | null = null; private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - private curSliceSelected: any = undefined; // d3 data of selected slice - private selectedData: any = undefined; // Selection of selected slice + private curSliceSelected: any = undefined; // d3 data of selected slice for when just one slice is selected + private selectedData: any[] = []; // array of selected slices private hoverOverData: any = undefined; // Selection of slice being hovered over @observable _currSelected: any | undefined = undefined; // Object of selected slice @@ -84,24 +84,30 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Links(this._props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link - // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { - this._disposers.chartData = reaction( - () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), - { fireImmediately: true } - ); + // restore selected slices + const svg = this._piechartSvg; + if (svg && this._pieChartData[0]) { + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_pie_selectedData); + svg.selectAll('path').attr('class', (d: any) => { + let selected = false; + selectedDataBars.forEach(eachSelectedData => { + if (d.data === eachSelectedData) selected = true; + }); + if (selected) { + this.selectedData.push(d); + return 'slice hover'; + } + return 'slice'; + }); + } } - @action - restoreView = (/* data: Doc */) => {}; // 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({ @@ -122,7 +128,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // 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 ? undefined @@ -136,7 +142,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // outlines the slice selected / hovered over highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => { let index = -1; - let sameAsCurrent: boolean; const selected = svg.selectAll('.slice').filter((d: any) => { index++; const p1 = [0, 0]; // center of pie @@ -160,31 +165,63 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) { if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++; } - if (lineCrossCount % 2 !== 0) { + if (lineCrossCount % 2 !== 0 || d.startAngle % (2 * Math.PI) === d.endAngle % (2 * Math.PI)) { // inside the slice of it crosses an odd number of edges const showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; + let key = 'data'; // key that represents slice + // eslint-disable-next-line prefer-destructuring + if (Object.keys(showSelected)[0] === 'frequency') key = Object.keys(showSelected)[1]; if (changeSelectedVariables) { - // for when a bar is selected - not just hovered over - sameAsCurrent = this._currSelected - ? showSelected[Object.keys(showSelected)[0]] === this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] === this._currSelected![Object.keys(showSelected)[1]] - : this._currSelected === showSelected; - this._currSelected = sameAsCurrent ? undefined : showSelected; - this.selectedData = sameAsCurrent ? undefined : d; + let sameAsAny = false; + const selectedDataSlices = Cast(this._props.layoutDoc.dataViz_pie_selectedData, listSpec('number'), null); + this.selectedData.forEach(eachData => { + if (!sameAsAny) { + let match = true; + Object.keys(d).forEach(objKey => { + if (d[objKey] !== eachData[objKey]) match = false; + }); + if (match) { + sameAsAny = true; + const selIndex = this.selectedData.indexOf(eachData); + this.selectedData.splice(selIndex, 1); + selectedDataSlices.splice(selIndex, 1); + this._currSelected = undefined; + } + } + }); + if (!sameAsAny) { + this.selectedData.push(d); + selectedDataSlices.push(d[key]); + 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; + if (this._props.records[rowID][this._props.axes[0]] == d[key]) 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.curSliceSelected = undefined; - else this.curSliceSelected = selected; + if (this._currSelected) this.curSliceSelected = selected; + else this.curSliceSelected = undefined; } }; // draws the pie chart drawChart = (dataSet: any, width: number, height: number) => { - d3.select(this._piechartRef.current).select('svg').remove(); - d3.select(this._piechartRef.current).select('.tooltip').remove(); + if (!dataSet?.length) return; + d3.select(this._piechartRef).select('svg').remove(); + d3.select(this._piechartRef).select('.tooltip').remove(); let percentField = Object.keys(dataSet[0])[0]; let descriptionField = Object.keys(dataSet[0])[1]!; @@ -192,7 +229,8 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // converts data into Objects let data = this.data(dataSet); - let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key]))); + let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key])); + if (!pieDataSet.length) return; if (this.byCategory) { const uniqueCategories = [...new Set(data)]; const pieStringDataSet: { frequency: number }[] = []; @@ -201,10 +239,11 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } for (let i = 0; i < data.length; i++) { // eslint-disable-next-line no-loop-func - const sliceData = pieStringDataSet.filter((each: any) => each[percentField] === data[i]); + const sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]); sliceData[0].frequency += 1; } pieDataSet = pieStringDataSet; + if (!pieDataSet.length) return; [percentField, descriptionField] = Object.keys(pieDataSet[0]); data = this.data(pieStringDataSet); } @@ -215,7 +254,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { // initial chart const svg = (this._piechartSvg = d3 - .select(this._piechartRef.current) + .select(this._piechartRef) .append('svg') .attr('class', 'graph') .attr('width', width + this._props.margin.right + this._props.margin.left) @@ -228,10 +267,15 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { const updateHighlights = () => { const hoverOverSlice = this.hoverOverData; const { selectedData } = this; - svg.selectAll('path').attr('class', (d: any) => - (selectedData && d.startAngle === selectedData.startAngle && d.endAngle === selectedData.endAngle) || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice' - ); + svg.selectAll('path').attr('class', (d: any) => { + let selected = false; + selectedData.forEach((eachSelectedData: any) => { + if (d.startAngle === eachSelectedData.startAngle) selected = true; + }); + return selected || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice'; + }); }; + // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { @@ -242,7 +286,6 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { this.hoverOverData = undefined; updateHighlights(); }); - // drawing the slices const selected = this.selectedData; const arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); @@ -259,8 +302,15 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { possibleDataPointVals.push(dataPointVal); }); const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + + // to make sure all important slice information is on 'd' object + let addKey: any = false; + if (pieDataSet.length && Object.keys(pieDataSet[0])[0] === 'frequency') { + // eslint-disable-next-line prefer-destructuring + addKey = Object.keys(pieDataSet[0])[1]; + } arcs.append('path') - .attr('fill', (d, i) => { + .attr('fill', (d: any, i) => { let dataPoint; const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); if (possibleDataPoints.length === 1) [dataPoint] = possibleDataPoints; @@ -270,6 +320,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } let sliceColor; if (dataPoint) { + if (addKey) d[addKey] = dataPoint[addKey]; // adding all slice information to d const sliceTitle = dataPoint[this._props.axes[0]]; const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle; sliceColors.forEach(each => { @@ -279,7 +330,13 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) - .attr('class', selected ? d => (selected && d.startAngle === selected.startAngle && d.endAngle === selected.endAngle ? 'slice hover' : 'slice') : () => 'slice') + .attr('class', d => { + let selectThisData = false; + selected.forEach((eachSelectedData: any) => { + if (d.startAngle === eachSelectedData.startAngle) selectThisData = true; + }); + return selectThisData ? 'slice hover' : 'slice'; + }) // @ts-ignore .attr('d', arc) .on('click', onPointClick) @@ -299,6 +356,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { const heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')'; }) + .attr('pointer-events', 'none') .attr('text-anchor', 'middle') .text(d => { let dataPoint; @@ -308,7 +366,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])]; trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; } - return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; + return dataPoint ? (descriptionField ? dataPoint[descriptionField] : dataPoint[percentField]!) : ''; }); }; @@ -335,6 +393,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); + if (!this._props.layoutDoc.dataViz_pie_selectedData) this._props.layoutDoc.dataViz_pie_selectedData = new List<string>(); let selected: string; let curSelectedSliceName = ''; if (this._currSelected) { @@ -388,7 +447,12 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { Organize data as histogram </div> ) : null} - <div ref={this._piechartRef} /> + <div + ref={r => { + this._piechartRef = r; + this.drawChart(this._pieChartData, this.width, this.height); + }} + /> {selected !== 'none' ? ( <div className="selected-data"> Selected: {selected} |