diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 255 |
1 files changed, 103 insertions, 152 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index bc35ab8c8..c2f5388a2 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -3,7 +3,7 @@ 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 } 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'; @@ -11,16 +11,12 @@ import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import {} from '../../../DocComponent'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PinDocView } from '../../../PinFuncs'; +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'; -import { DocumentView } from '../../DocumentView'; -export interface DataPoint { - x: number; - y: number; -} export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } @@ -46,15 +42,23 @@ export interface LineChartProps { @observer export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; - private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _lineChartRef: HTMLDivElement | null = null; private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - @observable _currSelected: any | undefined = undefined; + @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) { super(props); makeObservable(this); } + @computed get titleAccessor() { + 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]; + return titleAccessor; + } + @computed get _tableDataIds() { return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } @@ -71,11 +75,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Links(this._props.Document) // out of all links - // .filter(link => { - // return 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 } @computed get incomingHighlited() { // return selected x and y axes @@ -91,40 +90,10 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount() { - this._disposers.chartData = reaction( - () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => { - if (dataSet) { - this.drawChart([dataSet], this.rangeVals, w, h); - } - }, - { 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, - incomingHighlited: this.incomingHighlited, - }), - ({ selected, incomingHighlited }) => { - // 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); - incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this._props.axes[0]]), Number(record[this._props.axes[1]]))); - }, - { fireImmediately: true } - ); + // coloring the selected point + if (!this._props.layoutDoc[this.titleAccessor]) this._props.layoutDoc[this.titleAccessor] = this.defaultGraphTitle; + if (!this._props.layoutDoc.dataViz_lineChart_selectedData) this._props.layoutDoc.dataViz_lineChart_selectedData = new List<string>(); + this._disposers.selector = reaction(() => StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData).slice(), this.colorSelectedPts, { 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 @@ -137,39 +106,6 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { 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 { - // } - } - }; - - @action - restoreView = (data: Doc) => { - const coords = Cast(data.config_dataVizSelection, listSpec('number'), null); - if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) { - this.setCurrSelected(coords[0], coords[1]); - return true; - } - if (this._currSelected) { - this.setCurrSelected(); - return true; - } - return false; - }; // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { @@ -182,6 +118,21 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { return anchor; }; + private colorSelectedPts = () => { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const dx = Number(elements[i].getAttribute('data-x')); + const dy = Number(elements[i].getAttribute('data-y')); + const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData); + const selected = selectedDataBars.some(eachSelectedData => { + const [sx, sy] = eachSelectedData.split(','); // parse each selected point into x,y + return Number(sx) === dx && Number(sy) === dy; + }); + if (selected) elements[i].classList.add('brushed'); + else elements[i].classList.remove('brushed'); + } + }; + @computed get height() { return this._props.height - this._props.margin.top - this._props.margin.bottom; } @@ -200,30 +151,46 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } setupTooltip() { - return d3 - .select(this._lineChartRef.current) - .append('div') - .attr('class', 'tooltip') - .style('opacity', 0) - .style('background', '#fff') - .style('border', '1px solid #ccc') - .style('padding', '5px') - .style('position', 'absolute') - .style('font-size', '12px'); + return d3.select(this._lineChartRef).append('div').attr('class', 'tooltip').style('opacity', 0).style('background', '#fff').style('border', '1px solid #ccc').style('padding', '5px').style('position', 'absolute').style('font-size', '12px'); } - // TODO: nda - use this everyewhere we update currSelected? @action - setCurrSelected(x?: number, y?: number) { - // TODO: nda - get rid of svg element in the list? - if (this._currSelected && this._currSelected.x === x && this._currSelected.y === y) this._currSelected = undefined; - else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this._props.records.forEach(record => { - record[this._props.axes[0]] === x && record[this._props.axes[1]] === y && (record.selected = true); + setCurrSelected(d: DataPoint) { + let ptWasSelected = false; + const selectedDatapoints = Cast(this._props.layoutDoc.dataViz_lineChart_selectedData, listSpec('string'), null); + 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 (!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) { + const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + this._tableDataIds.forEach(rowID => { + 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 + } + }); + } } - 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 @@ -235,14 +202,28 @@ 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); + }); } } drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { // clearing tooltip and the current chart - d3.select(this._lineChartRef.current).select('svg').remove(); - d3.select(this._lineChartRef.current).select('.tooltip').remove(); + d3.select(this._lineChartRef).select('svg').remove(); + d3.select(this._lineChartRef).select('.tooltip').remove(); let { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { @@ -252,7 +233,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { // adding svg const { margin } = this._props; const svg = (this._lineChartSvg = d3 - .select(this._lineChartRef.current) + .select(this._lineChartRef) .append('svg') .attr('class', 'graph') .attr('width', `${width + margin.left + margin.right}`) @@ -286,9 +267,13 @@ 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); + this.drawDataPoints(validSecondData, 0, xScale, yScale, higlightFocusPt, tooltip); svg.append('path').attr('stroke', 'red'); // legend @@ -320,45 +305,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { // 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 - svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); - this.setCurrSelected(d0.x, d0.y); - 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') @@ -373,6 +322,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .attr('width', 20) .style('text-anchor', 'middle') .text(this._props.axes[1]); + this.colorSelectedPts(); }; private updateTooltip( @@ -392,18 +342,14 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } render() { - 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; const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; let selectedTitle = ''; if (this._currSelected && this._props.titleCol) { selectedTitle += '\n' + this._props.titleCol + ': '; this._tableData.forEach(each => { 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 (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); @@ -413,10 +359,10 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText - val={StrCast(this._props.layoutDoc[titleAccessor])} + val={StrCast(this._props.layoutDoc[this.titleAccessor])} setVal={undoable( action(val => { - this._props.layoutDoc[titleAccessor] = val as string; + this._props.layoutDoc[this.titleAccessor] = val as string; }), 'Change Graph Title' )} @@ -425,7 +371,12 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { fillWidth /> </div> - <div ref={this._lineChartRef} /> + <div + ref={r => { + this._lineChartRef = r; + this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height); + }} + /> {selectedPt !== 'none' ? ( <div className="selected-data"> {`Selected: ${selectedPt}`} |