From a8343cad405a146fdff8fc2d66ef41fdaefb8bda Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 6 Apr 2023 00:14:24 -0400
Subject: more simplification/cleanup of dataviz

---
 .../nodes/DataVizBox/components/LineChart.tsx      | 187 ++++++++++-----------
 1 file changed, 88 insertions(+), 99 deletions(-)

(limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')

diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index e5f7dc4f4..313164691 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -1,18 +1,15 @@
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { DataPoint } from '../ChartBox';
 // import d3
 import * as d3 from 'd3';
-import { minMaxRange, createLineGenerator, xGrid, yGrid, drawLine, xAxisCreator, yAxisCreator, scaleCreatorNumerical, scaleCreatorCategorical } from '../utils/D3Utils';
-import { Docs } from '../../../../documents/Documents';
 import { Doc, DocListCast } from '../../../../../fields/Doc';
-import { Chart, ChartProps } from '../ChartInterface';
-import './Chart.scss';
-import { List } from '../../../../../fields/List';
-import { PinProps, PresBox } from '../../trails';
 import { listSpec } from '../../../../../fields/Schema';
 import { Cast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { PinProps, PresBox } from '../../trails';
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
+import './Chart.scss';
 
 type minMaxRange = {
     xMin: number | undefined;
@@ -21,22 +18,43 @@ type minMaxRange = {
     yMax: number | undefined;
 };
 
-interface SelectedDataPoint {
+export interface DataPoint {
     x: number;
     y: number;
+}
+interface SelectedDataPoint extends DataPoint {
     elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
 }
+export interface LineChartData {
+    xLabel: string;
+    yLabel: string;
+    dataSet: DataPoint[][];
+}
+export interface LineChartProps {
+    rootDoc: Doc;
+    pairs: { x: number; y: number }[];
+    width: number;
+    height: number;
+    dataDoc: Doc;
+    fieldKey: string;
+    margin: {
+        top: number;
+        right: number;
+        bottom: number;
+        left: number;
+    };
+}
 
 @observer
-export class LineChart extends React.Component<ChartProps> implements Chart {
-    private _dataReactionDisposer: IReactionDisposer | undefined = undefined;
-    private _heightReactionDisposer: IReactionDisposer | undefined = undefined;
-    private _widthReactionDisposer: IReactionDisposer | undefined;
+export class LineChart extends React.Component<LineChartProps> {
+    private _disposers: { [key: string]: IReactionDisposer } = {};
     @observable private _x: number = 0;
     @observable private _y: number = 0;
     @observable private _currSelected: SelectedDataPoint | undefined = undefined;
+    @observable private _lineChartData: LineChartData | undefined = undefined;
     // create ref for the div
-    private _chartRef: React.RefObject<HTMLDivElement> = React.createRef();
+    private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef();
+    private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
     private _rangeVals: minMaxRange = {
         xMin: undefined,
         xMax: undefined,
@@ -45,43 +63,47 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
     };
     // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
 
-    private _chartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
-
-    // 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
-
-    // write the getanchor function that gets whatever I want as the link anchor
-
+    componentWillUnmount() {
+        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+    }
     componentDidMount = () => {
-        this._dataReactionDisposer = reaction(
-            () => this.props.chartData,
-            chartData => {
-                this._rangeVals = minMaxRange(chartData.data);
-                this.drawChart();
+        this._disposers.chartData = reaction(
+            () => ({ dataSet: this._lineChartData?.dataSet, w: this.props.width, h: this.props.height }),
+            vals => {
+                if (vals.dataSet) {
+                    this._rangeVals = minMaxRange(vals.dataSet);
+                    this.drawChart(vals.dataSet);
+                }
             },
             { fireImmediately: true }
         );
-        // DocumentDecorations.Instance.Interacting
-        this._heightReactionDisposer = reaction(() => this.props.height, this.drawChart.bind(this));
-        this._widthReactionDisposer = reaction(() => this.props.width, this.drawChart.bind(this));
-        reaction(
+        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
-                console.log(annotations);
                 // this.drawAnnotations()
                 // loop through annotations and draw them
-                annotations.forEach(a => {
-                    this.drawAnnotations(Number(a.x), Number(a.y));
-                });
+                annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
                 // this.drawAnnotations(annotations.x, annotations.y);
             },
             { fireImmediately: true }
         );
-
-        this.props.setCurrChart(this);
+        this.generateChartData();
     };
 
+    @action
+    generateChartData() {
+        this._lineChartData = {
+            xLabel: 'x',
+            yLabel: 'y',
+            // TODO: nda - add actual support for multiple sets of data
+            dataSet: [this.props.pairs?.map(pair => ({ x: pair.x, y: pair.y }))],
+        };
+    }
+
+    // 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
+
     // gets called whenever the "data-annotations" fields gets updated
     drawAnnotations(dataX: number, dataY: number) {
         // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
@@ -115,45 +137,27 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
         }
         return false;
     };
-    _getAnchor = (pinProps?: PinProps) => {
-        // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element)
 
+    // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+    getAnchor = (pinProps?: PinProps) => {
         const anchor = Docs.Create.TextanchorDocument({
-            /*put in some options*/
             title: 'line doc selection' + this._currSelected?.x,
         });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), dataviz: this._currSelected ? [this._currSelected.x, this._currSelected.y] : undefined } }, this.props.dataDoc);
         return anchor;
-        // have some other function in code that
     };
 
-    componentWillUnmount() {
-        if (this._dataReactionDisposer) {
-            this._dataReactionDisposer();
-        }
-        if (this._heightReactionDisposer) {
-            this._heightReactionDisposer();
-        }
-        if (this._widthReactionDisposer) {
-            this._widthReactionDisposer();
-        }
-    }
-
-    tooltipContent(data: DataPoint) {
-        return `<b>(${data.x},${data.y})</b>`;
-    }
-
-    @computed get height(): number {
+    @computed get height() {
         return this.props.height - this.props.margin.top - this.props.margin.bottom;
     }
 
-    @computed get width(): number {
+    @computed get width() {
         return this.props.width - this.props.margin.left - this.props.margin.right;
     }
 
     setupTooltip() {
         const tooltip = d3
-            .select(this._chartRef.current)
+            .select(this._lineChartRef.current)
             .append('div')
             .attr('class', 'tooltip')
             .style('opacity', 0)
@@ -169,13 +173,13 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
     @action
     setCurrSelected(x: number, y: number) {
         // TODO: nda - get rid of svg element in the list?
-        this._currSelected = { x: x, y: y };
+        this._currSelected = { x, y };
     }
 
     drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
-        if (this._chartSvg) {
+        if (this._lineChartSvg) {
             const circleClass = '.circle-' + idx;
-            this._chartSvg
+            this._lineChartSvg
                 .selectAll(circleClass)
                 .data(data)
                 .join('circle') // enter append
@@ -189,30 +193,13 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
     }
 
     // TODO: nda - can use d3.create() to create html element instead of appending
-    drawChart() {
-        const { chartData, margin } = this.props;
-        const data = chartData.data[0];
+    drawChart = (dataSet: DataPoint[][]) => {
         // clearing tooltip and the current chart
-        d3.select(this._chartRef.current).select('svg').remove();
-        d3.select(this._chartRef.current).select('.tooltip').remove();
-
-        // TODO: nda - refactor code so that it only recalculates min max and things related to data on data update
+        d3.select(this._lineChartRef.current).select('svg').remove();
+        d3.select(this._lineChartRef.current).select('.tooltip').remove();
 
         const { xMin, xMax, yMin, yMax } = this._rangeVals;
-        // const svg = d3.select(this._chartRef.current).append(this.svgContainer.html());
-        // adding svg
-        this._chartSvg = d3
-            .select(this._chartRef.current)
-            .append('svg')
-            .attr('width', `${this.width + margin.right + margin.left}`)
-            .attr('height', `${this.height + margin.top + margin.bottom}`)
-            .append('g')
-            .attr('transform', `translate(${margin.left}, ${margin.top})`);
-
-        const svg = this._chartSvg;
-
-        if (xMin == undefined || xMax == undefined || yMin == undefined || yMax == undefined) {
-            // TODO: nda - error handle
+        if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
             return;
         }
 
@@ -220,9 +207,15 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
         const xScale = scaleCreatorNumerical(xMin, xMax, 0, this.width);
         const yScale = scaleCreatorNumerical(0, yMax, this.height, 0);
 
-        // create a line function that takes in the data.data.x and data.data.y
-        // TODO: nda - fix the types for the d here
-        const lineGen = createLineGenerator(xScale, yScale);
+        // adding svg
+        const margin = this.props.margin;
+        const svg = (this._lineChartSvg = d3
+            .select(this._lineChartRef.current)
+            .append('svg')
+            .attr('width', `${this.width + margin.right + margin.left}`)
+            .attr('height', `${this.height + margin.top + margin.bottom}`)
+            .append('g')
+            .attr('transform', `translate(${margin.left}, ${margin.top})`));
 
         // create x and y grids
         xGrid(svg.append('g'), this.height, xScale);
@@ -230,7 +223,9 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
         xAxisCreator(svg.append('g'), this.height, xScale);
         yAxisCreator(svg.append('g'), this.width, yScale);
 
-        // draw the line
+        // draw the plot line
+        const data = dataSet[0];
+        const lineGen = createLineGenerator(xScale, yScale);
         drawLine(svg.append('path'), data, lineGen);
 
         // draw the datapoint circle
@@ -243,16 +238,15 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
         const mousemove = 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 - 25));
+            const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis
             const d0 = data[x0];
             this._x = d0.x;
             this._y = d0.y;
             focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`);
-            // TODO: nda - implement tooltips
             tooltip.transition().duration(300).style('opacity', 0.9);
             // TODO: nda - updating the inner html could be deadly cause injection attacks!
             tooltip
-                .html(() => this.tooltipContent(d0))
+                .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip
                 .style('pointer-events', 'none')
                 .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}px,${yScale(d0.y) + 30}px)`);
         });
@@ -260,7 +254,7 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
         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 - 25));
+            const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis
             const d0 = data[x0];
             this._x = d0.x;
             this._y = d0.y;
@@ -269,27 +263,22 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
             this._currSelected = { x: d0.x, y: d0.y, elem: selected };
         });
 
-        this._chartSvg
-            .append('rect')
+        svg.append('rect')
             .attr('class', 'overlay')
             .attr('width', this.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', () => {
-                focus.style('display', null);
-            })
-            .on('mouseout', () => {
-                tooltip.transition().duration(300).style('opacity', 0);
-            })
+            .on('mouseover', () => focus.style('display', null))
+            .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
             .on('mousemove', mousemove)
             .on('click', onPointClick);
-    }
+    };
 
     render() {
         return (
-            <div ref={this._chartRef} className="chart-container">
+            <div ref={this._lineChartRef} className="chart-container">
                 <span>Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}</span>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2

