diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 105 |
1 files changed, 86 insertions, 19 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 6b564b0c9..8bace941f 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,13 +1,12 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -// import d3 import * as d3 from 'd3'; -import { Doc, DocListCast } from '../../../../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../../../../fields/Doc'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; -import { Cast, DocCast } from '../../../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; import { LinkManager } from '../../../../util/LinkManager'; @@ -15,16 +14,19 @@ import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; +import { EditableText, Size } from 'browndash-components'; +import { undoable } from '../../../../util/UndoManager'; export interface DataPoint { x: number; y: number; } -interface SelectedDataPoint extends DataPoint { +export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } export interface LineChartProps { rootDoc: Doc; + layoutDoc: Doc; axes: string[]; pairs: { [key: string]: any }[]; width: number; @@ -48,18 +50,25 @@ export class LineChart extends React.Component<LineChartProps> { // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates @computed get _lineChartData() { + var guids = StrListCast(this.props.layoutDoc.rowGuids); if (this.props.axes.length <= 1) return []; return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select')))) + ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)]))) .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })) .sort((a, b) => (a.x < b.x ? -1 : 1)); } + @computed get graphTitle(){ + return this.props.axes[1] + " vs. " + this.props.axes[0] + " Line Chart"; + } @computed get incomingLinks() { return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 !== this.props.rootDoc) // get links where this chart doc is the target of the link + .filter(link => { + return 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) @@ -180,6 +189,15 @@ export class LineChart extends React.Component<LineChartProps> { return this.props.width - this.props.margin.left - this.props.margin.right; } + @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 + " Line Chart"; + } + else return ax1 + " by " + ax0 + " Line Chart"; + } + setupTooltip() { return d3 .select(this._lineChartRef.current) @@ -197,9 +215,9 @@ export class LineChart extends React.Component<LineChartProps> { @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; + 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.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)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -219,7 +237,7 @@ export class LineChart extends React.Component<LineChartProps> { } // TODO: nda - can use d3.create() to create html element instead of appending - drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { + 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(); @@ -238,6 +256,7 @@ export class LineChart extends React.Component<LineChartProps> { const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') + .attr("class", "graph") .attr('width', `${width + margin.left + margin.right}`) .attr('height', `${height + margin.top + margin.bottom}`) .append('g') @@ -249,13 +268,20 @@ export class LineChart extends React.Component<LineChartProps> { xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); - // draw the plot line + // get valid data points const data = dataSet[0]; const lineGen = createLineGenerator(xScale, yScale); - drawLine(svg.append('path'), data, lineGen); - + var validData = data.filter((d => { + var valid = true; + Object.keys(data[0]).map(key => { + if (!d[key] || Number.isNaN(d[key])) valid = false; + }) + return valid; + })) + // draw the plot line + drawLine(svg.append('path'), validData, lineGen); // draw the datapoint circle - this.drawDataPoints(data, 0, xScale, yScale); + this.drawDataPoints(validData, 0, xScale, yScale); const higlightFocusPt = svg.append('g').style('display', 'none'); higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); @@ -293,6 +319,20 @@ export class LineChart extends React.Component<LineChartProps> { .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) .on('mousemove', mousemove) .on('click', onPointClick); + + // axis titles + svg.append("text") + .attr("transform", "translate(" + (width/2) + " ," + (height+40) + ")") + .style("text-anchor", "middle") + .text(this.props.axes[0]); + svg.append("text") + .attr("transform", "rotate(-90)" + " " + "translate( 0, " + -10 + ")") + .attr("x", -(height/2)) + .attr("y", -20) + .attr("height", 20) + .attr("width", 20) + .style("text-anchor", "middle") + .text(this.props.axes[1]); }; private updateTooltip( @@ -308,15 +348,42 @@ export class LineChart extends React.Component<LineChartProps> { tooltip .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip .style('pointer-events', 'none') - .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`); + .style('transform', `translate(${xScale(d0.x)-this.width}px,${yScale(d0.y)}px)`); } render() { - const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'; - return ( - <div ref={this._lineChartRef} className="chart-container"> - <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span> + this.componentDidMount(); + var titleAccessor:any = ''; + if (this.props.axes.length==2) titleAccessor = 'lineChart-title-'+this.props.axes[0]+'-'+this.props.axes[1]; + else if (this.props.axes.length>0) titleAccessor = 'lineChart-title-'+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'; + if (this._lineChartData.length>0){ + return ( + this.props.axes.length>=2 && /\d/.test(this.props.pairs[0][this.props.axes[0]]) && /\d/.test(this.props.pairs[0][this.props.axes[1]]) ? ( + <div className="chart-container" > + <div className="graph-title"> + <EditableText + val={StrCast(this.props.layoutDoc[titleAccessor])} + setVal={undoable (action(val => this.props.layoutDoc[titleAccessor] = val as string), "Change Graph Title")} + color={"black"} + size={Size.LARGE} + fillWidth + /> + </div> + <div ref={this._lineChartRef} /> + {selectedPt!='none'? + <div className={'selected-data'}> {`Selected: ${selectedPt}`}</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> - ); + ) } } |