From ba8cb4194062b0f939afd136a269625f9f26dcaa Mon Sep 17 00:00:00 2001
From: Naafiyan Ahmed <naafiyan@gmail.com>
Date: Mon, 25 Jul 2022 19:10:55 -0400
Subject: cleaned up code and moved files around

---
 .../views/nodes/DataVizBox/components/TableBox.tsx | 37 ++++++++++++++++++++++
 1 file changed, 37 insertions(+)
 create mode 100644 src/client/views/nodes/DataVizBox/components/TableBox.tsx

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

diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
new file mode 100644
index 000000000..dfa8262d8
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -0,0 +1,37 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+
+interface TableBoxProps {
+    pairs: {x: number, y:number}[]
+}
+
+
+export class TableBox extends React.Component<TableBoxProps> {
+
+
+
+    render() {
+        return (
+            <div className="table-container">
+                <table className="table">
+                    <thead>
+                        <tr className="table-row">
+                            <th>x</th>
+                            <th>y</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    {this.props.pairs.map(p => {
+                        return (<tr className="table-row">
+                            <td>{p.x}</td>
+                            <td>{p.y}</td>
+                        </tr>)
+                    })}
+                    </tbody>
+                </table>
+            </div>
+        )
+    }
+
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


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

---
 src/client/views/nodes/DataVizBox/ChartBox.tsx     | 100 -----------
 .../views/nodes/DataVizBox/ChartInterface.ts       |  36 ----
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |  84 ++++-----
 .../nodes/DataVizBox/components/LineChart.tsx      | 187 ++++++++++-----------
 .../views/nodes/DataVizBox/components/TableBox.tsx |  31 ++--
 src/client/views/nodes/DataVizBox/utils/D3Utils.ts |   2 +-
 6 files changed, 133 insertions(+), 307 deletions(-)
 delete mode 100644 src/client/views/nodes/DataVizBox/ChartBox.tsx
 delete mode 100644 src/client/views/nodes/DataVizBox/ChartInterface.ts

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

diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx
deleted file mode 100644
index ee577b9fd..000000000
--- a/src/client/views/nodes/DataVizBox/ChartBox.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc } from '../../../../fields/Doc';
-import { action, computed, observable } from 'mobx';
-import { NumCast, StrCast } from '../../../../fields/Types';
-import { LineChart } from './components/LineChart';
-import { Chart, ChartData } from './ChartInterface';
-import { PinProps } from '../trails';
-
-export interface ChartBoxProps {
-    rootDoc: Doc;
-    dataDoc: Doc;
-    height: number;
-    pairs: { x: number; y: number }[];
-    setChartBox: (chartBox: ChartBox) => void;
-    getAnchor: () => Doc;
-}
-
-export interface DataPoint {
-    x: number;
-    y: number;
-}
-
-@observer
-export class ChartBox extends React.Component<ChartBoxProps> {
-    @observable private _chartData: ChartData | undefined = undefined;
-    private currChart: Chart | undefined;
-
-    @computed get currView() {
-        if (this.props.rootDoc._dataVizView) {
-            return StrCast(this.props.rootDoc._currChartView);
-        } else {
-            return 'table';
-        }
-    }
-
-    constructor(props: any) {
-        super(props);
-        if (!this.props.rootDoc._currChartView) {
-            this.props.rootDoc._currChartView = 'bar';
-        }
-    }
-
-    setCurrChart = (chart: Chart) => {
-        this.currChart = chart;
-    };
-
-    @action
-    generateChartData() {
-        if (this.props.rootDoc._chartData) {
-            this._chartData = JSON.parse(StrCast(this.props.rootDoc._chartData));
-            return;
-        }
-
-        const data: ChartData = {
-            xLabel: '',
-            yLabel: '',
-            data: [[]],
-        };
-
-        if (this.props.pairs && this.props.pairs.length > 0) {
-            data.xLabel = 'x';
-            data.yLabel = 'y';
-            this.props.pairs.forEach(pair => {
-                // TODO: nda - add actual support for multiple sets of data
-                data.data[0].push({ x: pair.x, y: pair.y });
-            });
-        }
-
-        this._chartData = data;
-        this.props.rootDoc._chartData = JSON.stringify(data);
-    }
-
-    componentDidMount = () => {
-        this.generateChartData();
-        this.props.setChartBox(this);
-    };
-
-    @action
-    onClickChangeChart = (e: React.MouseEvent<HTMLButtonElement>) => {
-        e.preventDefault();
-        e.stopPropagation();
-        console.log(e.currentTarget.value);
-        this.props.rootDoc._currChartView = e.currentTarget.value.toLowerCase();
-    };
-
-    getAnchor = (pinProps?: PinProps) => this.currChart?._getAnchor(pinProps);
-
-    restoreView = (data: Doc) => this.currChart?.restoreView(data);
-
-    render() {
-        if (this.props.pairs && this._chartData) {
-            const width = NumCast(this.props.rootDoc._width) * 0.9;
-            const height = this.props.height * 0.9;
-            const margin = { top: 10, right: 50, bottom: 50, left: 50 };
-            return <LineChart margin={margin} width={width} height={height} chartData={this._chartData} setCurrChart={this.setCurrChart} dataDoc={this.props.dataDoc} fieldKey={'data'} getAnchor={this.props.getAnchor} />;
-        }
-        return <div />;
-    }
-}
diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts
deleted file mode 100644
index 8ebcc2a5f..000000000
--- a/src/client/views/nodes/DataVizBox/ChartInterface.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Doc } from '../../../../fields/Doc';
-import { PinProps } from '../trails';
-import { DataPoint } from './ChartBox';
-
-export interface Chart {
-    tooltipContent: (data: DataPoint) => string;
-    drawChart: () => void;
-    restoreView: (data: any) => boolean;
-    height: number;
-    width: number;
-    // TODO: nda - probably want to get rid of this to keep charts more generic
-    _getAnchor: (pinProps?: PinProps) => Doc;
-}
-
-export interface ChartProps {
-    chartData: ChartData;
-    width: number;
-    height: number;
-    dataDoc: Doc;
-    fieldKey: string;
-    // returns linechart component but should be generic chart
-    setCurrChart: (chart: Chart) => void;
-    getAnchor: () => Doc;
-    margin: {
-        top: number;
-        right: number;
-        bottom: number;
-        left: number;
-    };
-}
-
-export interface ChartData {
-    xLabel: string;
-    yLabel: string;
-    data: DataPoint[][];
-}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 68766e8dc..e279a1262 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -2,29 +2,32 @@ import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
-import { BoolCast, Cast, StrCast } from '../../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { CsvField } from '../../../../fields/URLField';
 import { Docs } from '../../../documents/Documents';
 import { ViewBoxAnnotatableComponent } from '../../DocComponent';
-import { FieldViewProps, FieldView } from '../FieldView';
-import { ChartBox } from './ChartBox';
-import './DataVizBox.scss';
-import { TableBox } from './components/TableBox';
+import { FieldView, FieldViewProps } from '../FieldView';
 import { PinProps } from '../trails';
+import { LineChart } from './components/LineChart';
+import { TableBox } from './components/TableBox';
+import './DataVizBox.scss';
 
 enum DataVizView {
     TABLE = 'table',
-    HISTOGRAM = 'histogram',
+    LINECHART = 'lineChart',
 }
 
 @observer
 export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(DataVizBox, fieldKey);
+    }
     // says we have an object and any string
     // 2 ways of doing it
     // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
     // @observable private pairs: { [key: string]: FieldResult }[] = [];
     @observable private pairs: { x: number; y: number }[] = [];
-    private _chartBox: ChartBox | undefined;
+    private _chartRenderer: LineChart | undefined;
     // // another way would be store a schema that defines the type of data we are expecting from an imported doc
 
     // method1() {
@@ -45,25 +48,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // [key: string]: FieldResult;
     // instead of numeric x,y in there,
 
-    // TODO: nda - make this use enum values instead
-    // @observable private currView: DataVizView = DataVizView.TABLE;
-
     // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
 
-    setChartBox = (chartBox: ChartBox) => {
-        this._chartBox = chartBox;
-    };
-
-    @computed get currView() {
-        return StrCast(this.rootDoc._dataVizView, 'table');
+    @computed get dataVizView(): DataVizView {
+        return StrCast(this.rootDoc._dataVizView, 'table') as DataVizView;
     }
 
     @action
     restoreView = (data: Doc) => {
-        const changed = this.currView !== data.dataView && (this.rootDoc._dataVizView = data.dataView);
-        const func = () => this._chartBox?.restoreView(data);
+        const changed = this.dataVizView !== data.presDataVizView && (this.rootDoc._dataVizView = data.presDataVizView);
+        const func = () => this._chartRenderer?.restoreView(data);
         if (changed) {
-            setTimeout(func);
+            setTimeout(func, 100);
             return changed ? true : false;
         }
         return func() ?? false;
@@ -71,38 +67,27 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
         const anchor =
-            this._chartBox?.getAnchor(pinProps) ??
+            this._chartRenderer?.getAnchor(pinProps) ??
             Docs.Create.TextanchorDocument({
                 // when we clear selection -> we should have it so chartBox getAnchor returns undefined
                 // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
                 /*put in some options*/
             });
 
-        anchor.dataView = this.currView;
+        anchor.presDataVizView = this.dataVizView;
 
         this.addDocument(anchor);
         return anchor;
     };
-    constructor(props: any) {
-        super(props);
-        if (!this.rootDoc._dataVizView) {
-            // TODO: nda - this might not always want to default to "table"
-            this.rootDoc._dataVizView = 'table';
-        }
-    }
-
-    public static LayoutString(fieldKey: string) {
-        return FieldView.LayoutString(DataVizBox, fieldKey);
-    }
 
     @computed get selectView() {
-        switch (this.currView) {
-            case 'table':
-                return <TableBox pairs={this.pairs} />;
-            case 'histogram':
-                return <ChartBox height={this.props.PanelHeight() - 32 /* height of 'change view' button */} getAnchor={this.getAnchor} setChartBox={this.setChartBox} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />;
-            // case "histogram":
-            //     return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+        const width = NumCast(this.rootDoc._width) * 0.9;
+        const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
+        const margin = { top: 10, right: 50, bottom: 50, left: 50 };
+        // prettier-ignore
+        switch (this.dataVizView) {
+            case DataVizView.TABLE:     return <TableBox pairs={this.pairs} />;
+            case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />;
         }
     }
 
@@ -116,34 +101,25 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     componentDidMount() {
         this.props.setContentView?.(this);
-        // this.createPairs();
         this.fetchData();
     }
 
     fetchData() {
-        const uri = this.dataUrl?.url.href;
-        fetch('/csvData?uri=' + uri).then(res =>
-            res.json().then(
-                action(res => {
-                    this.pairs = res;
-                })
-            )
-        );
+        fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+            .then(res => res.json().then(action(res => !res.errno && (this.pairs = res))));
     }
 
     // handle changing the view using a button
     @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
         e.preventDefault();
         e.stopPropagation();
-        this.rootDoc._dataVizView = this.currView === 'table' ? 'histogram' : 'table';
+        this.rootDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
     }
 
     render() {
-        if (this.pairs.length == 0) {
-            return <div>Loading...</div>;
-        }
-
-        return (
+        return this.pairs.length == 0 ? (
+            <div>Loading...</div>
+        ) : (
             <div
                 className="dataViz"
                 onWheel={e => e.stopPropagation()}
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>
         );
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index dfa8262d8..28114036f 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,16 +1,12 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
 
 interface TableBoxProps {
-    pairs: {x: number, y:number}[]
+    pairs: { x: number; y: number }[];
 }
 
-
 export class TableBox extends React.Component<TableBoxProps> {
-
-
-
     render() {
         return (
             <div className="table-container">
@@ -22,16 +18,17 @@ export class TableBox extends React.Component<TableBoxProps> {
                         </tr>
                     </thead>
                     <tbody>
-                    {this.props.pairs.map(p => {
-                        return (<tr className="table-row">
-                            <td>{p.x}</td>
-                            <td>{p.y}</td>
-                        </tr>)
-                    })}
+                        {this.props.pairs?.map(p => {
+                            return (
+                                <tr className="table-row">
+                                    <td>{p.x}</td>
+                                    <td>{p.y}</td>
+                                </tr>
+                            );
+                        })}
                     </tbody>
                 </table>
             </div>
-        )
+        );
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
index 2bb091999..e1ff6f8eb 100644
--- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -1,5 +1,5 @@
 import * as d3 from 'd3';
-import { DataPoint } from '../ChartBox';
+import { DataPoint } from '../components/LineChart';
 
 // TODO: nda - implement function that can handle range for strings
 
-- 
cgit v1.2.3-70-g09d2


From 7975e3f4aaa3601a5512a7dce4c261e7f9411374 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 6 Apr 2023 10:01:39 -0400
Subject: added selection ui for choosing linechart axes.

---
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   | 31 +++++++++-------
 .../nodes/DataVizBox/components/LineChart.tsx      | 33 ++++++++++++-----
 .../views/nodes/DataVizBox/components/TableBox.tsx | 43 +++++++++++++++++++---
 src/client/views/nodes/trails/PresBox.tsx          |  7 +---
 4 files changed, 79 insertions(+), 35 deletions(-)

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

diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index e279a1262..0ce589a13 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,7 +1,8 @@
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc } from '../../../../fields/Doc';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
 import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { CsvField } from '../../../../fields/URLField';
 import { Docs } from '../../../documents/Documents';
@@ -26,7 +27,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // 2 ways of doing it
     // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
     // @observable private pairs: { [key: string]: FieldResult }[] = [];
-    @observable private pairs: { x: number; y: number }[] = [];
+    @observable private pairs: { [key: string]: string }[] = [];
     private _chartRenderer: LineChart | undefined;
     // // another way would be store a schema that defines the type of data we are expecting from an imported doc
 
@@ -51,16 +52,17 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
 
     @computed get dataVizView(): DataVizView {
-        return StrCast(this.rootDoc._dataVizView, 'table') as DataVizView;
+        return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
     }
 
     @action
     restoreView = (data: Doc) => {
-        const changed = this.dataVizView !== data.presDataVizView && (this.rootDoc._dataVizView = data.presDataVizView);
+        const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView);
+        const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._dataVizAxes = new List<string>(StrListCast(data.presDataVizAxes)));
         const func = () => this._chartRenderer?.restoreView(data);
-        if (changed) {
+        if (changedView || changedAxes) {
             setTimeout(func, 100);
-            return changed ? true : false;
+            return true;
         }
         return func() ?? false;
     };
@@ -75,26 +77,27 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             });
 
         anchor.presDataVizView = this.dataVizView;
+        anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
 
         this.addDocument(anchor);
         return anchor;
     };
 
+    @computed.struct get axes() {
+        return StrListCast(this.layoutDoc.dataVizAxes);
+    }
+    selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List<string>(axes));
+
     @computed get selectView() {
         const width = NumCast(this.rootDoc._width) * 0.9;
         const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
         const margin = { top: 10, right: 50, bottom: 50, left: 50 };
         // prettier-ignore
         switch (this.dataVizView) {
-            case DataVizView.TABLE:     return <TableBox pairs={this.pairs} />;
-            case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />;
+            case DataVizView.TABLE:     return <TableBox pairs={this.pairs} axes={this.axes} selectAxes={this.selectAxes}/>;
+            case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
         }
     }
-
-    @computed get pairVals() {
-        return fetch('/csvData?uri=' + this.dataUrl?.url.href).then(res => res.json());
-    }
-
     @computed get dataUrl() {
         return Cast(this.dataDoc[this.fieldKey], CsvField);
     }
@@ -113,7 +116,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
         e.preventDefault();
         e.stopPropagation();
-        this.rootDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
+        this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
     }
 
     render() {
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 313164691..2357b7c69 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
 // import d3
 import * as d3 from 'd3';
 import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
 import { Cast } from '../../../../../fields/Types';
 import { Docs } from '../../../../documents/Documents';
@@ -32,7 +33,8 @@ export interface LineChartData {
 }
 export interface LineChartProps {
     rootDoc: Doc;
-    pairs: { x: number; y: number }[];
+    axes: string[];
+    pairs: { [key: string]: any }[];
     width: number;
     height: number;
     dataDoc: Doc;
@@ -67,8 +69,13 @@ export class LineChart extends React.Component<LineChartProps> {
         Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
     }
     componentDidMount = () => {
+        this._disposers.chartdata = reaction(
+            () => this.props.axes.slice(),
+            axes => axes.length > 1 && this.generateChartData(),
+            { fireImmediately: true }
+        );
         this._disposers.chartData = reaction(
-            () => ({ dataSet: this._lineChartData?.dataSet, w: this.props.width, h: this.props.height }),
+            () => ({ dataSet: this._lineChartData?.dataSet, axes: this.props.axes.slice(), w: this.props.width, h: this.props.height }),
             vals => {
                 if (vals.dataSet) {
                     this._rangeVals = minMaxRange(vals.dataSet);
@@ -89,16 +96,15 @@ export class LineChart extends React.Component<LineChartProps> {
             },
             { fireImmediately: true }
         );
-        this.generateChartData();
     };
 
     @action
     generateChartData() {
         this._lineChartData = {
-            xLabel: 'x',
-            yLabel: 'y',
+            xLabel: this.props.axes[0],
+            yLabel: this.props.axes[1],
             // TODO: nda - add actual support for multiple sets of data
-            dataSet: [this.props.pairs?.map(pair => ({ x: pair.x, y: pair.y }))],
+            dataSet: [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))],
         };
     }
 
@@ -129,12 +135,17 @@ export class LineChart extends React.Component<LineChartProps> {
         // loop through and remove any annotations that no longer exist
     }
 
+    @action
     restoreView = (data: Doc) => {
-        const coords = Cast(data.presDataViz, listSpec('number'), null);
-        if ((coords && this._currSelected?.x !== coords[0]) || this._currSelected?.y !== coords[1]) {
+        const coords = Cast(data.presDataVizSelection, 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._currSelected = undefined;
+            return true;
+        }
         return false;
     };
 
@@ -143,7 +154,8 @@ export class LineChart extends React.Component<LineChartProps> {
         const anchor = Docs.Create.TextanchorDocument({
             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);
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
+        anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
         return anchor;
     };
 
@@ -277,9 +289,10 @@ export class LineChart extends React.Component<LineChartProps> {
     };
 
     render() {
+        const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none';
         return (
             <div ref={this._lineChartRef} className="chart-container">
-                <span>Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}</span>
+                <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Curr Selected: ${selectedPt}`}</span>
             </div>
         );
     }
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 28114036f..adefe90cd 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,28 +1,59 @@
-import { action, computed, observable } from 'mobx';
+import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../../Utils';
 
 interface TableBoxProps {
-    pairs: { x: number; y: number }[];
+    pairs: { [key: string]: any }[];
+    selectAxes: (axes: string[]) => void;
+    axes: string[];
 }
 
+@observer
 export class TableBox extends React.Component<TableBoxProps> {
+    @computed get columns() {
+        return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : [];
+    }
     render() {
         return (
             <div className="table-container">
                 <table className="table">
                     <thead>
                         <tr className="table-row">
-                            <th>x</th>
-                            <th>y</th>
+                            {this.columns.map(col => (
+                                <th
+                                    style={{ color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined, fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal' }}
+                                    onPointerDown={e =>
+                                        setupMoveUpEvents(
+                                            {},
+                                            e,
+                                            returnFalse,
+                                            emptyFunction,
+                                            action(e => {
+                                                const newAxes = this.props.axes;
+                                                if (newAxes.includes(col)) {
+                                                    newAxes.splice(newAxes.indexOf(col), 1);
+                                                } else if (newAxes.length >= 1) {
+                                                    newAxes[1] = col;
+                                                } else {
+                                                    newAxes[0] = col;
+                                                }
+                                                this.props.selectAxes(newAxes);
+                                            })
+                                        )
+                                    }>
+                                    {col}
+                                </th>
+                            ))}
                         </tr>
                     </thead>
                     <tbody>
                         {this.props.pairs?.map(p => {
                             return (
                                 <tr className="table-row">
-                                    <td>{p.x}</td>
-                                    <td>{p.y}</td>
+                                    {this.columns.map(col => (
+                                        <td>{p[col]}</td>
+                                    ))}
                                 </tr>
                             );
                         })}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0e22ea3b1..3589a9065 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -492,10 +492,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
-        if (!pinDataTypes && activeItem.presDataViz !== undefined) {
-            if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
-                changed = true;
-            }
+        if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
+            changed = true;
         }
 
         if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
@@ -654,7 +652,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType;
             if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
             if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
-            if (pinProps.pinData.dataviz) pinDoc.presDataViz = new List<number>(pinProps.pinData.dataviz);
             if (pinProps.pinData.pannable) {
                 pinDoc.presPanX = NumCast(targetDoc._panX);
                 pinDoc.presPanY = NumCast(targetDoc._panY);
-- 
cgit v1.2.3-70-g09d2


From 986c5bdc899954c4609f71405d3334147be721fe Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 7 Apr 2023 10:42:16 -0400
Subject: experimetnal changes to datavizbox to allow brushing data items and
 better highlighting of selections.  Also working on drawing link lines
 between chart aliases.

---
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |  11 ++-
 .../views/nodes/DataVizBox/components/Chart.scss   |  20 ++++
 .../nodes/DataVizBox/components/LineChart.tsx      | 105 +++++++++++++++------
 .../views/nodes/DataVizBox/components/TableBox.tsx |  98 +++++++++++++------
 src/fields/Doc.ts                                  |   1 +
 5 files changed, 170 insertions(+), 65 deletions(-)

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

diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 0ce589a13..aaa8c3c53 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -13,7 +13,7 @@ import { LineChart } from './components/LineChart';
 import { TableBox } from './components/TableBox';
 import './DataVizBox.scss';
 
-enum DataVizView {
+export enum DataVizView {
     TABLE = 'table',
     LINECHART = 'lineChart',
 }
@@ -27,7 +27,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     // 2 ways of doing it
     // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
     // @observable private pairs: { [key: string]: FieldResult }[] = [];
-    @observable private pairs: { [key: string]: string }[] = [];
+    @observable pairs: { [key: string]: string }[] = [];
     private _chartRenderer: LineChart | undefined;
     // // another way would be store a schema that defines the type of data we are expecting from an imported doc
 
@@ -71,6 +71,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const anchor =
             this._chartRenderer?.getAnchor(pinProps) ??
             Docs.Create.TextanchorDocument({
+                unrendered: true,
                 // when we clear selection -> we should have it so chartBox getAnchor returns undefined
                 // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
                 /*put in some options*/
@@ -89,12 +90,12 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List<string>(axes));
 
     @computed get selectView() {
-        const width = NumCast(this.rootDoc._width) * 0.9;
+        const width = this.props.PanelWidth() * 0.9;
         const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
         const margin = { top: 10, right: 50, bottom: 50, left: 50 };
         // prettier-ignore
         switch (this.dataVizView) {
-            case DataVizView.TABLE:     return <TableBox pairs={this.pairs} axes={this.axes} selectAxes={this.selectAxes}/>;
+            case DataVizView.TABLE:     return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>;
             case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
         }
     }
@@ -136,7 +137,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         { passive: false }
                     )
                 }>
-                <button onClick={e => this.changeViewHandler(e)}>Change View</button>
+                <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
                 {this.selectView}
             </div>
         );
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index 2d6c5f0f2..c8daf7c90 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -8,8 +8,28 @@
     // change the color of the circle element to be red
     fill: red;
 }
+.focus-selected,
+.selected {
+    // change the color of the circle element to be red
+    fill: lightblue;
+    position: absolute;
+    transform-box: fill-box;
+    scale: 2;
+    transform-origin: center;
+}
+.focus {
+    fill: transparent;
+    outline: black solid 1px;
+    border-radius: 100%;
+}
+.focus-selected {
+    scale: 1;
+    outline: black solid 1px;
+    border-radius: 100%;
+}
 .chart-container {
     display: flex;
     flex-direction: column;
     align-items: center;
+    cursor: default;
 }
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index e6a06a454..6a223e683 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -6,11 +6,15 @@ import * as d3 from 'd3';
 import { Doc, DocListCast } from '../../../../../fields/Doc';
 import { List } from '../../../../../fields/List';
 import { listSpec } from '../../../../../fields/Schema';
-import { Cast } from '../../../../../fields/Types';
+import { Cast, DocCast, NumCast } 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';
+import { LinkManager } from '../../../../util/LinkManager';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { DataVizBox } from '../DataVizBox';
 
 export interface DataPoint {
     x: number;
@@ -40,7 +44,7 @@ export class LineChart extends React.Component<LineChartProps> {
     private _disposers: { [key: string]: IReactionDisposer } = {};
     private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef();
     private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
-    private _rangeVals: { xMin?: number; xMax?: number;yMin?: number;yMax?: number;}= {};
+    private _rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number } = {};
     @observable _currSelected: SelectedDataPoint | undefined = undefined;
     @observable _lineChartData: 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
@@ -51,9 +55,11 @@ export class LineChart extends React.Component<LineChartProps> {
     componentDidMount = () => {
         this._disposers.chartdata = reaction(
             () => this.props.axes.slice(),
-            axes => {if (axes.length > 1) {
-                this._lineChartData = [this.props.pairs?.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)]
-            }},
+            axes => {
+                if (axes.length > 1) {
+                    this._lineChartData = [this.props.pairs?.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))];
+                }
+            },
             { fireImmediately: true }
         );
         this._disposers.chartData = reaction(
@@ -78,12 +84,38 @@ export class LineChart extends React.Component<LineChartProps> {
             },
             { fireImmediately: true }
         );
+        this._disposers.highlights = reaction(
+            () => ({
+                selected: this._currSelected,
+                pairs: LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc)
+                    .filter(link => link.anchor1 !== this.props.rootDoc) // all links that are pointing to this node
+                    .map(link => DocCast(link.anchor1)) // get the documents that are pointing to this node
+                    .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes
+                    .filter(dvb => dvb)
+                    .map(dvb => dvb.pairs.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor
+                    .lastElement(),
+            }),
+            ({ selected, pairs }) => {
+                this.clearAnnoations();
+                selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
+                pairs.forEach(pair => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+            },
+            { 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
 
+    clearAnnoations = () => {
+        const elements = document.querySelectorAll('.datapoint');
+        for (let i = 0; i < elements.length; i++) {
+            const element = elements[i];
+            element.classList.remove('highlight');
+            element.classList.remove('selected');
+        }
+    };
     // gets called whenever the "data-annotations" fields gets updated
-    drawAnnotations(dataX: number, dataY: number) {
+    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
@@ -94,14 +126,13 @@ export class LineChart extends React.Component<LineChartProps> {
             const x = element.getAttribute('data-x');
             const y = element.getAttribute('data-y');
             if (x === dataX.toString() && y === dataY.toString()) {
-                element.classList.add('highlight');
+                element.classList.add(selected ? 'selected' : 'highlight');
             }
             // TODO: nda - this remove highlight code should go where we remove the links
             // } else {
-            //     element.classList.remove('highlight');
             // }
         }
-    }
+    };
 
     removeAnnotations(dataX: number, dataY: number) {
         // loop through and remove any annotations that no longer exist
@@ -115,7 +146,7 @@ export class LineChart extends React.Component<LineChartProps> {
             return true;
         }
         if (this._currSelected) {
-            this._currSelected = undefined;
+            this.setCurrSelected();
             return true;
         }
         return false;
@@ -123,9 +154,7 @@ export class LineChart extends React.Component<LineChartProps> {
 
     // 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({
-            title: 'line doc selection' + this._currSelected?.x,
-        });
+        const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true });
         PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
         anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
         return anchor;
@@ -140,7 +169,7 @@ export class LineChart extends React.Component<LineChartProps> {
     }
 
     setupTooltip() {
-        const tooltip = d3
+        return d3
             .select(this._lineChartRef.current)
             .append('div')
             .attr('class', 'tooltip')
@@ -150,14 +179,15 @@ export class LineChart extends React.Component<LineChartProps> {
             .style('padding', '5px')
             .style('position', 'absolute')
             .style('font-size', '12px');
-        return tooltip;
     }
 
     // TODO: nda - use this everyewhere we update currSelected?
     @action
-    setCurrSelected(x: number, y: number) {
+    setCurrSelected(x?: number, y?: number) {
         // TODO: nda - get rid of svg element in the list?
-        this._currSelected = { x, y };
+        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>) {
@@ -215,32 +245,29 @@ export class LineChart extends React.Component<LineChartProps> {
         // draw the datapoint circle
         this.drawDataPoints(data, 0, xScale, yScale);
 
-        const focus = svg.append('g').attr('class', 'focus').style('display', 'none');
-        focus.append('circle').attr('r', 5).attr('class', 'circle');
+        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 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis
+            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];
-            focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`);
-            tooltip.transition().duration(300).style('opacity', 0.9);
-            // TODO: nda - updating the inner html could be deadly cause injection attacks!
-            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 + 12}px,${yScale(d0.y) + 30}px)`);
+            if (!d0) return;
+
+            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 - 25)); // shift x by -25 so that you can reach points on the left-side axis
+            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
             const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
-            this._currSelected = { x: d0.x, y: d0.y, elem: selected };
+            this.setCurrSelected(d0.x, d0.y);
+            this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
         });
 
         svg.append('rect')
@@ -250,17 +277,33 @@ export class LineChart extends React.Component<LineChartProps> {
             .attr('fill', 'none')
             .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
             .style('opacity', 0)
-            .on('mouseover', () => focus.style('display', null))
+            .on('mouseover', () => higlightFocusPt.style('display', null))
             .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
             .on('mousemove', mousemove)
             .on('click', onPointClick);
     };
 
+    private updateTooltip(
+        higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+        xScale: d3.ScaleLinear<number, number, never>,
+        d0: DataPoint,
+        yScale: d3.ScaleLinear<number, number, never>,
+        tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+    ) {
+        higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'focus-selected' : 'focus');
+        tooltip.transition().duration(300).style('opacity', 0.9);
+        // TODO: nda - updating the inner html could be deadly cause injection attacks!
+        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)`);
+    }
+
     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' : `Curr Selected: ${selectedPt}`}</span>
+                <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span>
             </div>
         );
     }
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index adefe90cd..0d69ac890 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,12 +1,19 @@
 import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../../Utils';
+import { AnimationSym, Doc } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
+import { DragManager } from '../../../../util/DragManager';
+import { DocumentView } from '../../DocumentView';
+import { DataVizView } from '../DataVizBox';
 
 interface TableBoxProps {
     pairs: { [key: string]: any }[];
     selectAxes: (axes: string[]) => void;
     axes: string[];
+    docView?: () => DocumentView | undefined;
 }
 
 @observer
@@ -20,39 +27,72 @@ export class TableBox extends React.Component<TableBoxProps> {
                 <table className="table">
                     <thead>
                         <tr className="table-row">
-                            {this.columns.map(col => (
-                                <th
-                                    style={{ color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined, fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal' }}
-                                    onPointerDown={e =>
-                                        setupMoveUpEvents(
-                                            {},
-                                            e,
-                                            returnFalse,
-                                            emptyFunction,
-                                            action(e => {
-                                                const newAxes = this.props.axes;
-                                                if (newAxes.includes(col)) {
-                                                    newAxes.splice(newAxes.indexOf(col), 1);
-                                                } else if (newAxes.length >= 1) {
-                                                    newAxes[1] = col;
-                                                } else {
-                                                    newAxes[0] = col;
-                                                }
-                                                this.props.selectAxes(newAxes);
-                                            })
-                                        )
-                                    }>
-                                    {col}
-                                </th>
-                            ))}
+                            {this.columns
+                                .filter(col => !col.startsWith('select'))
+                                .map(col => {
+                                    const header = React.createRef<HTMLElement>();
+                                    return (
+                                        <th
+                                            ref={header as any}
+                                            style={{
+                                                color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined,
+                                                fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal',
+                                            }}
+                                            onPointerDown={e => {
+                                                const downX = e.clientX;
+                                                const downY = e.clientY;
+                                                setupMoveUpEvents(
+                                                    {},
+                                                    e,
+                                                    e => {
+                                                        const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
+                                                        const targetCreator = (annotationOn: Doc | undefined) => {
+                                                            const alias = Doc.MakeAlias(this.props.docView?.()!.rootDoc!);
+                                                            alias._dataVizView = DataVizView.LINECHART;
+                                                            alias._dataVizAxes = new List<string>([col, col]);
+                                                            alias.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!;
+                                                            return alias;
+                                                        };
+                                                        if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+                                                            DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
+                                                                dragComplete: e => {
+                                                                    if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+                                                                        e.linkDocument.linkDisplay = true;
+                                                                        // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+                                                                        // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+                                                                    }
+                                                                },
+                                                            });
+                                                            return true;
+                                                        }
+                                                        return false;
+                                                    },
+                                                    emptyFunction,
+                                                    action(e => {
+                                                        const newAxes = this.props.axes;
+                                                        if (newAxes.includes(col)) {
+                                                            newAxes.splice(newAxes.indexOf(col), 1);
+                                                        } else if (newAxes.length >= 1) {
+                                                            newAxes[1] = col;
+                                                        } else {
+                                                            newAxes[0] = col;
+                                                        }
+                                                        this.props.selectAxes(newAxes);
+                                                    })
+                                                );
+                                            }}>
+                                            {col}
+                                        </th>
+                                    );
+                                })}
                         </tr>
                     </thead>
                     <tbody>
-                        {this.props.pairs?.map(p => {
+                        {this.props.pairs?.map((p, i) => {
                             return (
-                                <tr className="table-row">
+                                <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}>
                                     {this.columns.map(col => (
-                                        <td>{p[col]}</td>
+                                        <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td>
                                     ))}
                                 </tr>
                             );
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index cc024d83a..e89f5db52 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1335,6 +1335,7 @@ export namespace Doc {
     }
 
     export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
+        if (linkDoc.anchor2 === anchorDoc || (linkDoc.anchor2 as Doc).annotationOn) return '2';
         return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2';
     }
 
-- 
cgit v1.2.3-70-g09d2

