aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/PieChart.tsx
diff options
context:
space:
mode:
authorNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
committerNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
commit9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch)
tree14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/DataVizBox/components/PieChart.tsx
parent1be63695875c9242fba43d580465e8765cf3991d (diff)
parent202e994515392892676f8f080852db1e32b8dbd3 (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.tsx140
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}