diff options
author | bobzel <zzzman@gmail.com> | 2024-05-14 23:15:24 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-05-14 23:15:24 -0400 |
commit | 3534aaf88a3c30a474b3b5a5b7f04adfe6f15fac (patch) | |
tree | 47fb7a8671b209bd4d76e0f755a5b035c6936607 /src/client/views/nodes/DataVizBox/components/LineChart.tsx | |
parent | 87bca251d87b5a95da06b2212400ce9427152193 (diff) | |
parent | 5cb7ad90e120123ca572e8ef5b1aa6ca41581134 (diff) |
Merge branch 'restoringEslint' into sarah-ai-visualization
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 293 |
1 files changed, 137 insertions, 156 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index e79f4cde5..80edf2c36 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -3,23 +3,20 @@ import * as d3 from 'd3'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; -import { DocumentManager } from '../../../../util/DocumentManager'; import { undoable } from '../../../../util/UndoManager'; +import {} from '../../../DocComponent'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PresBox } from '../../trails'; +import { PinDocView, PinProps } from '../../../PinFuncs'; +import { DocumentView } from '../../DocumentView'; import { DataVizBox } from '../DataVizBox'; -import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; +import { DataPoint, createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; -export interface DataPoint { - x: number; - y: number; -} export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } @@ -47,8 +44,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - @observable _currSelected: any | undefined = undefined; - private selectedData: any[] = []; // array of selected data points + @observable _currSelected: DataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates constructor(props: any) { @@ -64,7 +60,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } @computed get _lineChartData() { - var guids = StrListCast(this._props.layoutDoc.dataViz_rowIds); if (this._props.axes.length <= 1) return []; return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @@ -77,7 +72,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @computed get incomingHighlited() { // return selected x and y axes // otherwise, use the selection of whatever is linked to us - const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows); return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @@ -100,21 +95,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { ); // coloring the selected point - const elements = document.querySelectorAll('.datapoint'); - for (let i = 0; i < elements.length; i++) { - const x = Number(elements[i].getAttribute('data-x')); - const y = Number(elements[i].getAttribute('data-y')); - const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData) - let selected = false; - selectedDataBars.forEach(eachSelectedData => { - // parse each selected point into x,y - let xy = eachSelectedData.split(','); - if (Number(xy[0])===x && Number(xy[1])===y) selected = true; - }) - if (selected) { - this.drawAnnotations(x, y, false); - } - } + this.colorSelectedPt(); } // 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 @@ -141,20 +122,35 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } }; - @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: 'line doc selection' + this._currSelected?.x, + title: 'line doc selection' + (this._currSelected?.x ?? ''), }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; + private colorSelectedPt() { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const x = Number(elements[i].getAttribute('data-x')); + const y = Number(elements[i].getAttribute('data-y')); + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData); + let selected = false; + selectedDataBars.forEach(eachSelectedData => { + // parse each selected point into x,y + const xy = eachSelectedData.split(','); + if (Number(xy[0]) === x && Number(xy[1]) === y) selected = true; + }); + if (selected) { + this.drawAnnotations(x, y, false); + } + } + } + @computed get height() { return this._props.height - this._props.margin.top - this._props.margin.bottom; } @@ -164,11 +160,12 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get defaultGraphTitle() { - var ax0 = this._props.axes[0]; - var ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; + const ax0 = this._props.axes[0]; + const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined; if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; - } else return ax1 + ' by ' + ax0 + ' Line Chart'; + } + return ax1 + ' by ' + ax0 + ' Line Chart'; } setupTooltip() { @@ -186,35 +183,37 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @action setCurrSelected(d: DataPoint) { - let sameAsAny = false; + let ptWasSelected = false; const selectedDatapoints = Cast(this._props.layoutDoc.dataViz_lineChart_selectedData, listSpec('string'), null); - this.selectedData.forEach(eachData => { - if (!sameAsAny){ - if (eachData.x==d.x && eachData.y==d.y) { - sameAsAny = true; - let index = this.selectedData.indexOf(eachData) - this.selectedData.splice(index, 1); + selectedDatapoints?.forEach(eachData => { + if (!ptWasSelected) { + const [dx, dy] = eachData.split(','); + if (Number(dx) === d.x && Number(dy) === d.y) { + ptWasSelected = true; + const index = selectedDatapoints.indexOf(eachData); selectedDatapoints.splice(index, 1); this._currSelected = undefined; } } - }) - if(!sameAsAny) { - this.selectedData.push(d); - selectedDatapoints.push(d.x + "," + d.y); - this._currSelected = this.selectedData.length>1? undefined : d; + }); + if (!ptWasSelected) { + selectedDatapoints.push(d.x + ',' + d.y); + this._currSelected = selectedDatapoints.length > 1 ? undefined : d; } // for filtering child dataviz docs - if (this._props.layoutDoc.dataViz_filterSelection){ + if (this._props.layoutDoc.dataViz_filterSelection) { const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); this._tableDataIds.forEach(rowID => { - if (this._props.records[rowID][this._props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d.x - && this._props.records[rowID][this._props.axes[1]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d.y) { - if (!selectedRows?.includes(rowID)) selectedRows?.push(rowID); // adding to filtered rows - else if (sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows + if ( + Number(this._props.records[rowID][this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.x && // + Number(this._props.records[rowID][this._props.axes[1]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.y + ) { + if (!selectedRows?.includes(rowID)) + selectedRows?.push(rowID); // adding to filtered rows + else if (ptWasSelected) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows } - }) + }); } // coloring the selected point @@ -222,14 +221,14 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { for (let i = 0; i < elements.length; i++) { const x = Number(elements[i].getAttribute('data-x')); const y = Number(elements[i].getAttribute('data-y')); - if (x===d.x && y===d.y) { - if (sameAsAny) elements[i].classList.remove('brushed'); + if (x === d.x && y === d.y) { + if (ptWasSelected) elements[i].classList.remove('brushed'); else elements[i].classList.add('brushed'); } } } - drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) { if (this._lineChartSvg) { const circleClass = '.circle-' + idx; this._lineChartSvg @@ -241,7 +240,22 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .attr('cx', d => xScale(d.x)) .attr('cy', d => yScale(d.y)) .attr('data-x', d => d.x) - .attr('data-y', d => d.y); + .attr('data-y', d => d.y) + .on('mouseenter', e => { + const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) }; + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + higlightFocusPt.style('display', null); + }) + .on('mouseleave', () => { + tooltip?.transition().duration(300).style('opacity', 0); + }) + .on('click', (e: any) => { + const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) }; + // find .circle-d1 with data-x = d0.x and data-y = d0.y + this.setCurrSelected(d0); + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + this.colorSelectedPt(); + }); } } @@ -250,13 +264,13 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); - var { xMin, xMax, yMin, yMax } = rangeVals; + let { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } // adding svg - const margin = this._props.margin; + const { margin } = this._props; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') @@ -266,18 +280,19 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); - var validSecondData; - if (this._props.axes.length>2){ // for when there are 2 lines on the chart - var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + let validSecondData; + if (this._props.axes.length > 2) { + // for when there are 2 lines on the chart + const next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); validSecondData = next.filter(d => { - if (!d.x || Number.isNaN(d.x) || !d.y || Number.isNaN(d.y)) return false; + if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false; return true; }); - var secondDataRange = minMaxRange([validSecondData]); - if (secondDataRange.xMax!>xMax) xMax = secondDataRange.xMax; - if (secondDataRange.yMax!>yMax) yMax = secondDataRange.yMax; - if (secondDataRange.xMin!<xMin) xMin = secondDataRange.xMin; - if (secondDataRange.yMin!<yMin) yMin = secondDataRange.yMin; + const secondDataRange = minMaxRange([validSecondData]); + if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax; + if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax; + if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin; + if (secondDataRange.yMin! < yMin) yMin = secondDataRange.yMin; } // creating the x and y scales @@ -291,84 +306,47 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); + const higlightFocusPt = svg.append('g').style('display', 'none'); + higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); + const tooltip = this.setupTooltip(); + if (validSecondData) { drawLine(svg.append('path'), validSecondData, lineGen, true); - this.drawDataPoints(validSecondData, 0, xScale, yScale); - svg.append('path').attr("stroke", "red"); + this.drawDataPoints(validSecondData, 0, xScale, yScale, higlightFocusPt, tooltip); + svg.append('path').attr('stroke', 'red'); // legend - var color = d3.scaleOrdinal() - .range(["black", "blue"]) - .domain([this._props.axes[1], this._props.axes[2]]) - svg.selectAll("mydots") + const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); + svg.selectAll('mydots') .data([this._props.axes[1], this._props.axes[2]]) .enter() - .append("circle") - .attr("cx", 5) - .attr("cy", function(d,i){ return -30 + i*15}) - .attr("r", 7) - .style("fill", function(d){ return color(d)}) - svg.selectAll("mylabels") + .append('circle') + .attr('cx', 5) + .attr('cy', (d, i) => -30 + i * 15) + .attr('r', 7) + .style('fill', d => color(d)); + svg.selectAll('mylabels') .data([this._props.axes[1], this._props.axes[2]]) .enter() - .append("text") - .attr("x", 25) - .attr("y", function(d,i){ return -30 + i*15}) - .style("fill", function(d){ return color(d)}) - .text(function(d){ return d}) - .attr("text-anchor", "left") - .style("alignment-baseline", "middle") + .append('text') + .attr('x', 25) + .attr('y', (d, i) => -30 + i * 15) + .style('fill', d => color(d)) + .text(d => d) + .attr('text-anchor', 'left') + .style('alignment-baseline', 'middle'); } // get valid data points const data = dataSet[0]; - var validData = data.filter(d => { - Object.keys(data[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) return false; - }); - return true; - }); + const keys = Object.keys(data[0]); + const validData = data.filter(d => !keys.some(key => isNaN(d[key]))); // draw the plot line drawLine(svg.append('path'), validData, lineGen, false); - // draw the datapoint circle - this.drawDataPoints(validData, 0, xScale, yScale); - - const higlightFocusPt = svg.append('g').style('display', 'none'); - higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); - const tooltip = this.setupTooltip(); - // add all the tooltipContent to the tooltip - const mousemove = action((e: any) => { - const bisect = d3.bisector((d: DataPoint) => d.x).left; - const xPos = d3.pointer(e)[0]; - const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis - const d0 = data[x0]; - if (d0) this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - - this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - }); - - const onPointClick = action((e: any) => { - const bisect = d3.bisector((d: DataPoint) => d.x).left; - const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis - const d0 = data[x0]; - // find .circle-d1 with data-x = d0.x and data-y = d0.y - this.setCurrSelected(d0); - this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); - }); - svg.append('rect') - .attr('class', 'overlay') - .attr('width', width) - .attr('height', this.height + margin.top + margin.bottom) - .attr('fill', 'none') - .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) - .style('opacity', 0) - .on('mouseover', () => higlightFocusPt.style('display', null)) - .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) - .on('mousemove', mousemove) - .on('click', onPointClick); + // draw the datapoint circle + this.drawDataPoints(validData, 0, xScale, yScale, higlightFocusPt, tooltip); // axis titles svg.append('text') @@ -376,7 +354,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .style('text-anchor', 'middle') .text(this._props.axes[0]); svg.append('text') - .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') + .attr('transform', 'rotate(-90) translate(0, -10)') .attr('x', -(height / 2)) .attr('y', -30) .attr('height', 20) @@ -402,58 +380,61 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } render() { - var titleAccessor: any = 'dataViz_lineChart_title'; - if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; + let titleAccessor: any = 'dataViz_lineChart_title'; + if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + 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_lineChart_selectedData) this._props.layoutDoc.dataViz_lineChart_selectedData = new List<string>(); const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; - var selectedTitle = ""; - if (this._currSelected && this._props.titleCol){ - selectedTitle+= "\n" + this._props.titleCol + ": " + let selectedTitle = ''; + if (this._currSelected && this._props.titleCol) { + selectedTitle += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { - var mapThisEntry = false; - if (this._currSelected.x==each[this._props.axes[0]] && this._currSelected.y==each[this._props.axes[1]]) mapThisEntry = true; - else if (this._currSelected.y==each[this._props.axes[0]] && this._currSelected.x==each[this._props.axes[1]]) mapThisEntry = true; - if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ", "; - }) - selectedTitle = selectedTitle.slice(0,-1).slice(0,-1); + let mapThisEntry = false; + if (this._currSelected?.x === each[this._props.axes[0]] && this._currSelected?.y === each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected?.y === each[this._props.axes[0]] && this._currSelected?.x === each[this._props.axes[1]]) mapThisEntry = true; + if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', '; + }); + selectedTitle = selectedTitle.slice(0, -1).slice(0, -1); } - if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { + if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length === 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} setVal={undoable( - action(val => (this._props.layoutDoc[titleAccessor] = val as string)), + action(val => { + this._props.layoutDoc[titleAccessor] = val as string; + }), 'Change Graph Title' )} - color={'black'} + color="black" size={Size.LARGE} fillWidth /> </div> <div ref={this._lineChartRef} /> - {selectedPt != 'none' ? ( - <div className={'selected-data'}> + {selectedPt !== 'none' ? ( + <div className="selected-data"> {`Selected: ${selectedPt}`} {`${selectedTitle}`} <Button - onClick={e => { + onClick={() => { this._props.vizBox.sidebarBtnDown; this._props.vizBox.sidebarAddDocument; - }}></Button> + }} + /> </div> ) : null} </div> ) : ( - <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span> - ); - } 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> + <span className="chart-container"> first use table view to select two numerical axes to plot</span> ); + } + 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> + ); } } |