aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/LineChart.tsx
diff options
context:
space:
mode:
authorNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
committerNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2024-06-03 13:33:37 -0400
commit9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch)
tree14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/DataVizBox/components/LineChart.tsx
parent1be63695875c9242fba43d580465e8765cf3991d (diff)
parent202e994515392892676f8f080852db1e32b8dbd3 (diff)
Merge branch 'master' into nathan-starter
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx255
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}`}