diff options
author | srichman333 <sarah_n_richman@brown.edu> | 2023-08-08 16:15:36 -0400 |
---|---|---|
committer | srichman333 <sarah_n_richman@brown.edu> | 2023-08-08 16:15:36 -0400 |
commit | 318d56e1dff94204b100f5636e1a3288724aaffc (patch) | |
tree | b89705e59e3fc4ed4177ddde7361de0e60fb32c5 | |
parent | 5614478b54b754665faff41c9a09b9bd0535e2f5 (diff) |
comments + cleanups
5 files changed, 78 insertions, 252 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 9a4546900..e71739231 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -26,45 +26,20 @@ export enum DataVizView { @observer export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DataVizBox, fieldKey); - } - // says we have an object and any string - // 2 ways of doing it - // @observable private pairs: { [key: string]: number | string | undefined }[] = []; - // @observable private pairs: { [key: string]: FieldResult }[] = []; + // all data static pairSet = new ObservableMap<string, { [key: string]: string }[]>(); @computed.struct get pairs() { return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); } - private _chartRenderer: LineChart | Histogram | PieChart | undefined; - // // another way would be store a schema that defines the type of data we are expecting from an imported doc - - // method1() { - // this.pairs[0].x = 3; - // } - - // method() { - // // this.pairs[0].x = 3; - // // go through the pairs - // const x = this.pairs[0].x; - // if (typeof x == 'number') { - // let x1 = Number(x); - // // let x1 = NumCast(x); - // } - // } - // could use field result - // [key: string]: FieldResult; - // instead of numeric x,y in there, - - // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops + private _chartRenderer: LineChart | Histogram | PieChart | undefined; + // current displayed chart type @computed get dataVizView(): DataVizView { return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView; } - @action + @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView); const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes))); @@ -75,7 +50,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Object.keys(this.layoutDoc).map(key => { if (key.startsWith('histogram-title') || key.startsWith('lineChart-title') || key.startsWith('pieChart-title')){ this.layoutDoc['_'+key] = data[key]; } }) - const func = () => this._chartRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); @@ -83,7 +57,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } return func() ?? false; }; - getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = !pinProps ? this.rootDoc @@ -93,7 +66,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ }); - anchor.presDataVizView = this.dataVizView; anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; anchor.selected = Field.Copy(this.layoutDoc.selected); @@ -103,7 +75,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { Object.keys(this.layoutDoc).map(key => { if (key.startsWith('histogram-title') || key.startsWith('lineChart-title') || key.startsWith('pieChart-title')){ anchor[key] = this.layoutDoc[key]; } }) - this.addDocument(anchor); return anchor; }; @@ -113,6 +84,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes)); + // toggles for user to decide which chart type to view the data in @computed get selectView() { const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; @@ -125,6 +97,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { case DataVizView.PIECHART: return <PieChart layoutDoc={this.layoutDoc} ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />; } } + @computed get dataUrl() { return Cast(this.dataDoc[this.fieldKey], CsvField); } @@ -141,16 +114,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); } - // handle changing the view using a button - @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) { - e.preventDefault(); - e.stopPropagation(); - this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; - } - render() { if (!this.layoutDoc._dataVizView) this.layoutDoc._dataVizView = this.dataVizView; return !this.pairs?.length ? ( + // displays how to get data into the DataVizBox if its empty <div className="start-message"> To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + t' to bring the data table diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 26c40045c..0b35f2856 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -44,12 +44,12 @@ export class Histogram extends React.Component<HistogramProps> { 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; - private curBarSelected: any = undefined; - private selectedData: any = undefined; - private hoverOverData: any = undefined; - // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + @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 hoverOverData: any = undefined; // Selection of bar being hovered over + // filters all data to just display selected data if brushed (created from an incoming link) @computed get _histogramData() { var guids = StrListCast(this.props.layoutDoc.rowGuids); if (this.props.axes.length < 1) return []; @@ -60,7 +60,6 @@ export class Histogram extends React.Component<HistogramProps> { ?.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.numericalXData = true;} @@ -69,6 +68,7 @@ export class Histogram extends React.Component<HistogramProps> { ?.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; @@ -77,21 +77,13 @@ export class Histogram extends React.Component<HistogramProps> { } else return ax1 + " by " + ax0 + " Histogram"; } + @computed get incomingLinks() { return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom}) // get links where this chart doc is the target of the link + .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 } - @computed get incomingSelected() { - // return selected x and y axes - // otherwise, use the selection of whatever is linked to us - return this.incomingLinks // all links that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs?.filter((pair: { [x: string]: any; }) => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(); - } + @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { if (this.numericalXData){ const data = this.data(this._histogramData); @@ -99,6 +91,7 @@ export class Histogram extends React.Component<HistogramProps> { } return {xMin:0, xMax:0, yMin:0, yMax:0} } + componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } @@ -108,79 +101,14 @@ export class Histogram extends React.Component<HistogramProps> { ({ dataSet, w, h }) => { if (dataSet!.length>0) { this.drawChart(dataSet, w, h); - - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); - this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); } }, { fireImmediately: true } ); - this._disposers.annos = reaction( - () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), - annotations => { - // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way - // could be blue colored to make it look like anchor - // this.drawAnnotations() - // loop through annotations and draw them - annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); - // this.drawAnnotations(annotations.x, annotations.y); - }, - { fireImmediately: true } - ); - this._disposers.highlights = reaction( - () => ({ - selected: this._currSelected, - incomingSelected: this.incomingSelected, - }), - ({ selected, incomingSelected }) => { - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); - incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); - }, - { fireImmediately: true } - ); }; - // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that - - clearAnnotations = () => { - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - element.classList.remove('brushed'); - element.classList.remove('selected'); - } - }; - // gets called whenever the "data_annotations" fields gets updated - drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { - // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements - // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY - // if it exists, then highlight it - // if it doesn't exist, then remove the highlight - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const x = element.getAttribute('data-x'); - const y = element.getAttribute('data-y'); - if (x === dataX.toString() && y === dataY.toString()) { - element.classList.add(selected ? 'selected' : 'brushed'); - } - // TODO: nda - this remove highlight code should go where we remove the links - // } else { - // } - } - }; - - removeAnnotations(dataX: number, dataY: number) { - // loop through and remove any annotations that no longer exist - } - @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({ @@ -188,7 +116,6 @@ export class Histogram extends React.Component<HistogramProps> { title: 'histogram doc selection' + this._currSelected, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); - // anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected]) : undefined; return anchor; }; @@ -200,6 +127,7 @@ export class Histogram extends React.Component<HistogramProps> { 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; @@ -216,21 +144,24 @@ export class Histogram extends React.Component<HistogramProps> { return data; } + // outlines the bar selected / hovered over highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { var sameAsCurrent: boolean; var barCounter = -1; const selected = svg.selectAll('.histogram-bar').filter((d: any) => { - barCounter++; + 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)){ var showSelected = this.numericalYData? this._histogramData.filter((data: { [x: string]: any; }) => 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]){ showSelected = {[xAxisTitle]: (d3.min(d) + " to " + d3.max(d)), frequency: d.length} } else if (!this.numericalYData) showSelected = {[xAxisTitle]: showSelected[xAxisTitle], frequency: d.length} } 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]) @@ -249,12 +180,12 @@ export class Histogram extends React.Component<HistogramProps> { } } + // 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(); var data = this.data(dataSet); - var xAxisTitle = Object.keys(dataSet[0])[0] var yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; let uniqueArr: unknown[] = [...new Set(data)] @@ -263,6 +194,8 @@ export class Histogram extends React.Component<HistogramProps> { if (numBins>this.maxBins) numBins = this.maxBins; var startingPoint = this.numericalXData? this.rangeVals.xMin! : 0; var endingPoint = this.numericalXData? this.rangeVals.xMax! : numBins; + + // converts data into Objects var histDataSet = dataSet.filter((d: { [x: string]: unknown; }) => { var valid = true; Object.keys(dataSet[0]).map(key => { @@ -289,6 +222,7 @@ export class Histogram extends React.Component<HistogramProps> { histDataSet = histStringDataSet } + // initial graph and binning data for histogram var svg = (this._histogramSvg = d3 .select(this._histogramRef.current) .append("svg") @@ -298,7 +232,6 @@ export class Histogram extends React.Component<HistogramProps> { .append("g") .attr("transform", "translate(" + this.props.margin.left + "," + this.props.margin.top + ")")); - var x = d3.scaleLinear() .domain(this.numericalXData? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width ]); @@ -314,6 +247,8 @@ export class Histogram extends React.Component<HistogramProps> { .range([0, Number.isInteger(this.rangeVals.xMin!)? (width-eachRectWidth) : width ]) var xAxis; + // more calculations based on bins + // x-axis if (!this.numericalXData) { // reorganize if the data is strings rather than numbers // uniqueArr.sort() histDataSet.sort() @@ -331,7 +266,7 @@ export class Histogram extends React.Component<HistogramProps> { bins.forEach(d => d.x0 = d.x0!) xAxis = d3.axisBottom(x) .ticks(bins.length-1) - .tickFormat( i => uniqueArr[i]) + .tickFormat( i => uniqueArr[i.valueOf()] as string) .tickPadding(10) x.range([0, width-eachRectWidth]) x.domain([0, bins.length-1]) @@ -343,10 +278,10 @@ export class Histogram extends React.Component<HistogramProps> { xAxis = d3.axisBottom(x) .ticks(numBins-1) } + // y-axis const maxFrequency = this.numericalYData? d3.max(histDataSet, function(d: any) { return Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''))}) : d3.max(bins, function(d) { return d.length; }) - var y = d3.scaleLinear() .range([height, 0]); y.domain([0, +maxFrequency!]); @@ -363,7 +298,8 @@ export class Histogram extends React.Component<HistogramProps> { svg.append("g") .attr("transform", "translate(" + translateXAxis + ", " + height + ")") .call(xAxis) - + + // click/hover const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); const onHover = action((e: any) => { const selected = this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet) @@ -378,12 +314,11 @@ export class Histogram extends React.Component<HistogramProps> { const selectedData = this.selectedData; svg.selectAll('rect').attr("class", function(d: any) { return ((hoverOverBar && hoverOverBar[0]==d[0]) || selectedData && selectedData[0]==d[0])? 'histogram-bar hover' : 'histogram-bar'; }) } - svg.on('click', onPointClick) .on('mouseover', onHover) .on('mouseout', mouseOut) - var selected = this.selectedData; + // axis titles svg.append("text") .attr("transform", "translate(" + (width/2) + " ," + (height+40) + ")") .style("text-anchor", "middle") @@ -395,6 +330,9 @@ export class Histogram extends React.Component<HistogramProps> { .style("text-anchor", "middle") .text(yAxisTitle); d3.format('.0f') + + // draw bars + var selected = this.selectedData; svg.selectAll("rect") .data(bins) .enter() diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 0fd4f6b54..d4570dee2 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -40,12 +40,12 @@ export class PieChart extends React.Component<PieChartProps> { private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; private byCategory: boolean = true; // whether the data is organized by category or by specified number percentages/ratios - @observable _currSelected: any | undefined = undefined; - private curSliceSelected: any = undefined; - private selectedData: any = undefined; - private hoverOverData: any = undefined; - // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + @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 []; @@ -56,7 +56,6 @@ export class PieChart extends React.Component<PieChartProps> { ?.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; } @@ -64,6 +63,7 @@ export class PieChart extends React.Component<PieChartProps> { ?.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; @@ -72,28 +72,13 @@ export class PieChart extends React.Component<PieChartProps> { } else return ax1 + " by " + ax0 + " Pie Chart"; } + @computed get incomingLinks() { return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom}) // get links where this chart doc is the target of the link + .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 } - @computed get incomingSelected() { - // return selected x and y axes - // otherwise, use the selection of whatever is linked to us - return this.incomingLinks // all links that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs?.filter((pair: { [x: string]: any; }) => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(); - } - @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { - if (!this.byCategory){ - const data = this.data(this._piechartData); - return {xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin:0, yMax:0} - } - return {xMin:0, xMax:0, yMin:0, yMax:0} - } + componentWillUnmount() { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } @@ -103,79 +88,14 @@ export class PieChart extends React.Component<PieChartProps> { ({ dataSet, w, h }) => { if (dataSet!.length>0) { this.drawChart(dataSet, w, h); - - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); - this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); } }, { fireImmediately: true } ); - this._disposers.annos = reaction( - () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), - annotations => { - // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way - // could be blue colored to make it look like anchor - // this.drawAnnotations() - // loop through annotations and draw them - annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); - // this.drawAnnotations(annotations.x, annotations.y); - }, - { fireImmediately: true } - ); - this._disposers.highlights = reaction( - () => ({ - selected: this._currSelected, - incomingSelected: this.incomingSelected, - }), - ({ selected, incomingSelected }) => { - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); - incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); - }, - { fireImmediately: true } - ); - }; - - // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that - - clearAnnotations = () => { - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - element.classList.remove('brushed'); - element.classList.remove('selected'); - } - }; - // gets called whenever the "data_annotations" fields gets updated - drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { - // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements - // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY - // if it exists, then highlight it - // if it doesn't exist, then remove the highlight - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const x = element.getAttribute('data-x'); - const y = element.getAttribute('data-y'); - if (x === dataX.toString() && y === dataY.toString()) { - element.classList.add(selected ? 'selected' : 'brushed'); - } - // TODO: nda - this remove highlight code should go where we remove the links - // } else { - // } - } }; - removeAnnotations(dataX: number, dataY: number) { - // loop through and remove any annotations that no longer exist - } - @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({ @@ -183,7 +103,6 @@ export class PieChart extends React.Component<PieChartProps> { title: 'piechart doc selection' + this._currSelected, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); - // anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected]) : undefined; return anchor; }; @@ -195,15 +114,7 @@ export class PieChart extends React.Component<PieChartProps> { return this.props.width - this.props.margin.left - this.props.margin.right; } - // TODO: nda - use this everyewhere we update currSelected? - @action - setCurrSelected(x?: number, y?: number) { - // TODO: nda - get rid of svg element in the list? - this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); - this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined)); - } - + // 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; @@ -220,15 +131,16 @@ export class PieChart extends React.Component<PieChartProps> { return data; } + // outlines the slice selected / hovered over highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => { var index = -1; var sameAsCurrent: boolean; const selected = svg.selectAll('.slice').filter((d: any) => { index++; - var p1 = [0,0]; - var p3 = [arc.centroid(d)[0]*2, arc.centroid(d)[1]*2]; - var p2 = [radius*Math.sin(d.startAngle), -radius*Math.cos(d.startAngle)]; - var p4 = [radius*Math.sin(d.endAngle), -radius*Math.cos(d.endAngle)]; + 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; @@ -241,10 +153,10 @@ export class PieChart extends React.Component<PieChartProps> { 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) { + 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]]) @@ -264,6 +176,7 @@ export class PieChart extends React.Component<PieChartProps> { } } + // 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(); @@ -271,18 +184,8 @@ export class PieChart extends React.Component<PieChartProps> { 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 - 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 + ")"); + // converts data into Objects var data = this.data(dataSet); var pieDataSet = dataSet.filter((d: { [x: string]: unknown; }) => { var valid = true; @@ -293,12 +196,12 @@ export class PieChart extends React.Component<PieChartProps> { }); if (this.byCategory){ let uniqueCategories = [...new Set(data)] - var pieStringDataSet: { frequency: number, [percentField]: string }[] = []; + var pieStringDataSet: { frequency: number }[] = []; for (let i=0; i<uniqueCategories.length; i++){ pieStringDataSet.push({frequency: 0, [percentField]: uniqueCategories[i]}) } for (let i=0; i<data.length; i++){ - let sliceData = pieStringDataSet.filter(each => each[percentField]==data[i]) + let sliceData = pieStringDataSet.filter((each: any) => each[percentField]==data[i]) sliceData[0].frequency = sliceData[0].frequency + 1; } pieDataSet = pieStringDataSet @@ -309,11 +212,23 @@ export class PieChart extends React.Component<PieChartProps> { 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) @@ -331,6 +246,7 @@ export class PieChart extends React.Component<PieChartProps> { || ((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)) @@ -359,11 +275,12 @@ export class PieChart extends React.Component<PieChartProps> { function(d) { return (selected && d.startAngle==selected.startAngle && d.endAngle==selected.endAngle)? 'slice hover' : 'slice'; }: function(d) {return 'slice'}) - .attr("d", arc) + .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") diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 38dd62d8d..277ee83ec 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -32,6 +32,7 @@ interface TableBoxProps { @observer export class TableBox extends React.Component<TableBoxProps> { + // filters all data to just display selected data if brushed (created from an incoming link) @computed get _tableData() { if (this.incomingLinks.length! <= 0) return this.props.pairs; var guids = StrListCast(this.props.layoutDoc.rowGuids); @@ -52,6 +53,7 @@ export class TableBox extends React.Component<TableBoxProps> { return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header!='' && header!=undefined) : []; } + // updates the 'selected' field to no longer include rows that aren't in the table filterSelectedRowsDown() { if (!this.props.layoutDoc.selected) this.props.layoutDoc.selected = new List<string>(); const selected = Cast(this.props.layoutDoc.selected, listSpec("string"), null); @@ -88,7 +90,7 @@ export class TableBox extends React.Component<TableBoxProps> { setupMoveUpEvents( {}, e, - e => { + e => { // dragging off a column to create a brushed DataVizBox const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; const targetCreator = (annotationOn: Doc | undefined) => { const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); @@ -144,6 +146,7 @@ export class TableBox extends React.Component<TableBoxProps> { if (containsData){ return ( <tr key={i} className="table-row" onClick={action(e => { + // selecting a row const selected = Cast(this.props.layoutDoc.selected, listSpec("string"), null); if (selected.includes(guid)) selected.splice(selected.indexOf(guid), 1); else { @@ -153,6 +156,7 @@ export class TableBox extends React.Component<TableBoxProps> { background: StrListCast(this.props.layoutDoc.selected).includes(guid) ? 'lightgrey' : '' }}> {this.columns.map(col => ( (this.props.layoutDoc.selected)? + // each cell <td key={this.columns.indexOf(col)} style={{border: '1px solid black'}}> {p[col]} </td> @@ -168,6 +172,7 @@ export class TableBox extends React.Component<TableBoxProps> { ); } else return ( + // when it is a brushed table and the incoming table doesn't have any rows selected <div className='chart-container'> Selected rows of data from the incoming DataVizBox to display. </div> diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index e1ff6f8eb..10bfb0c64 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never }; export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { - console.log('x axis creator being called'); g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); }; |