aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2023-04-05 22:44:38 -0400
committerbobzel <zzzman@gmail.com>2023-04-05 22:44:38 -0400
commit99fba6d6e99da0154d40d2e3e87e120d8e6f2810 (patch)
tree28002db4cc426ba68da4bc1b837fe21e16b1e27b /src
parent9b41da1af16b982ee8ac2fc09f2f8b5d67eac9fb (diff)
fixed up dataviz to work again. fixed point selection, tooltips, making and following links
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts2
-rw-r--r--src/client/views/nodes/DataVizBox/ChartBox.tsx62
-rw-r--r--src/client/views/nodes/DataVizBox/ChartInterface.ts6
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx63
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss9
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx41
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx9
-rw-r--r--src/server/DataVizUtils.ts22
11 files changed, 98 insertions, 125 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c4014a752..1f09cdd3d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1148,8 +1148,8 @@ export namespace Docs {
return Prototypes.get(DocumentType.PRESELEMENT);
}
- export function DataVizDocument(url: string, options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options });
+ export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) {
+ return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc);
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9aceed366..623aaf357 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -273,7 +273,7 @@ export class CurrentUserUtils {
{key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
{key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
- // {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
+ {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, treeViewHideUnrendered: true}},
{key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _fitWidth: true, _backgroundGridShow: true, }},
diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx
index c229e5471..ee577b9fd 100644
--- a/src/client/views/nodes/DataVizBox/ChartBox.tsx
+++ b/src/client/views/nodes/DataVizBox/ChartBox.tsx
@@ -5,10 +5,12 @@ import { action, computed, observable } from 'mobx';
import { NumCast, StrCast } from '../../../../fields/Types';
import { LineChart } from './components/LineChart';
import { Chart, ChartData } from './ChartInterface';
+import { PinProps } from '../trails';
export interface ChartBoxProps {
rootDoc: Doc;
dataDoc: Doc;
+ height: number;
pairs: { x: number; y: number }[];
setChartBox: (chartBox: ChartBox) => void;
getAnchor: () => Doc;
@@ -71,11 +73,7 @@ export class ChartBox extends React.Component<ChartBoxProps> {
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
@@ -86,59 +84,17 @@ export class ChartBox extends React.Component<ChartBoxProps> {
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
+ getAnchor = (pinProps?: PinProps) => this.currChart?._getAnchor(pinProps);
- // 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();
- }
+ restoreView = (data: Doc) => this.currChart?.restoreView(data);
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>;
+ const width = NumCast(this.props.rootDoc._width) * 0.9;
+ const height = this.props.height * 0.9;
+ const margin = { top: 10, right: 50, bottom: 50, left: 50 };
+ return <LineChart margin={margin} width={width} height={height} chartData={this._chartData} setCurrChart={this.setCurrChart} dataDoc={this.props.dataDoc} fieldKey={'data'} getAnchor={this.props.getAnchor} />;
}
+ return <div />;
}
}
diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts
index 494242ac5..8ebcc2a5f 100644
--- a/src/client/views/nodes/DataVizBox/ChartInterface.ts
+++ b/src/client/views/nodes/DataVizBox/ChartInterface.ts
@@ -1,15 +1,15 @@
import { Doc } from '../../../../fields/Doc';
+import { PinProps } from '../trails';
import { DataPoint } from './ChartBox';
-import { LineChart } from './components/LineChart';
export interface Chart {
tooltipContent: (data: DataPoint) => string;
drawChart: () => void;
+ restoreView: (data: any) => boolean;
height: number;
width: number;
// TODO: nda - probably want to get rid of this to keep charts more generic
- _getAnchor: () => Doc;
- setCurrSelected: (x: number, y: number) => void;
+ _getAnchor: (pinProps?: PinProps) => Doc;
}
export interface ChartProps {
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 db677ff61..68766e8dc 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -2,7 +2,7 @@ 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 { BoolCast, Cast, StrCast } from '../../../../fields/Types';
import { CsvField } from '../../../../fields/URLField';
import { Docs } from '../../../documents/Documents';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
@@ -10,6 +10,7 @@ import { FieldViewProps, FieldView } from '../FieldView';
import { ChartBox } from './ChartBox';
import './DataVizBox.scss';
import { TableBox } from './components/TableBox';
+import { PinProps } from '../trails';
enum DataVizView {
TABLE = 'table',
@@ -54,40 +55,23 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
@computed get currView() {
- if (this.rootDoc._dataVizView) {
- return StrCast(this.rootDoc._dataVizView);
- } else {
- return 'table';
- }
+ return StrCast(this.rootDoc._dataVizView, '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);
+ @action
+ restoreView = (data: Doc) => {
+ const changed = this.currView !== data.dataView && (this.rootDoc._dataVizView = data.dataView);
+ const func = () => this._chartBox?.restoreView(data);
+ if (changed) {
+ setTimeout(func);
+ return changed ? true : false;
+ }
+ return func() ?? false;
};
- 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)
+ getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
const anchor =
- // this._COMPONENTNAME._getAnchor() ??
- this._chartBox?._getAnchor() ??
+ this._chartBox?.getAnchor(pinProps) ??
Docs.Create.TextanchorDocument({
// when we clear selection -> we should have it so chartBox getAnchor returns undefined
// this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
@@ -98,7 +82,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.addDocument(anchor);
return anchor;
- // have some other function in code that
};
constructor(props: any) {
super(props);
@@ -117,7 +100,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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} />;
+ return <ChartBox height={this.props.PanelHeight() - 32 /* height of 'change view' button */} getAnchor={this.getAnchor} setChartBox={this.setChartBox} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />;
// case "histogram":
// return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
}
@@ -152,7 +135,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@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() {
@@ -161,7 +144,19 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
return (
- <div className="dataViz">
+ <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)}>Change View</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 7792a2758..2d6c5f0f2 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -1,10 +1,15 @@
.tooltip {
// make the height width bigger
- width: 50px;
- height: 50px;
+ width: fit-content;
+ height: fit-content;
}
.highlight {
// change the color of the circle element to be red
fill: red;
}
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index d893b3e12..e5f7dc4f4 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -9,6 +9,10 @@ import { Docs } from '../../../../documents/Documents';
import { Doc, DocListCast } from '../../../../../fields/Doc';
import { Chart, ChartProps } from '../ChartInterface';
import './Chart.scss';
+import { List } from '../../../../../fields/List';
+import { PinProps, PresBox } from '../../trails';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast } from '../../../../../fields/Types';
type minMaxRange = {
xMin: number | undefined;
@@ -103,21 +107,25 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
// loop through and remove any annotations that no longer exist
}
- _getAnchor() {
+ restoreView = (data: Doc) => {
+ const coords = Cast(data.presDataViz, listSpec('number'), null);
+ if ((coords && this._currSelected?.x !== coords[0]) || this._currSelected?.y !== coords[1]) {
+ this.setCurrSelected(coords[0], coords[1]);
+ return true;
+ }
+ return false;
+ };
+ _getAnchor = (pinProps?: PinProps) => {
// store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element)
- // TODO: nda - look at pdfviewer get anchor for args
- const doc = Docs.Create.TextanchorDocument({
+ const anchor = 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;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), dataviz: this._currSelected ? [this._currSelected.x, this._currSelected.y] : undefined } }, this.props.dataDoc);
+ return anchor;
// have some other function in code that
- }
+ };
componentWillUnmount() {
if (this._dataReactionDisposer) {
@@ -132,7 +140,7 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
}
tooltipContent(data: DataPoint) {
- return `<b>x: ${data.x} y: ${data.y}</b>`;
+ return `<b>(${data.x},${data.y})</b>`;
}
@computed get height(): number {
@@ -235,7 +243,7 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
const mousemove = action((e: any) => {
const bisect = d3.bisector((d: DataPoint) => d.x).left;
const xPos = d3.pointer(e)[0];
- const x0 = bisect(data, xScale.invert(xPos));
+ const x0 = bisect(data, xScale.invert(xPos - 25));
const d0 = data[x0];
this._x = d0.x;
this._y = d0.y;
@@ -243,23 +251,22 @@ export class LineChart extends React.Component<ChartProps> implements Chart {
// 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)`);
+ tooltip
+ .html(() => this.tooltipContent(d0))
+ .style('pointer-events', 'none')
+ .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}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 x0 = bisect(data, xScale.invert(xPos - 25));
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
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0e0e22b84..e24bf35f5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -108,6 +108,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p
export interface DocComponentView {
updateIcon?: () => void; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+ restoreView?: (viewSpec: Doc) => boolean;
scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index bfaae8069..0e22ea3b1 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField, InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -39,6 +39,7 @@ const { Howl } = require('howler');
export interface pinDataTypes {
scrollable?: boolean;
+ dataviz?: number[];
pannable?: boolean;
viewType?: boolean;
inkable?: boolean;
@@ -491,6 +492,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
}
+ if (!pinDataTypes && activeItem.presDataViz !== undefined) {
+ if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
+ changed = true;
+ }
+ }
if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
if (bestTarget._scrollTop !== activeItem.presViewScroll) {
@@ -648,6 +654,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType;
if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
+ if (pinProps.pinData.dataviz) pinDoc.presDataViz = new List<number>(pinProps.pinData.dataviz);
if (pinProps.pinData.pannable) {
pinDoc.presPanX = NumCast(targetDoc._panX);
pinDoc.presPanY = NumCast(targetDoc._panY);
diff --git a/src/server/DataVizUtils.ts b/src/server/DataVizUtils.ts
index 2528fb1ab..15f03b319 100644
--- a/src/server/DataVizUtils.ts
+++ b/src/server/DataVizUtils.ts
@@ -1,19 +1,17 @@
-import { readFileSync } from "fs";
+import { readFileSync } from 'fs';
export function csvParser(csv: string) {
- const lines = csv.split("\n");
- const headers = lines[0].split(",");
- const data = lines.slice(1).map(line => {
- const values = line.split(",");
- const obj: any = {};
- for (let i = 0; i < headers.length; i++) {
- obj[headers[i]] = values[i];
- }
- return obj;
- });
+ const lines = csv.split('\n');
+ const headers = lines[0].split(',').map(header => header.trim());
+ const data = lines.slice(1).map(line =>
+ line.split(',').reduce((last, value, i) => {
+ last[headers[i]] = value.trim();
+ return last;
+ }, {} as any)
+ );
return data;
}
export function csvToString(path: string) {
return readFileSync(path, 'utf8');
-} \ No newline at end of file
+}