import { observer } from "mobx-react"; import { Doc, StrListCast } from "../../../../../fields/Doc"; import * as React from 'react'; import * as d3 from 'd3'; import { IReactionDisposer, action, computed, observable, reaction } from "mobx"; import { LinkManager } from "../../../../util/LinkManager"; import { Cast, DocCast, StrCast} from "../../../../../fields/Types"; import { PinProps, PresBox } from "../../trails"; import { Docs } from "../../../../documents/Documents"; import { List } from "../../../../../fields/List"; import './Chart.scss'; import { ColorPicker, EditableText, Size, Type } from "browndash-components"; import { FaFillDrip } from "react-icons/fa"; import { listSpec } from "../../../../../fields/Schema"; import { undoable } from "../../../../util/UndoManager"; export interface PieChartProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; pairs: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; fieldKey: string; margin: { top: number; right: number; bottom: number; left: number; }; } @observer export class PieChart extends React.Component { private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject = React.createRef(); private _piechartSvg: d3.Selection | undefined; private byCategory: boolean = true; // whether the data is organized by category or by specified number percentages/ratios @observable _currSelected: any | undefined = undefined; // Object of selected slice private curSliceSelected: any = undefined; // d3 data of selected slice private selectedData: any = undefined; // Selection of selected slice private hoverOverData: any = undefined; // Selection of slice being hovered over // filters all data to just display selected data if brushed (created from an incoming link) @computed get _piechartData() { var guids = StrListCast(this.props.layoutDoc.rowGuids); if (this.props.axes.length < 1) return []; if (this.props.axes.length < 2) { var ax0 = this.props.axes[0]; if (/\d/.test(this.props.pairs[0][ax0])){ this.byCategory = false } return this.props.pairs ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)]))) .map(pair => ({ [ax0]: (pair[this.props.axes[0]])})) }; var ax0 = this.props.axes[0]; var ax1 = this.props.axes[1]; if (/\d/.test(this.props.pairs[0][ax0])) { this.byCategory = false; } return this.props.pairs ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)]))) .map(pair => ({ [ax0]: (pair[this.props.axes[0]]), [ax1]: (pair[this.props.axes[1]]) })) } @computed get defaultGraphTitle(){ var ax0 = this.props.axes[0]; var ax1 = (this.props.axes.length>1)? this.props.axes[1] : undefined; if (this.props.axes.length<2 || !/\d/.test(this.props.pairs[0][ax0]) || !ax1){ return ax0 + " Pie Chart"; } else return ax1 + " by " + ax0 + " Pie Chart"; } @computed get incomingLinks() { return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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 }) => { if (dataSet!.length>0) { this.drawChart(dataSet, w, h); } }, { fireImmediately: true } ); }; @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({ // title: 'piechart doc selection' + this._currSelected, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); return anchor; }; @computed get height() { return this.props.height - this.props.margin.top - this.props.margin.bottom; } @computed get width() { return this.props.width - this.props.margin.left - this.props.margin.right; } // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { var validData = dataSet.filter((d: { [x: string]: unknown; }) => { var valid = true; Object.keys(dataSet[0]).map(key => { if (!d[key] || Number.isNaN(d[key])) valid = false; }) return valid; }) var field = dataSet[0]? Object.keys(dataSet[0])[0] : undefined; const data = validData.map((d: { [x: string]: any; }) => { if (!this.byCategory) { return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { var index = -1; var sameAsCurrent: boolean; const selected = svg.selectAll('.slice').filter((d: any) => { index++; var p1 = [0,0]; // center of pie var p3 = [arc.centroid(d)[0]*2, arc.centroid(d)[1]*2]; // outward peak of arc var p2 = [radius*Math.sin(d.startAngle), -radius*Math.cos(d.startAngle)]; // start of arc var p4 = [radius*Math.sin(d.endAngle), -radius*Math.cos(d.endAngle)]; // end of arc // draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge var lineCrossCount = 0; // if for all 4 lines if (Math.min(p1[1], p2[1])<=pointer[1] && pointer[1]<=Math.max(p1[1], p2[1])){ // within y bounds if (pointer[0] <= (pointer[1]-p1[1])*(p2[0]-p1[0])/(p2[1]-p1[1])+p1[0]) lineCrossCount++; } // intercepts x if (Math.min(p2[1], p3[1])<=pointer[1] && pointer[1]<=Math.max(p2[1], p3[1])){ if (pointer[0] <= (pointer[1]-p2[1])*(p3[0]-p2[0])/(p3[1]-p2[1])+p2[0]) lineCrossCount++; } if (Math.min(p3[1], p4[1])<=pointer[1] && pointer[1]<=Math.max(p3[1], p4[1])){ if (pointer[0] <= (pointer[1]-p3[1])*(p4[0]-p3[0])/(p4[1]-p3[1])+p3[0]) lineCrossCount++; } 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) { // inside the slice of it crosses an odd number of edges var showSelected = this.byCategory? pieDataSet[index] : this._piechartData[index]; 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; } else this.hoverOverData = d; return true; } return false; }); if (changeSelectedVariables){ if (sameAsCurrent!) this.curSliceSelected = undefined; else this.curSliceSelected = selected; } } // 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(); var percentField = Object.keys(dataSet[0])[0] var descriptionField = Object.keys(dataSet[0])[1]! var radius = Math.min(width, height-this.props.margin.top-this.props.margin.bottom) /2 // converts data into Objects var data = this.data(dataSet); var pieDataSet = dataSet.filter((d: { [x: string]: unknown; }) => { var valid = true; Object.keys(dataSet[0]).map(key => { if (!d[key] || Number.isNaN(d[key])) valid = false; }) return valid; }); if (this.byCategory){ let uniqueCategories = [...new Set(data)] var pieStringDataSet: { frequency: number }[] = []; for (let i=0; i each[percentField]==data[i]) sliceData[0].frequency = sliceData[0].frequency + 1; } pieDataSet = pieStringDataSet percentField = Object.keys(pieDataSet[0])[0] descriptionField = Object.keys(pieDataSet[0])[1]! data = this.data(pieStringDataSet) } var trackDuplicates : {[key: string]: any} = {}; data.forEach((eachData: any) => !trackDuplicates[eachData]? trackDuplicates[eachData] = 0: null) // initial chart var svg = (this._piechartSvg = d3 .select(this._piechartRef.current) .append("svg") .attr("class", "graph") .attr("width", width + this.props.margin.right + this.props.margin.left) .attr("height", height + this.props.margin.top + this.props.margin.bottom) .append("g")); let g = svg.append("g") .attr("transform", "translate(" + (width/2 + this.props.margin.left) + "," + height/2 + ")"); var pie = d3.pie(); var arc = d3.arc() .innerRadius(0) .outerRadius(radius); // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { const selected = this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet) updateHighlights(); }) const mouseOut = action((e: any) => { this.hoverOverData = undefined; updateHighlights(); }) const updateHighlights = () => { const hoverOverSlice = this.hoverOverData; const selectedData = this.selectedData; svg.selectAll('path').attr("class", function(d: any) { return ((selectedData && d.startAngle==selectedData.startAngle && d.endAngle==selectedData.endAngle) || ((hoverOverSlice && d.startAngle==hoverOverSlice.startAngle && d.endAngle==hoverOverSlice.endAngle)))? 'slice hover' : 'slice'; }) } // drawing the slices var selected = this.selectedData; var arcs = g.selectAll("arc") .data(pie(data)) .enter() .append("g") arcs.append("path") .attr("fill", (d, i)=>{ var possibleDataPoints = pieDataSet.filter((each: { [x: string]: any | { valueOf(): number; }; }) => { try { return Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\ each.split('::')); sliceColors.map(each => {if (each[0]==StrCast(accessByName)) sliceColor = each[1]}); } return sliceColor? StrCast(sliceColor) : d3.schemeSet3[i]? d3.schemeSet3[i]: d3.schemeSet3[i%(d3.schemeSet3.length)] }) .attr("class", selected? function(d) { return (selected && d.startAngle==selected.startAngle && d.endAngle==selected.endAngle)? 'slice hover' : 'slice'; }: function(d) {return 'slice'}) .attr('d', arc) .on('click', onPointClick) .on('mouseover', onHover) .on('mouseout', mouseOut); // adding labels trackDuplicates = {}; data.forEach((eachData: any) => !trackDuplicates[eachData]? trackDuplicates[eachData] = 0: null) arcs.append("text") .attr("transform",function(d){ var centroid = arc.centroid(d as unknown as d3.DefaultArcObject) var heightOffset = (centroid[1]/radius) * Math.abs(centroid[1]) return "translate("+ (centroid[0]+centroid[0]/(radius*.02)) + "," + (centroid[1]+heightOffset) + ")"; }) .attr("text-anchor", "middle") .text(function(d){ var possibleDataPoints = pieDataSet.filter((each: { [x: string]: any | { valueOf(): number; }; }) => { try { return Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { this.curSliceSelected.attr("fill", color); var sliceName = this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1) }); sliceColors.push(StrCast(sliceName + '::' + color)); }; render() { this.componentDidMount(); var titleAccessor: any=''; if (this.props.axes.length==2) titleAccessor = 'pieChart-title-'+this.props.axes[0]+'-'+this.props.axes[1]; else if (this.props.axes.length>0) titleAccessor = 'pieChart-title-'+this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this.props.layoutDoc.pieSliceColors) this.props.layoutDoc.pieSliceColors = new List(); var selected: string; var curSelectedSliceName; if (this._currSelected){ curSelectedSliceName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\ { key!=''? selected += key + ': ' + this._currSelected[key] + ', ': ''; }) selected = selected.substring(0, selected.length-2); selected += ' }'; } else selected = 'none'; var selectedSliceColor; var sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); sliceColors.map(each => {if (each[0]==curSelectedSliceName!) selectedSliceColor = each[1]}); return ( this.props.axes.length >= 1 ? (
this.props.layoutDoc[titleAccessor] = val as string), "Change Graph Title")} color={"black"} size={Size.LARGE} fillWidth />
{selected != 'none' ?
Selected: {selected}     } selectedColor={selectedSliceColor? selectedSliceColor : this.curSliceSelected.attr("fill")} setSelectedColor={undoable (color => this.changeSelectedColor(color), "Change Selected Slice Color")} size={Size.XSMALL} />
: null}
) : {'first use table view to select a column to graph'} ); } }