aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx11
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss20
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx105
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx98
-rw-r--r--src/fields/Doc.ts1
5 files changed, 170 insertions, 65 deletions
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';
}