aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/LineChart.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx211
1 files changed, 100 insertions, 111 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index bc35ab8c8..80edf2c36 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>;
}
@@ -48,7 +44,8 @@ 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;
+ @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);
@@ -71,11 +68,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,6 +83,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
}
componentDidMount() {
+ // draw chart
this._disposers.chartData = reaction(
() => ({ dataSet: this._lineChartData, w: this.width, h: this.height }),
({ dataSet, w, h }) => {
@@ -100,31 +93,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
},
{ 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
+ 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
@@ -137,12 +108,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
element.classList.remove('selected');
}
};
- // gets called whenever the "data_annotations" fields gets updated
+
+ // draws red annotation on data points when selected
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];
@@ -151,26 +119,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
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) => {
const anchor = Docs.Create.ConfigDocument({
@@ -182,6 +133,24 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
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;
}
@@ -212,18 +181,54 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
.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
+ }
+ });
+ }
+
+ // 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'));
+ 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
@@ -235,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();
+ });
}
}
@@ -286,9 +306,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 +344,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')
@@ -396,14 +384,15 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
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';
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);