aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/LineChart.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-08-16 13:31:56 -0400
committerGitHub <noreply@github.com>2023-08-16 13:31:56 -0400
commitd1e31265f8707bea63e21bf9a7b1dd10ccbf2009 (patch)
treeb9c8b7d2aa084c206d272828843fc2b2ce911089 /src/client/views/nodes/DataVizBox/components/LineChart.tsx
parentad74cd03fa38a66101331ac0d3ea6cda841e3eee (diff)
parent61fb855ec92540c48cc4cc844d3b21728e8a4754 (diff)
Merge pull request #206 from brown-dash/data-visualization-sarah
Data visualization sarah
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx105
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>
- );
+ )
}
}