aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox')
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx181
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss41
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx319
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.scss (renamed from src/client/views/nodes/DataVizBox/TableBox.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx105
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts67
11 files changed, 657 insertions, 521 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..cd500e9ae 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -0,0 +1,4 @@
+.dataviz {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 592723ee9..eb25d3264 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,90 +1,151 @@
-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";
-
-enum DataVizView {
- TABLE = "table",
- HISTOGRAM= "histogram"
-}
+import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+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';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import { LineChart } from './components/LineChart';
+import { TableBox } from './components/TableBox';
+import './DataVizBox.scss';
+export enum DataVizView {
+ TABLE = 'table',
+ LINECHART = 'lineChart',
+}
@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
-
- // TODO: nda - make this use enum values instead
- // @observable private currView: DataVizView = DataVizView.TABLE;
- @computed get currView() {
- if (this.rootDoc._dataVizView) {
- return StrCast(this.rootDoc._dataVizView);
- } else {
- return "table";
- }
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DataVizBox, fieldKey);
}
-
- constructor(props: any) {
- super(props);
- if (!this.rootDoc._dataVizView) {
- // TODO: nda - this might not always want to default to "table"
- this.rootDoc._dataVizView = "table";
- }
+ // 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 }[] = [];
+ static pairSet = new ObservableMap<string, { [key: string]: string }[]>();
+ @computed.struct get pairs() {
+ return DataVizBox.pairSet.get(StrCast(this.rootDoc.fileUpload));
}
+ 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() {
+ // 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 - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+ @computed get dataVizView(): DataVizView {
+ return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
+ }
@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]});
+ restoreView = (data: Doc) => {
+ 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 (changedView || changedAxes) {
+ setTimeout(func, 100);
+ return true;
}
- this.pairs = pairs;
- return pairs;
+ return func() ?? false;
+ };
+
+ getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
+ 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*/
+ });
+
+ 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() {
- switch(this.currView) {
- case "table":
- return (<TableBox pairs={this.pairs} />)
- case "histogram":
- return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ 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: 25, bottom: 50, left:25};
+ if (!this.pairs) return 'no data';
+ // prettier-ignore
+ switch (this.dataVizView) {
+ 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} />;
}
}
-
- @computed get pairVals() {
- return this.createPairs();
+ @computed get dataUrl() {
+ return Cast(this.dataDoc[this.fieldKey], CsvField);
}
componentDidMount() {
- this.createPairs();
+ this.props.setContentView?.(this);
+ this.fetchData();
+ }
+
+ fetchData() {
+ if (DataVizBox.pairSet.has(StrCast(this.rootDoc.fileUpload))) return;
+ DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), []);
+ fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+ .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), 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.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
}
render() {
- return (
- <div className="dataViz">
- <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ return !this.pairs?.length ? (
+ <div>Loading...</div>
+ ) : (
+ <div
+ className="dataViz"
+ onWheel={e => e.stopPropagation()}
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+ (e: WheelEvent) => {
+ if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ },
+ { passive: false }
+ )
+ }>
+ <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</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/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
deleted file mode 100644
index dfa8262d8..000000000
--- a/src/client/views/nodes/DataVizBox/TableBox.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-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
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..d4f7bfb32
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -0,0 +1,41 @@
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+
+ .tooltip {
+ // make the height width bigger
+ width: fit-content;
+ height: fit-content;
+ }
+
+ .hoverHighlight-selected,
+ .selected {
+ // change the color of the circle element to be red
+ fill: transparent;
+ outline: red solid 2px;
+ border-radius: 100%;
+ position: absolute;
+ transform-box: fill-box;
+ transform-origin: center;
+ }
+ .hoverHighlight {
+ fill: transparent;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .hoverHighlight-selected {
+ fill: transparent;
+ scale: 1;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .datapoint {
+ fill: black;
+ }
+ .brushed {
+ // 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..777bf2f66
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -0,0 +1,319 @@
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+// import d3
+import * as d3 from 'd3';
+import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { LinkManager } from '../../../../util/LinkManager';
+import { PinProps, PresBox } from '../../trails';
+import { DataVizBox } from '../DataVizBox';
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
+import './Chart.scss';
+
+export interface DataPoint {
+ x: number;
+ y: number;
+}
+interface SelectedDataPoint extends DataPoint {
+ elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
+}
+export interface LineChartProps {
+ rootDoc: Doc;
+ axes: string[];
+ pairs: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+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;
+ @observable _currSelected: SelectedDataPoint | undefined = undefined;
+ // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+
+ @computed get _lineChartData() {
+ if (this.props.axes.length <= 1) return [];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .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));
+ }
+ @computed get incomingLinks() {
+ return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ .filter(link => link.anchor1 !== this.props.rootDoc) // get links where this chart doc is the target of the link
+ .map(link => DocCast(link.anchor1)); // then return the source of the link
+ }
+ @computed get incomingSelected() {
+ return this.incomingLinks // all links 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();
+ }
+ @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
+ return minMaxRange([this._lineChartData]);
+ }
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet) {
+ this.drawChart([dataSet], this.rangeVals, w, h);
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true);
+ this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.annos = reaction(
+ () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']),
+ annotations => {
+ // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
+ // could be blue colored to make it look like anchor
+ // this.drawAnnotations()
+ // loop through annotations and draw them
+ annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
+ // this.drawAnnotations(annotations.x, annotations.y);
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.highlights = reaction(
+ () => ({
+ selected: this._currSelected,
+ incomingSelected: this.incomingSelected,
+ }),
+ ({ selected, incomingSelected }) => {
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
+ incomingSelected?.forEach((pair: any) => 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
+
+ clearAnnotations = () => {
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ element.classList.remove('brushed');
+ element.classList.remove('selected');
+ }
+ };
+ // gets called whenever the "data-annotations" fields gets updated
+ drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => {
+ // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
+ // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY
+ // if it exists, then highlight it
+ // if it doesn't exist, then remove the highlight
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ const x = element.getAttribute('data-x');
+ const y = element.getAttribute('data-y');
+ if (x === dataX.toString() && y === dataY.toString()) {
+ element.classList.add(selected ? 'selected' : 'brushed');
+ }
+ // TODO: nda - this remove highlight code should go where we remove the links
+ // } else {
+ // }
+ }
+ };
+
+ removeAnnotations(dataX: number, dataY: number) {
+ // loop through and remove any annotations that no longer exist
+ }
+
+ @action
+ restoreView = (data: Doc) => {
+ 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.setCurrSelected();
+ return true;
+ }
+ return false;
+ };
+
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.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;
+ };
+
+ @computed get height() {
+ return this.props.height - this.props.margin.top - this.props.margin.bottom;
+ }
+
+ @computed get width() {
+ return this.props.width - this.props.margin.left - this.props.margin.right;
+ }
+
+ setupTooltip() {
+ return d3
+ .select(this._lineChartRef.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');
+ }
+
+ // 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 !== 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>) {
+ if (this._lineChartSvg) {
+ const circleClass = '.circle-' + idx;
+ this._lineChartSvg
+ .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 = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ // clearing tooltip and the current chart
+ d3.select(this._lineChartRef.current).select('svg').remove();
+ d3.select(this._lineChartRef.current).select('.tooltip').remove();
+
+ const { xMin, xMax, yMin, yMax } = rangeVals;
+ if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
+ return;
+ }
+
+ // creating the x and y scales
+ const xScale = scaleCreatorNumerical(xMin, xMax, 0, width);
+ const yScale = scaleCreatorNumerical(0, yMax,height, 0);
+
+ // adding svg
+ const margin = this.props.margin;
+ const svg = (this._lineChartSvg = d3
+ .select(this._lineChartRef.current)
+ .append('svg')
+ .attr('width', `${width +margin.left + margin.right}`)
+ .attr('height', `${height + margin.top + margin.bottom }`)
+ .append('g')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`));
+
+ // create x and y grids
+ xGrid(svg.append('g'), height, xScale);
+ yGrid(svg.append('g'), width, yScale);
+ xAxisCreator(svg.append('g'), height, xScale);
+ yAxisCreator(svg.append('g'), width, yScale);
+
+ // draw the plot line
+ const data = dataSet[0];
+ const lineGen = createLineGenerator(xScale, yScale);
+ drawLine(svg.append('path'), data, lineGen);
+
+ // draw the datapoint circle
+ this.drawDataPoints(data, 0, xScale, yScale);
+
+ const higlightFocusPt = svg.append('g').style('display', 'none');
+ higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
+ const tooltip = this.setupTooltip();
+ // add all the tooltipContent to the tooltip
+ const mousemove = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ if (!d0) 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 - 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.setCurrSelected(d0.x, d0.y);
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ svg.append('rect')
+ .attr('class', 'overlay')
+ .attr('width', width)
+ .attr('height', this.height + margin.top + margin.bottom)
+ .attr('fill', 'none')
+ .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
+ .style('opacity', 0)
+ .on('mouseover', () => higlightFocusPt.style('display', null))
+ .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
+ .on('mousemove', mousemove)
+ .on('click', onPointClick);
+ };
+
+ 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 ? 'hoverHighlight-selected' : 'hoverHighlight');
+ 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' : `Selected: ${selectedPt}`}</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/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
new file mode 100644
index 000000000..0d69ac890
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -0,0 +1,105 @@
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+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
+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">
+ {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, i) => {
+ return (
+ <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 style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td>
+ ))}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
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..e1ff6f8eb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -0,0 +1,67 @@
+import * as d3 from 'd3';
+import { DataPoint } from '../components/LineChart';
+
+// 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);
+};