diff options
Diffstat (limited to 'src')
16 files changed, 716 insertions, 488 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 4751acf44..69407d6c2 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx deleted file mode 100644 index df4c8f937..000000000 --- a/src/client/views/nodes/DataViz.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import './DataViz.scss'; -import { FieldView, FieldViewProps } from './FieldView'; - -@observer -export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DataVizBox, fieldKey); - } - - render() { - return ( - <div> - <div>Hi</div> - </div> - ); - } -} diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx new file mode 100644 index 000000000..c229e5471 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/ChartBox.tsx @@ -0,0 +1,144 @@ +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'; + +export interface ChartBoxProps { + rootDoc: Doc; + dataDoc: Doc; + 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(); + // setting timeout to allow rerender cycle of the actual chart tof inish + // setTimeout(() => { + this.props.setChartBox(this); + // }); + // 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(); + }; + + scrollFocus(doc: Doc, smooth: boolean): number | undefined { + // if x and y exist on this + // then highlight/focus the x and y position (datapoint) + // highlight for a few seconds and then remove (set a timer to unhighlight after a period of time, + // to be consistent, use 5 seconds and highlight color is orange) + // if x and y don't exist => link to whole doc => dash will take care of focusing on the entire doc + // return value is how long this action takes + // undefined => immediately, 0 => introduces a timeout + // number in ms => won't do any of the focus or call the automated highlighting thing until this timeout expires + // in this case probably number in ms doesn't matter + + // for now could just update the currSelected in linechart to be what doc.x and doc.y + + if (this.currChart && doc.x && doc.y) { + // update curr selected + this.currChart.setCurrSelected(Number(doc.x), Number(doc.y)); + } + return 0; + } + + _getAnchor() { + return this.currChart?._getAnchor(); + } + + render() { + if (this.props.pairs && this._chartData) { + let width = NumCast(this.props.rootDoc._width); + width = width * 0.7; + let height = NumCast(this.props.rootDoc._height); + height = height * 0.7; + console.log(width, height); + const margin = { top: 50, right: 50, bottom: 50, left: 50 }; + return ( + <div> + <div> + {/*{this.props.rootDoc._currChartView == 'line' ? ( + <Line ref={this._chartRef} options={this.options} data={this._chartJsData} onClick={e => this.onClick(e)} /> + ) : ( + <Bar ref={this._chartRef} options={this.options} data={this._chartJsData} onClick={e => this.onClick(e)} /> + )} */} + {/* {this.reLineChart} */} + <LineChart margin={margin} width={width} height={height} chartData={this._chartData} setCurrChart={this.setCurrChart} dataDoc={this.props.dataDoc} fieldKey={'data'} getAnchor={this.props.getAnchor} /> + </div> + <button onClick={e => this.onClickChangeChart(e)} value="line"> + Line + </button> + <button onClick={e => this.onClickChangeChart(e)} value="bar"> + Bar + </button> + </div> + ); + } else { + return <div></div>; + } + } +} diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts new file mode 100644 index 000000000..494242ac5 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/ChartInterface.ts @@ -0,0 +1,36 @@ +import { Doc } from '../../../../fields/Doc'; +import { DataPoint } from './ChartBox'; +import { LineChart } from './components/LineChart'; + +export interface Chart { + tooltipContent: (data: DataPoint) => string; + drawChart: () => void; + height: number; + width: number; + // TODO: nda - probably want to get rid of this to keep charts more generic + _getAnchor: () => Doc; + setCurrSelected: (x: number, y: number) => void; +} + +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 592723ee9..db677ff61 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,90 +1,170 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { StrCast } from "../../../../fields/Types"; -import { ViewBoxBaseComponent } from "../../DocComponent"; -import { FieldViewProps, FieldView } from "../FieldView"; -import "./DataVizBox.scss"; -import { HistogramBox } from "./HistogramBox"; -import { TableBox } from "./TableBox"; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc } from '../../../../fields/Doc'; +import { Cast, 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'; enum DataVizView { - TABLE = "table", - HISTOGRAM= "histogram" + TABLE = 'table', + HISTOGRAM = 'histogram', } - @observer -export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() { - @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}]; +export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + // 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; + // // another way would be store a schema that defines the type of data we are expecting from an imported doc + + // method1() { + // this.pairs[0].x = 3; + // } + + // method() { + // // this.pairs[0].x = 3; + // // go through the pairs + // const x = this.pairs[0].x; + // if (typeof x == 'number') { + // let x1 = Number(x); + // // let x1 = NumCast(x); + // } + // } + + // could use field result + // [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() { if (this.rootDoc._dataVizView) { return StrCast(this.rootDoc._dataVizView); } else { - return "table"; + return 'table'; } } + scrollFocus = (doc: Doc, smooth: boolean) => { + // reconstruct the view based on the anchor -> + // keep track of the datavizbox view and the type of chart we're rendering + + // use dataview to restore part of it and then pass on the rest to the chartbox + + // pseudocode + return this._chartBox?.scrollFocus(doc, smooth); + }; + + getAnchor = () => { + // TODO: nda - look into DocumentButtonBar and DocumentLinksButton + + /* + if nothing selected: + return this.rootDoc + this part does not specify view type (doesn't change type when following the link) + + // this slightly diff than the stuff below: + below part returns an anchor that specifies the viewtype for the whole doc and nothing else + + */ + + // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element) + const anchor = + // this._COMPONENTNAME._getAnchor() ?? + this._chartBox?._getAnchor() ?? + 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; + + this.addDocument(anchor); + return anchor; + // have some other function in code that + }; constructor(props: any) { super(props); if (!this.rootDoc._dataVizView) { // TODO: nda - this might not always want to default to "table" - this.rootDoc._dataVizView = "table"; + this.rootDoc._dataVizView = 'table'; } } - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); } - - @action - private createPairs() { - const xVals: number[] = [0, 1, 2, 3, 4, 5]; - // const yVals: number[] = [10, 20, 30, 40, 50, 60]; - const yVals: number[] = [1, 2, 3, 4, 5, 6]; - let pairs: { - x: number, - y:number - }[] = []; - if (xVals.length != yVals.length) return pairs; - for (let i = 0; i < xVals.length; i++) { - pairs.push({x: xVals[i], y: yVals[i]}); - } - this.pairs = pairs; - return pairs; + 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 (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>) + switch (this.currView) { + case 'table': + return <TableBox pairs={this.pairs} />; + case 'histogram': + return <ChartBox 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}/>) } } @computed get pairVals() { - return this.createPairs(); + return fetch('/csvData?uri=' + this.dataUrl?.url.href).then(res => res.json()); + } + + @computed get dataUrl() { + return Cast(this.dataDoc[this.fieldKey], CsvField); } componentDidMount() { - this.createPairs(); + 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; + }) + ) + ); } // 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.currView == 'table' ? 'histogram' : 'table'; } render() { + if (this.pairs.length == 0) { + return <div>Loading...</div>; + } + return ( <div className="dataViz"> - <button onClick={(e) => this.changeViewHandler(e)}>Change View</button> + <button onClick={e => this.changeViewHandler(e)}>Change View</button> {this.selectView} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts deleted file mode 100644 index 595cecebf..000000000 --- a/src/client/views/nodes/DataVizBox/DrawHelper.ts +++ /dev/null @@ -1,247 +0,0 @@ -export class PIXIPoint { - public get x() { return this.coords[0]; } - public get y() { return this.coords[1]; } - public set x(value: number) { this.coords[0] = value; } - public set y(value: number) { this.coords[1] = value; } - public coords: number[] = [0, 0]; - constructor(x: number, y: number) { - this.coords[0] = x; - this.coords[1] = y; - } -} - -export class PIXIRectangle { - public x: number; - public y: number; - public width: number; - public height: number; - public get left() { return this.x; } - public get right() { return this.x + this.width; } - public get top() { return this.y; } - public get bottom() { return this.top + this.height; } - public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); } - constructor(x: number, y: number, width: number, height: number) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } -} - -export class MathUtil { - - public static EPSILON: number = 0.001; - - public static Sign(value: number): number { - return value >= 0 ? 1 : -1; - } - - public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x += p2.x; - p1.y += p2.y; - return p1; - } - else { - return new PIXIPoint(p1.x + p2.x, p1.y + p2.y); - } - } - - public static Perp(p1: PIXIPoint): PIXIPoint { - return new PIXIPoint(-p1.y, p1.x); - } - - public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x /= by; - p1.y /= by; - return p1; - } - else { - return new PIXIPoint(p1.x / by, p1.y / by); - } - } - - public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) { - if (inline) { - p1.x *= by; - p1.y *= by; - return p1; - } - else { - return new PIXIPoint(p1.x * by, p1.y * by); - } - } - - public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x -= p2.x; - p1.y -= p2.y; - return p1; - } - else { - return new PIXIPoint(p1.x - p2.x, p1.y - p2.y); - } - } - - public static Area(rect: PIXIRectangle): number { - return rect.width * rect.height; - } - - public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) { - // Return minimum distance between line segment vw and point p - var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt - if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - var dot = MathUtil.Dot( - MathUtil.SubtractPoint(p, v), - MathUtil.SubtractPoint(w, v)) / l2; - var t = Math.max(0, Math.min(1, dot)); - // Projection falls on the segment - var projection = MathUtil.AddPoint(v, - MathUtil.MultiplyConstant( - MathUtil.SubtractPoint(w, v), t)); - return MathUtil.Dist(p, projection); - } - - public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined { - var a1 = pe1.y - ps1.y; - var b1 = ps1.x - pe1.x; - - var a2 = pe2.y - ps2.y; - var b2 = ps2.x - pe2.x; - - var delta = a1 * b2 - a2 * b1; - if (delta === 0) { - return undefined; - } - var c2 = a2 * ps2.x + b2 * ps2.y; - var c1 = a1 * ps1.x + b1 * ps1.y; - var invdelta = 1 / delta; - return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta); - } - - public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean { - if (p.x < rect.left - this.EPSILON) { - return false; - } - if (p.x > rect.right + this.EPSILON) { - return false; - } - if (p.y < rect.top - this.EPSILON) { - return false; - } - if (p.y > rect.bottom + this.EPSILON) { - return false; - } - - return true; - } - - public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> { - var r1 = new PIXIPoint(rect.left, rect.top); - var r2 = new PIXIPoint(rect.right, rect.top); - var r3 = new PIXIPoint(rect.right, rect.bottom); - var r4 = new PIXIPoint(rect.left, rect.bottom); - var ret = new Array<PIXIPoint>(); - var dist = this.Dist(lineFrom, lineTo); - var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - return ret; - } - - public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle { - const left = Math.max(rect1.x, rect2.x); - const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width); - const top = Math.max(rect1.y, rect2.y); - const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height); - return new PIXIRectangle(left, top, right - left, bottom - top); - } - - public static Dist(p1: PIXIPoint, p2: PIXIPoint): number { - return Math.sqrt(MathUtil.DistSquared(p1, p2)); - } - - public static Dot(p1: PIXIPoint, p2: PIXIPoint): number { - return p1.x * p2.x + p1.y * p2.y; - } - - public static Normalize(p1: PIXIPoint) { - var d = this.Length(p1); - return new PIXIPoint(p1.x / d, p1.y / d); - } - - public static Length(p1: PIXIPoint): number { - return Math.sqrt(p1.x * p1.x + p1.y * p1.y); - } - - public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number { - const a = p1.x - p2.x; - const b = p1.y - p2.y; - return (a * a + b * b); - } - - public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean { - return !(r2.x > r1.x + r1.width || - r2.x + r2.width < r1.x || - r2.y > r1.y + r1.height || - r2.y + r2.height < r1.y); - } - - public static ArgMin(temp: number[]): number { - let index = 0; - let value = temp[0]; - for (let i = 1; i < temp.length; i++) { - if (temp[i] < value) { - value = temp[i]; - index = i; - } - } - return index; - } - - public static ArgMax(temp: number[]): number { - let index = 0; - let value = temp[0]; - for (let i = 1; i < temp.length; i++) { - if (temp[i] > value) { - value = temp[i]; - index = i; - } - } - return index; - } - - public static Combinations<T>(chars: T[]) { - let result = new Array<T>(); - let f = (prefix: any, chars: any) => { - for (let i = 0; i < chars.length; i++) { - result.push(prefix.concat(chars[i])); - f(prefix.concat(chars[i]), chars.slice(i + 1)); - } - }; - f([], chars); - return result; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss deleted file mode 100644 index 5aac9dc77..000000000 --- a/src/client/views/nodes/DataVizBox/HistogramBox.scss +++ /dev/null @@ -1,18 +0,0 @@ -// change the stroke color of line-svg class -.svgLine { - position: absolute; - background: darkGray; - stroke: #000; - stroke-width: 1px; - width:100%; - height:100%; - opacity: 0.4; -} - -.svgContainer { - position: absolute; - top:0; - left:0; - width:100%; - height: 100%; -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx deleted file mode 100644 index 00dc2ef46..000000000 --- a/src/client/views/nodes/DataVizBox/HistogramBox.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc } from "../../../../fields/Doc"; -import { NumCast } from "../../../../fields/Types"; -import "./HistogramBox.scss"; - -interface HistogramBoxProps { - rootDoc: Doc; - pairs: { - x: number, - y: number - }[] -} - - -export class HistogramBox extends React.Component<HistogramBoxProps> { - - private origin = {x: 0.1 * this.width, y: 0.9 * this.height}; - - @computed get width() { - return NumCast(this.props.rootDoc.width); - } - - @computed get height() { - return NumCast(this.props.rootDoc.height); - } - - @computed get x() { - return NumCast(this.props.rootDoc.x); - } - - @computed get y() { - return NumCast(this.props.rootDoc.y); - } - - @computed get generatePoints() { - // evenly distribute points along the x axis - const xVals: number[] = this.props.pairs.map(p => p.x); - const yVals: number[] = this.props.pairs.map(p => p.y); - - const xMin = Math.min(...xVals); - const xMax = Math.max(...xVals); - const yMin = Math.min(...yVals); - const yMax = Math.max(...yVals); - - const xRange = xMax - xMin; - const yRange = yMax - yMin; - - const xScale = this.width / xRange; - const yScale = this.height / yRange; - - const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale; - const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale; - - const points: { - x: number, - y: number - }[] = this.props.pairs.map(p => { - return { - x: (p.x * xScale + xOffset) + this.origin.x, - y: (p.y * yScale + yOffset) - } - }); - - return points; - } - - @computed get generateGraphLine() { - const points = this.generatePoints; - // loop through points and create a line from each point to the next - let lines: { - x1: number, - y1: number, - x2: number, - y2: number - }[] = []; - for (let i = 0; i < points.length - 1; i++) { - lines.push({ - x1: points[i].x, - y1: points[i].y, - x2: points[i + 1].x, - y2: points[i + 1].y - }); - } - // generate array of svg with lines - let svgLines: JSX.Element[] = []; - for (let i = 0; i < lines.length; i++) { - svgLines.push( - <line - className="svgLine" - key={i} - x1={lines[i].x1} - y1={lines[i].y1} - x2={lines[i].x2} - y2={lines[i].y2} - stroke="black" - strokeWidth={2} - /> - ); - } - - let res = []; - for (let i = 0; i < svgLines.length; i++) { - res.push(<svg className="svgContainer">{svgLines[i]}</svg>) - } - return res; - } - - @computed get generateAxes() { - - const xAxis = { - x1: 0.1 * this.width, - x2: 0.9 * this.width, - y1: 0.9 * this.height, - y2: 0.9 * this.height, - }; - - const yAxis = { - x1: 0.1 * this.width, - x2: 0.1 * this.width, - y1: 0.25 * this.height, - y2: 0.9 * this.height, - }; - - - return ( - [ - (<svg className="svgContainer"> - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */} - <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/> - - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */} - </svg>), - ( - <svg className="svgContainer"> - <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} /> - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */} - </svg>) - ] - ) - } - - - render() { - return ( - <div>histogram box - {/* <svg className="svgContainer"> - {this.generateSVGLine} - </svg> */} - {this.generateAxes[0]} - {this.generateAxes[1]} - {this.generateGraphLine.map(line => line)} - </div> - ) - - } - -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss new file mode 100644 index 000000000..7792a2758 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -0,0 +1,10 @@ +.tooltip { + // make the height width bigger + width: 50px; + height: 50px; +} + +.highlight { + // change the color of the circle element to be red + fill: red; +} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx new file mode 100644 index 000000000..d893b3e12 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -0,0 +1,290 @@ +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'; + +type minMaxRange = { + xMin: number | undefined; + xMax: number | undefined; + yMin: number | undefined; + yMax: number | undefined; +}; + +interface SelectedDataPoint { + x: number; + y: number; + elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; +} + +@observer +export class LineChart extends React.Component<ChartProps> implements Chart { + private _dataReactionDisposer: IReactionDisposer | undefined = undefined; + private _heightReactionDisposer: IReactionDisposer | undefined = undefined; + private _widthReactionDisposer: IReactionDisposer | undefined; + @observable private _x: number = 0; + @observable private _y: number = 0; + @observable private _currSelected: SelectedDataPoint | undefined = undefined; + // create ref for the div + private _chartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _rangeVals: minMaxRange = { + xMin: undefined, + xMax: undefined, + yMin: undefined, + yMax: undefined, + }; + // 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 + + componentDidMount = () => { + this._dataReactionDisposer = reaction( + () => this.props.chartData, + chartData => { + this._rangeVals = minMaxRange(chartData.data); + this.drawChart(); + }, + { 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( + () => 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)); + }); + // this.drawAnnotations(annotations.x, annotations.y); + }, + { fireImmediately: true } + ); + + this.props.setCurrChart(this); + }; + + // 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 + // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY + // if it exists, then highlight it + // if it doesn't exist, then remove the highlight + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const x = element.getAttribute('data-x'); + const y = element.getAttribute('data-y'); + if (x === dataX.toString() && y === dataY.toString()) { + element.classList.add('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 + } + + _getAnchor() { + // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element) + + // TODO: nda - look at pdfviewer get anchor for args + const doc = Docs.Create.TextanchorDocument({ + /*put in some options*/ + title: 'line doc selection' + this._currSelected?.x, + }); + // access curr selected from the charts + doc.x = this._currSelected?.x; + doc.y = this._currSelected?.y; + doc.chartType = 'line'; + return doc; + // 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>x: ${data.x} y: ${data.y}</b>`; + } + + @computed get height(): number { + return this.props.height - this.props.margin.top - this.props.margin.bottom; + } + + @computed get width(): number { + return this.props.width - this.props.margin.left - this.props.margin.right; + } + + setupTooltip() { + const tooltip = d3 + .select(this._chartRef.current) + .append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + .style('background', '#fff') + .style('border', '1px solid #ccc') + .style('padding', '5px') + .style('position', 'absolute') + .style('font-size', '12px'); + return tooltip; + } + + // TODO: nda - use this everyewhere we update currSelected? + @action + setCurrSelected(x: number, y: number) { + // TODO: nda - get rid of svg element in the list? + this._currSelected = { x: x, y: y }; + } + + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { + if (this._chartSvg) { + const circleClass = '.circle-' + idx; + this._chartSvg + .selectAll(circleClass) + .data(data) + .join('circle') // enter append + .attr('class', `${circleClass} datapoint`) + .attr('r', '3') // radius + .attr('cx', d => xScale(d.x)) + .attr('cy', d => yScale(d.y)) + .attr('data-x', d => d.x) + .attr('data-y', d => d.y); + } + } + + // TODO: nda - can use d3.create() to create html element instead of appending + drawChart() { + const { chartData, margin } = this.props; + const data = chartData.data[0]; + // 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 + + 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 + return; + } + + // creating the x and y scales + 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); + + // create x and y grids + xGrid(svg.append('g'), this.height, xScale); + yGrid(svg.append('g'), this.width, yScale); + xAxisCreator(svg.append('g'), this.height, xScale); + yAxisCreator(svg.append('g'), this.width, yScale); + + // draw the line + drawLine(svg.append('path'), data, lineGen); + + // 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 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)); + 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)).style('transform', `translate(${xScale(d0.x) - (this.width + margin.left + margin.right) + 30}px,${yScale(d0.y) + 30}px)`); + }); + + 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)); + const d0 = data[x0]; + this._x = d0.x; + this._y = d0.y; + // 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 }; + console.log('Getting here'); + // this.drawAnnotations(this._x, this._y); + // this.props.getAnchor(); + console.log(this._currSelected); + }); + + this._chartSvg + .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('mousemove', mousemove) + .on('click', onPointClick); + } + + render() { + return ( + <div ref={this._chartRef} 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/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss index 1264d6a46..1264d6a46 100644 --- a/src/client/views/nodes/DataVizBox/TableBox.scss +++ b/src/client/views/nodes/DataVizBox/components/TableBox.scss diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index dfa8262d8..dfa8262d8 100644 --- a/src/client/views/nodes/DataVizBox/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts new file mode 100644 index 000000000..2bb091999 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -0,0 +1,67 @@ +import * as d3 from 'd3'; +import { DataPoint } from '../ChartBox'; + +// TODO: nda - implement function that can handle range for strings + +export const minMaxRange = (dataPts: DataPoint[][]) => { + // find the max and min of all the data points + const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y))); + const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y))); + + const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x))); + const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x))); + + return { xMin, xMax, yMin, yMax }; +}; + +export const scaleCreatorCategorical = (labels: string[], range: number[]) => { + const scale = d3.scaleBand().domain(labels).range(range); + + return scale; +}; + +export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => { + return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]); +}; + +export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => { + // TODO: nda - look into the different types of curves + return d3 + .line<DataPoint>() + .x(d => xScale(d.x)) + .y(d => yScale(d.y)) + .curve(d3.curveMonotoneX); +}; + +export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { + console.log('x axis creator being called'); + g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); +}; + +export const yAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, marginLeft: number, yScale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'y-axis').call(d3.axisLeft(yScale)); +}; + +export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, scale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'xGrid') + .attr('transform', `translate(0,${height})`) + .call( + d3 + .axisBottom(scale) + .tickSize(-height) + .tickFormat((a, b) => '') + ); +}; + +export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, width: number, scale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'yGrid').call( + d3 + .axisLeft(scale) + .tickSize(-width) + .tickFormat((a, b) => '') + ); +}; + +export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => { + p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen); +}; diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts new file mode 100644 index 000000000..0d43130d1 --- /dev/null +++ b/src/server/ApiManagers/DataVizManager.ts @@ -0,0 +1,26 @@ +import { csvParser, csvToString } from "../DataVizUtils"; +import { Method, _success } from "../RouteManager"; +import ApiManager, { Registration } from "./ApiManager"; +import { Directory, serverPathToFile } from "./UploadManager"; +import * as path from 'path'; + +export default class DataVizManager extends ApiManager { + protected initialize(register: Registration): void { + register({ + method: Method.GET, + subscription: "/csvData", + secureHandler: async ({ req, res }) => { + const uri = req.query.uri as string; + + return new Promise<void>(resolve => { + const name = path.basename(uri); + const sPath = serverPathToFile(Directory.csv, name); + const parsedCsv = csvParser(csvToString(sPath)); + _success(res, parsedCsv); + resolve(); + }); + } + }); + } + +}
\ No newline at end of file diff --git a/src/server/DataVizUtils.ts b/src/server/DataVizUtils.ts index 4fd0ca6ff..2528fb1ab 100644 --- a/src/server/DataVizUtils.ts +++ b/src/server/DataVizUtils.ts @@ -1,3 +1,5 @@ +import { readFileSync } from "fs"; + export function csvParser(csv: string) { const lines = csv.split("\n"); const headers = lines[0].split(","); @@ -10,4 +12,8 @@ export function csvParser(csv: string) { return obj; }); return data; +} + +export function csvToString(path: string) { + return readFileSync(path, 'utf8'); }
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 6e6bde3cb..6562860fe 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,6 +4,7 @@ import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; import * as qs from 'query-string'; import { log_execution } from './ActionUtilities'; +import DataVizManager from './ApiManagers/DataVizManager'; import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager'; @@ -62,7 +63,19 @@ async function preliminaryFunctions() { * with the server */ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) { - const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new PDFManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), new GooglePhotosManager()]; + const managers = [ + new SessionManager(), + new UserManager(), + new UploadManager(), + new DownloadManager(), + new SearchManager(), + new PDFManager(), + new DeleteManager(), + new UtilManager(), + new GeneralGoogleManager(), + new GooglePhotosManager(), + new DataVizManager(), + ]; // initialize API Managers console.log(yellow('\nregistering server routes...')); |