From ba8cb4194062b0f939afd136a269625f9f26dcaa Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Mon, 25 Jul 2022 19:10:55 -0400 Subject: cleaned up code and moved files around --- .../nodes/DataVizBox/components/LineChart.tsx | 270 +++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/client/views/nodes/DataVizBox/components/LineChart.tsx (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') 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..88598be3d --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -0,0 +1,270 @@ +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; +} + +@observer +export class LineChart extends React.Component 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 = React.createRef(); + private _rangeVals: minMaxRange = { + xMin: undefined, + xMax: undefined, + yMin: undefined, + yMax: undefined, + }; + + private _chartSvg: d3.Selection | 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 } + ); + 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 + this.drawAnnotations(this.props.chartData.data[0][1].x, this.props.chartData.data[0][1].y); + }, + { fireImmediately: true } + ); + + setTimeout(() => { + 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'); + } else { + element.classList.remove('highlight'); + } + } + } + + _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*/ + }); + // 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 `x: ${data.x} y: ${data.y}`; + } + + @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; + } + + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear, yScale: d3.ScaleLinear) { + 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('.circle-d1').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'); + setTimeout(() => this.props.getAnchor()); + // 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 ( +
+ Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'} +
+ ); + } +} -- cgit v1.2.3-70-g09d2 From 50797032a67c6e3920edd8ab38e6453a17674cb8 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Wed, 27 Jul 2022 13:57:26 -0400 Subject: this.addDocument does not exist --- src/client/views/nodes/DataVizBox/ChartBox.tsx | 10 +++++----- src/client/views/nodes/DataVizBox/ChartInterface.ts | 2 ++ src/client/views/nodes/DataVizBox/DataVizBox.tsx | 6 +++--- src/client/views/nodes/DataVizBox/components/LineChart.tsx | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx index a2e4fd73c..34eee8ee8 100644 --- a/src/client/views/nodes/DataVizBox/ChartBox.tsx +++ b/src/client/views/nodes/DataVizBox/ChartBox.tsx @@ -1,8 +1,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym } from '../../../../fields/Doc'; +import { Doc } from '../../../../fields/Doc'; import { action, computed, observable } from 'mobx'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { NumCast, StrCast } from '../../../../fields/Types'; import { LineChart } from './components/LineChart'; import { Chart, ChartData } from './ChartInterface'; @@ -22,7 +22,7 @@ export interface DataPoint { @observer export class ChartBox extends React.Component { @observable private _chartData: ChartData | undefined = undefined; - @observable private currChart: LineChart | undefined; + private currChart: Chart | undefined; @computed get currView() { if (this.props.rootDoc._dataVizView) { @@ -39,8 +39,8 @@ export class ChartBox extends React.Component { } } - setCurrChart(chart: Chart | undefined) { - // this.currChart = chart; + setCurrChart(chart: Chart) { + this.currChart = chart; } @action diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts index 6e37f966c..05ef35b95 100644 --- a/src/client/views/nodes/DataVizBox/ChartInterface.ts +++ b/src/client/views/nodes/DataVizBox/ChartInterface.ts @@ -7,6 +7,8 @@ export interface Chart { drawChart: () => void; height: number; width: number; + // TODO: nda - probably want to get rid of this to keep charts more generic + _getAnchor: () => Doc; } export interface ChartProps { diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 1d9b309ce..944468a43 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,11 +1,11 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, FieldResult } from '../../../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { Cast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { Docs } from '../../../documents/Documents'; -import { ViewBoxAnnotatableComponent, ViewBoxBaseComponent } from '../../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldViewProps, FieldView } from '../FieldView'; import { ChartBox } from './ChartBox'; import './DataVizBox.scss'; diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 88598be3d..458ff5f23 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -62,7 +62,7 @@ export class LineChart extends React.Component implements Chart { 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(this.props.chartData.data[0][1].x, this.props.chartData.data[0][1].y); + this.drawAnnotations(this.props.chartData.data[0][4].x, this.props.chartData.data[0][4].y); }, { fireImmediately: true } ); @@ -78,7 +78,7 @@ export class LineChart extends React.Component implements Chart { // 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'); + const elements = document.querySelectorAll('.datapoint'); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const x = element.getAttribute('data-x'); @@ -234,7 +234,7 @@ export class LineChart extends React.Component implements Chart { 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('.circle-d1').filter((d: any) => d['data-x'] === d0.x && d['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'); setTimeout(() => this.props.getAnchor()); -- cgit v1.2.3-70-g09d2 From c80d0763c87d1965f451bbd7b31d8da8228dc048 Mon Sep 17 00:00:00 2001 From: Naafiyan Ahmed Date: Wed, 27 Jul 2022 23:04:22 -0400 Subject: got basic scroll focus to work --- src/client/views/nodes/DataVizBox/ChartBox.tsx | 38 +++++++++++++------- .../views/nodes/DataVizBox/ChartInterface.ts | 1 + src/client/views/nodes/DataVizBox/DataVizBox.tsx | 22 +++++++++--- .../nodes/DataVizBox/components/LineChart.tsx | 40 ++++++++++++++++------ 4 files changed, 75 insertions(+), 26 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx index 34eee8ee8..c229e5471 100644 --- a/src/client/views/nodes/DataVizBox/ChartBox.tsx +++ b/src/client/views/nodes/DataVizBox/ChartBox.tsx @@ -39,9 +39,9 @@ export class ChartBox extends React.Component { } } - setCurrChart(chart: Chart) { + setCurrChart = (chart: Chart) => { this.currChart = chart; - } + }; @action generateChartData() { @@ -69,14 +69,14 @@ export class ChartBox extends React.Component { this.props.rootDoc._chartData = JSON.stringify(data); } - componentDidMount() { + componentDidMount = () => { this.generateChartData(); // setting timeout to allow rerender cycle of the actual chart tof inish - setTimeout(() => { - this.props.setChartBox(this); - }); + // setTimeout(() => { + this.props.setChartBox(this); + // }); // this.props.setChartBox(this); - } + }; @action onClickChangeChart = (e: React.MouseEvent) => { @@ -86,11 +86,25 @@ export class ChartBox extends React.Component { this.props.rootDoc._currChartView = e.currentTarget.value.toLowerCase(); }; - scrollFocus(doc: Doc, smooth: boolean) {} - - handleDotClick = (e: any) => { - console.log(e); - }; + 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(); diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts index 05ef35b95..494242ac5 100644 --- a/src/client/views/nodes/DataVizBox/ChartInterface.ts +++ b/src/client/views/nodes/DataVizBox/ChartInterface.ts @@ -9,6 +9,7 @@ export interface Chart { 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 { diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 944468a43..db677ff61 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -68,16 +68,29 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // use dataview to restore part of it and then pass on the rest to the chartbox // pseudocode - setTimeout(() => this._chartBox?.scrollFocus(doc, smooth)); /* smooth parameter true = do animations */ - return 0; + return this._chartBox?.scrollFocus(doc, smooth); }; - getAnchor() { + 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*/ }); @@ -86,7 +99,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { this.addDocument(anchor); return anchor; // have some other function in code that - } + }; constructor(props: any) { super(props); if (!this.rootDoc._dataVizView) { @@ -119,6 +132,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { } componentDidMount() { + this.props.setContentView?.(this); // this.createPairs(); this.fetchData(); } diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 458ff5f23..d893b3e12 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -20,7 +20,7 @@ type minMaxRange = { interface SelectedDataPoint { x: number; y: number; - elem: d3.Selection; + elem?: d3.Selection; } @observer @@ -39,6 +39,7 @@ export class LineChart extends React.Component implements Chart { 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 | undefined; @@ -46,7 +47,7 @@ export class LineChart extends React.Component implements Chart { // write the getanchor function that gets whatever I want as the link anchor - componentDidMount() { + componentDidMount = () => { this._dataReactionDisposer = reaction( () => this.props.chartData, chartData => { @@ -55,6 +56,7 @@ export class LineChart extends React.Component implements Chart { }, { 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( @@ -62,15 +64,19 @@ export class LineChart extends React.Component implements Chart { 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(this.props.chartData.data[0][4].x, this.props.chartData.data[0][4].y); + 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 } ); - setTimeout(() => { - this.props.setCurrChart(this); - }); - } + this.props.setCurrChart(this); + }; // gets called whenever the "data-annotations" fields gets updated drawAnnotations(dataX: number, dataY: number) { @@ -85,18 +91,25 @@ export class LineChart extends React.Component implements Chart { const y = element.getAttribute('data-y'); if (x === dataX.toString() && y === dataY.toString()) { element.classList.add('highlight'); - } else { - element.classList.remove('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; @@ -144,6 +157,13 @@ export class LineChart extends React.Component implements Chart { 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, yScale: d3.ScaleLinear) { if (this._chartSvg) { const circleClass = '.circle-' + idx; @@ -237,7 +257,7 @@ export class LineChart extends React.Component implements Chart { 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'); - setTimeout(() => this.props.getAnchor()); + // this.drawAnnotations(this._x, this._y); // this.props.getAnchor(); console.log(this._currSelected); }); -- cgit v1.2.3-70-g09d2 From 99fba6d6e99da0154d40d2e3e87e120d8e6f2810 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 5 Apr 2023 22:44:38 -0400 Subject: fixed up dataviz to work again. fixed point selection, tooltips, making and following links --- src/client/documents/Documents.ts | 4 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/nodes/DataVizBox/ChartBox.tsx | 62 ++++----------------- .../views/nodes/DataVizBox/ChartInterface.ts | 6 +-- src/client/views/nodes/DataVizBox/DataVizBox.scss | 4 ++ src/client/views/nodes/DataVizBox/DataVizBox.tsx | 63 ++++++++++------------ .../views/nodes/DataVizBox/components/Chart.scss | 9 +++- .../nodes/DataVizBox/components/LineChart.tsx | 41 ++++++++------ src/client/views/nodes/DocumentView.tsx | 1 + src/client/views/nodes/trails/PresBox.tsx | 9 +++- src/server/DataVizUtils.ts | 22 ++++---- 11 files changed, 98 insertions(+), 125 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') 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, 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(["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 { 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 { 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 ( -
-
- {/*{this.props.rootDoc._currChartView == 'line' ? ( - this.onClick(e)} /> - ) : ( - this.onClick(e)} /> - )} */} - {/* {this.reLineChart} */} - -
- - -
- ); - } else { - return
; + 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 ; } + return
; } } 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() { }; @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() { 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() { case 'table': return ; case 'histogram': - return ; + return ; // case "histogram": // return () } @@ -152,7 +135,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { @action changeViewHandler(e: React.MouseEvent) { 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() { } return ( -
+
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 } + ) + }> {this.selectView}
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 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 implements Chart { } tooltipContent(data: DataPoint) { - return `x: ${data.x} y: ${data.y}`; + return `(${data.x},${data.y})`; } @computed get height(): number { @@ -235,7 +243,7 @@ export class LineChart extends React.Component 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 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, props: Opt, 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; // returns the duration of the focus brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; getView?: (doc: Doc) => Promise>; // 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() { 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() { 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(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 +} -- cgit v1.2.3-70-g09d2 From a8343cad405a146fdff8fc2d66ef41fdaefb8bda Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 6 Apr 2023 00:14:24 -0400 Subject: more simplification/cleanup of dataviz --- src/client/views/nodes/DataVizBox/ChartBox.tsx | 100 ----------- .../views/nodes/DataVizBox/ChartInterface.ts | 36 ---- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 84 ++++----- .../nodes/DataVizBox/components/LineChart.tsx | 187 ++++++++++----------- .../views/nodes/DataVizBox/components/TableBox.tsx | 31 ++-- src/client/views/nodes/DataVizBox/utils/D3Utils.ts | 2 +- 6 files changed, 133 insertions(+), 307 deletions(-) delete mode 100644 src/client/views/nodes/DataVizBox/ChartBox.tsx delete mode 100644 src/client/views/nodes/DataVizBox/ChartInterface.ts (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/ChartBox.tsx b/src/client/views/nodes/DataVizBox/ChartBox.tsx deleted file mode 100644 index ee577b9fd..000000000 --- a/src/client/views/nodes/DataVizBox/ChartBox.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../../fields/Doc'; -import { action, computed, observable } from 'mobx'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { LineChart } from './components/LineChart'; -import { Chart, ChartData } from './ChartInterface'; -import { PinProps } from '../trails'; - -export interface ChartBoxProps { - rootDoc: Doc; - dataDoc: Doc; - height: number; - pairs: { x: number; y: number }[]; - setChartBox: (chartBox: ChartBox) => void; - getAnchor: () => Doc; -} - -export interface DataPoint { - x: number; - y: number; -} - -@observer -export class ChartBox extends React.Component { - @observable private _chartData: ChartData | undefined = undefined; - private currChart: Chart | undefined; - - @computed get currView() { - if (this.props.rootDoc._dataVizView) { - return StrCast(this.props.rootDoc._currChartView); - } else { - return 'table'; - } - } - - constructor(props: any) { - super(props); - if (!this.props.rootDoc._currChartView) { - this.props.rootDoc._currChartView = 'bar'; - } - } - - setCurrChart = (chart: Chart) => { - this.currChart = chart; - }; - - @action - generateChartData() { - if (this.props.rootDoc._chartData) { - this._chartData = JSON.parse(StrCast(this.props.rootDoc._chartData)); - return; - } - - const data: ChartData = { - xLabel: '', - yLabel: '', - data: [[]], - }; - - if (this.props.pairs && this.props.pairs.length > 0) { - data.xLabel = 'x'; - data.yLabel = 'y'; - this.props.pairs.forEach(pair => { - // TODO: nda - add actual support for multiple sets of data - data.data[0].push({ x: pair.x, y: pair.y }); - }); - } - - this._chartData = data; - this.props.rootDoc._chartData = JSON.stringify(data); - } - - componentDidMount = () => { - this.generateChartData(); - this.props.setChartBox(this); - }; - - @action - onClickChangeChart = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - console.log(e.currentTarget.value); - this.props.rootDoc._currChartView = e.currentTarget.value.toLowerCase(); - }; - - getAnchor = (pinProps?: PinProps) => this.currChart?._getAnchor(pinProps); - - restoreView = (data: Doc) => this.currChart?.restoreView(data); - - render() { - if (this.props.pairs && this._chartData) { - const width = NumCast(this.props.rootDoc._width) * 0.9; - const height = this.props.height * 0.9; - const margin = { top: 10, right: 50, bottom: 50, left: 50 }; - return ; - } - return
; - } -} diff --git a/src/client/views/nodes/DataVizBox/ChartInterface.ts b/src/client/views/nodes/DataVizBox/ChartInterface.ts deleted file mode 100644 index 8ebcc2a5f..000000000 --- a/src/client/views/nodes/DataVizBox/ChartInterface.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Doc } from '../../../../fields/Doc'; -import { PinProps } from '../trails'; -import { DataPoint } from './ChartBox'; - -export interface Chart { - tooltipContent: (data: DataPoint) => string; - drawChart: () => void; - restoreView: (data: any) => boolean; - height: number; - width: number; - // TODO: nda - probably want to get rid of this to keep charts more generic - _getAnchor: (pinProps?: PinProps) => Doc; -} - -export interface ChartProps { - chartData: ChartData; - width: number; - height: number; - dataDoc: Doc; - fieldKey: string; - // returns linechart component but should be generic chart - setCurrChart: (chart: Chart) => void; - getAnchor: () => Doc; - margin: { - top: number; - right: number; - bottom: number; - left: number; - }; -} - -export interface ChartData { - xLabel: string; - yLabel: string; - data: DataPoint[][]; -} diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 68766e8dc..e279a1262 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -2,29 +2,32 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; -import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; -import { FieldViewProps, FieldView } from '../FieldView'; -import { ChartBox } from './ChartBox'; -import './DataVizBox.scss'; -import { TableBox } from './components/TableBox'; +import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; +import { LineChart } from './components/LineChart'; +import { TableBox } from './components/TableBox'; +import './DataVizBox.scss'; enum DataVizView { TABLE = 'table', - HISTOGRAM = 'histogram', + LINECHART = 'lineChart', } @observer export class DataVizBox extends ViewBoxAnnotatableComponent() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(DataVizBox, fieldKey); + } // says we have an object and any string // 2 ways of doing it // @observable private pairs: { [key: string]: number | string | undefined }[] = []; // @observable private pairs: { [key: string]: FieldResult }[] = []; @observable private pairs: { x: number; y: number }[] = []; - private _chartBox: ChartBox | undefined; + private _chartRenderer: LineChart | undefined; // // another way would be store a schema that defines the type of data we are expecting from an imported doc // method1() { @@ -45,25 +48,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // [key: string]: FieldResult; // instead of numeric x,y in there, - // TODO: nda - make this use enum values instead - // @observable private currView: DataVizView = DataVizView.TABLE; - // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops - setChartBox = (chartBox: ChartBox) => { - this._chartBox = chartBox; - }; - - @computed get currView() { - return StrCast(this.rootDoc._dataVizView, 'table'); + @computed get dataVizView(): DataVizView { + return StrCast(this.rootDoc._dataVizView, 'table') as DataVizView; } @action restoreView = (data: Doc) => { - const changed = this.currView !== data.dataView && (this.rootDoc._dataVizView = data.dataView); - const func = () => this._chartBox?.restoreView(data); + const changed = this.dataVizView !== data.presDataVizView && (this.rootDoc._dataVizView = data.presDataVizView); + const func = () => this._chartRenderer?.restoreView(data); if (changed) { - setTimeout(func); + setTimeout(func, 100); return changed ? true : false; } return func() ?? false; @@ -71,38 +67,27 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = - this._chartBox?.getAnchor(pinProps) ?? + this._chartRenderer?.getAnchor(pinProps) ?? Docs.Create.TextanchorDocument({ // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ }); - anchor.dataView = this.currView; + anchor.presDataVizView = this.dataVizView; this.addDocument(anchor); return anchor; }; - constructor(props: any) { - super(props); - if (!this.rootDoc._dataVizView) { - // TODO: nda - this might not always want to default to "table" - this.rootDoc._dataVizView = 'table'; - } - } - - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DataVizBox, fieldKey); - } @computed get selectView() { - switch (this.currView) { - case 'table': - return ; - case 'histogram': - return ; - // case "histogram": - // return () + const width = NumCast(this.rootDoc._width) * 0.9; + const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; + const margin = { top: 10, right: 50, bottom: 50, left: 50 }; + // prettier-ignore + switch (this.dataVizView) { + case DataVizView.TABLE: return ; + case DataVizView.LINECHART: return (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />; } } @@ -116,34 +101,25 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { componentDidMount() { this.props.setContentView?.(this); - // this.createPairs(); this.fetchData(); } fetchData() { - const uri = this.dataUrl?.url.href; - fetch('/csvData?uri=' + uri).then(res => - res.json().then( - action(res => { - this.pairs = res; - }) - ) - ); + fetch('/csvData?uri=' + this.dataUrl?.url.href) // + .then(res => res.json().then(action(res => !res.errno && (this.pairs = res)))); } // handle changing the view using a button @action changeViewHandler(e: React.MouseEvent) { e.preventDefault(); e.stopPropagation(); - this.rootDoc._dataVizView = this.currView === 'table' ? 'histogram' : 'table'; + this.rootDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; } render() { - if (this.pairs.length == 0) { - return
Loading...
; - } - - return ( + return this.pairs.length == 0 ? ( +
Loading...
+ ) : (
e.stopPropagation()} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index e5f7dc4f4..313164691 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,18 +1,15 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DataPoint } from '../ChartBox'; // import d3 import * as d3 from 'd3'; -import { minMaxRange, createLineGenerator, xGrid, yGrid, drawLine, xAxisCreator, yAxisCreator, scaleCreatorNumerical, scaleCreatorCategorical } from '../utils/D3Utils'; -import { Docs } from '../../../../documents/Documents'; import { Doc, DocListCast } from '../../../../../fields/Doc'; -import { Chart, ChartProps } from '../ChartInterface'; -import './Chart.scss'; -import { List } from '../../../../../fields/List'; -import { PinProps, PresBox } from '../../trails'; import { listSpec } from '../../../../../fields/Schema'; import { Cast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { PinProps, PresBox } from '../../trails'; +import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; +import './Chart.scss'; type minMaxRange = { xMin: number | undefined; @@ -21,22 +18,43 @@ type minMaxRange = { yMax: number | undefined; }; -interface SelectedDataPoint { +export interface DataPoint { x: number; y: number; +} +interface SelectedDataPoint extends DataPoint { elem?: d3.Selection; } +export interface LineChartData { + xLabel: string; + yLabel: string; + dataSet: DataPoint[][]; +} +export interface LineChartProps { + rootDoc: Doc; + pairs: { x: number; y: number }[]; + width: number; + height: number; + dataDoc: Doc; + fieldKey: string; + margin: { + top: number; + right: number; + bottom: number; + left: number; + }; +} @observer -export class LineChart extends React.Component implements Chart { - private _dataReactionDisposer: IReactionDisposer | undefined = undefined; - private _heightReactionDisposer: IReactionDisposer | undefined = undefined; - private _widthReactionDisposer: IReactionDisposer | undefined; +export class LineChart extends React.Component { + private _disposers: { [key: string]: IReactionDisposer } = {}; @observable private _x: number = 0; @observable private _y: number = 0; @observable private _currSelected: SelectedDataPoint | undefined = undefined; + @observable private _lineChartData: LineChartData | undefined = undefined; // create ref for the div - private _chartRef: React.RefObject = React.createRef(); + private _lineChartRef: React.RefObject = React.createRef(); + private _lineChartSvg: d3.Selection | undefined; private _rangeVals: minMaxRange = { xMin: undefined, xMax: undefined, @@ -45,43 +63,47 @@ export class LineChart extends React.Component implements Chart { }; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates - private _chartSvg: d3.Selection | undefined; - - // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that - - // write the getanchor function that gets whatever I want as the link anchor - + componentWillUnmount() { + Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); + } componentDidMount = () => { - this._dataReactionDisposer = reaction( - () => this.props.chartData, - chartData => { - this._rangeVals = minMaxRange(chartData.data); - this.drawChart(); + this._disposers.chartData = reaction( + () => ({ dataSet: this._lineChartData?.dataSet, w: this.props.width, h: this.props.height }), + vals => { + if (vals.dataSet) { + this._rangeVals = minMaxRange(vals.dataSet); + this.drawChart(vals.dataSet); + } }, { fireImmediately: true } ); - // DocumentDecorations.Instance.Interacting - this._heightReactionDisposer = reaction(() => this.props.height, this.drawChart.bind(this)); - this._widthReactionDisposer = reaction(() => this.props.width, this.drawChart.bind(this)); - reaction( + this._disposers.annos = reaction( () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), annotations => { // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way // could be blue colored to make it look like anchor - console.log(annotations); // this.drawAnnotations() // loop through annotations and draw them - annotations.forEach(a => { - this.drawAnnotations(Number(a.x), Number(a.y)); - }); + annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); // this.drawAnnotations(annotations.x, annotations.y); }, { fireImmediately: true } ); - - this.props.setCurrChart(this); + this.generateChartData(); }; + @action + generateChartData() { + this._lineChartData = { + xLabel: 'x', + yLabel: 'y', + // TODO: nda - add actual support for multiple sets of data + dataSet: [this.props.pairs?.map(pair => ({ x: pair.x, y: pair.y }))], + }; + } + + // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that + // gets called whenever the "data-annotations" fields gets updated drawAnnotations(dataX: number, dataY: number) { // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements @@ -115,45 +137,27 @@ export class LineChart extends React.Component implements Chart { } return false; }; - _getAnchor = (pinProps?: PinProps) => { - // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element) + // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) + getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.TextanchorDocument({ - /*put in some options*/ title: 'line doc selection' + this._currSelected?.x, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), dataviz: this._currSelected ? [this._currSelected.x, this._currSelected.y] : undefined } }, this.props.dataDoc); return anchor; - // have some other function in code that }; - componentWillUnmount() { - if (this._dataReactionDisposer) { - this._dataReactionDisposer(); - } - if (this._heightReactionDisposer) { - this._heightReactionDisposer(); - } - if (this._widthReactionDisposer) { - this._widthReactionDisposer(); - } - } - - tooltipContent(data: DataPoint) { - return `(${data.x},${data.y})`; - } - - @computed get height(): number { + @computed get height() { return this.props.height - this.props.margin.top - this.props.margin.bottom; } - @computed get width(): number { + @computed get width() { return this.props.width - this.props.margin.left - this.props.margin.right; } setupTooltip() { const tooltip = d3 - .select(this._chartRef.current) + .select(this._lineChartRef.current) .append('div') .attr('class', 'tooltip') .style('opacity', 0) @@ -169,13 +173,13 @@ export class LineChart extends React.Component implements Chart { @action setCurrSelected(x: number, y: number) { // TODO: nda - get rid of svg element in the list? - this._currSelected = { x: x, y: y }; + this._currSelected = { x, y }; } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear, yScale: d3.ScaleLinear) { - if (this._chartSvg) { + if (this._lineChartSvg) { const circleClass = '.circle-' + idx; - this._chartSvg + this._lineChartSvg .selectAll(circleClass) .data(data) .join('circle') // enter append @@ -189,30 +193,13 @@ export class LineChart extends React.Component implements Chart { } // TODO: nda - can use d3.create() to create html element instead of appending - drawChart() { - const { chartData, margin } = this.props; - const data = chartData.data[0]; + drawChart = (dataSet: DataPoint[][]) => { // clearing tooltip and the current chart - d3.select(this._chartRef.current).select('svg').remove(); - d3.select(this._chartRef.current).select('.tooltip').remove(); - - // TODO: nda - refactor code so that it only recalculates min max and things related to data on data update + d3.select(this._lineChartRef.current).select('svg').remove(); + d3.select(this._lineChartRef.current).select('.tooltip').remove(); const { xMin, xMax, yMin, yMax } = this._rangeVals; - // const svg = d3.select(this._chartRef.current).append(this.svgContainer.html()); - // adding svg - this._chartSvg = d3 - .select(this._chartRef.current) - .append('svg') - .attr('width', `${this.width + margin.right + margin.left}`) - .attr('height', `${this.height + margin.top + margin.bottom}`) - .append('g') - .attr('transform', `translate(${margin.left}, ${margin.top})`); - - const svg = this._chartSvg; - - if (xMin == undefined || xMax == undefined || yMin == undefined || yMax == undefined) { - // TODO: nda - error handle + if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } @@ -220,9 +207,15 @@ export class LineChart extends React.Component implements Chart { const xScale = scaleCreatorNumerical(xMin, xMax, 0, this.width); const yScale = scaleCreatorNumerical(0, yMax, this.height, 0); - // create a line function that takes in the data.data.x and data.data.y - // TODO: nda - fix the types for the d here - const lineGen = createLineGenerator(xScale, yScale); + // adding svg + const margin = this.props.margin; + const svg = (this._lineChartSvg = d3 + .select(this._lineChartRef.current) + .append('svg') + .attr('width', `${this.width + margin.right + margin.left}`) + .attr('height', `${this.height + margin.top + margin.bottom}`) + .append('g') + .attr('transform', `translate(${margin.left}, ${margin.top})`)); // create x and y grids xGrid(svg.append('g'), this.height, xScale); @@ -230,7 +223,9 @@ export class LineChart extends React.Component implements Chart { xAxisCreator(svg.append('g'), this.height, xScale); yAxisCreator(svg.append('g'), this.width, yScale); - // draw the line + // draw the plot line + const data = dataSet[0]; + const lineGen = createLineGenerator(xScale, yScale); drawLine(svg.append('path'), data, lineGen); // draw the datapoint circle @@ -243,16 +238,15 @@ export class LineChart extends React.Component implements Chart { const mousemove = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 25)); + const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis const d0 = data[x0]; this._x = d0.x; this._y = d0.y; focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`); - // TODO: nda - implement tooltips tooltip.transition().duration(300).style('opacity', 0.9); // TODO: nda - updating the inner html could be deadly cause injection attacks! tooltip - .html(() => this.tooltipContent(d0)) + .html(() => `(${d0.x},${d0.y})`) // text content for tooltip .style('pointer-events', 'none') .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}px,${yScale(d0.y) + 30}px)`); }); @@ -260,7 +254,7 @@ export class LineChart extends React.Component implements Chart { const onPointClick = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 25)); + const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis const d0 = data[x0]; this._x = d0.x; this._y = d0.y; @@ -269,27 +263,22 @@ export class LineChart extends React.Component implements Chart { this._currSelected = { x: d0.x, y: d0.y, elem: selected }; }); - this._chartSvg - .append('rect') + svg.append('rect') .attr('class', 'overlay') .attr('width', this.width) .attr('height', this.height + margin.top + margin.bottom) .attr('fill', 'none') .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) .style('opacity', 0) - .on('mouseover', () => { - focus.style('display', null); - }) - .on('mouseout', () => { - tooltip.transition().duration(300).style('opacity', 0); - }) + .on('mouseover', () => focus.style('display', null)) + .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) .on('mousemove', mousemove) .on('click', onPointClick); - } + }; render() { return ( -
+
Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}
); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index dfa8262d8..28114036f 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,16 +1,12 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; interface TableBoxProps { - pairs: {x: number, y:number}[] + pairs: { x: number; y: number }[]; } - export class TableBox extends React.Component { - - - render() { return (
@@ -22,16 +18,17 @@ export class TableBox extends React.Component { - {this.props.pairs.map(p => { - return ( - {p.x} - {p.y} - ) - })} + {this.props.pairs?.map(p => { + return ( + + {p.x} + {p.y} + + ); + })}
- ) + ); } - -} \ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index 2bb091999..e1ff6f8eb 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -1,5 +1,5 @@ import * as d3 from 'd3'; -import { DataPoint } from '../ChartBox'; +import { DataPoint } from '../components/LineChart'; // TODO: nda - implement function that can handle range for strings -- cgit v1.2.3-70-g09d2 From 7975e3f4aaa3601a5512a7dce4c261e7f9411374 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 6 Apr 2023 10:01:39 -0400 Subject: added selection ui for choosing linechart axes. --- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 31 +++++++++------- .../nodes/DataVizBox/components/LineChart.tsx | 33 ++++++++++++----- .../views/nodes/DataVizBox/components/TableBox.tsx | 43 +++++++++++++++++++--- src/client/views/nodes/trails/PresBox.tsx | 7 +--- 4 files changed, 79 insertions(+), 35 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index e279a1262..0ce589a13 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,7 +1,8 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../../fields/Doc'; +import { Doc, StrListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { Docs } from '../../../documents/Documents'; @@ -26,7 +27,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // 2 ways of doing it // @observable private pairs: { [key: string]: number | string | undefined }[] = []; // @observable private pairs: { [key: string]: FieldResult }[] = []; - @observable private pairs: { x: number; y: number }[] = []; + @observable private pairs: { [key: string]: string }[] = []; private _chartRenderer: LineChart | undefined; // // another way would be store a schema that defines the type of data we are expecting from an imported doc @@ -51,16 +52,17 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops @computed get dataVizView(): DataVizView { - return StrCast(this.rootDoc._dataVizView, 'table') as DataVizView; + return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView; } @action restoreView = (data: Doc) => { - const changed = this.dataVizView !== data.presDataVizView && (this.rootDoc._dataVizView = data.presDataVizView); + const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView); + const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._dataVizAxes = new List(StrListCast(data.presDataVizAxes))); const func = () => this._chartRenderer?.restoreView(data); - if (changed) { + if (changedView || changedAxes) { setTimeout(func, 100); - return changed ? true : false; + return true; } return func() ?? false; }; @@ -75,26 +77,27 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { }); anchor.presDataVizView = this.dataVizView; + anchor.presDataVizAxes = this.axes.length ? new List(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(axes)); + @computed get selectView() { const width = NumCast(this.rootDoc._width) * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; const margin = { top: 10, right: 50, bottom: 50, left: 50 }; // prettier-ignore switch (this.dataVizView) { - case DataVizView.TABLE: return ; - case DataVizView.LINECHART: return (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} pairs={this.pairs} dataDoc={this.dataDoc} />; + case DataVizView.TABLE: return ; + case DataVizView.LINECHART: return (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />; } } - - @computed get pairVals() { - return fetch('/csvData?uri=' + this.dataUrl?.url.href).then(res => res.json()); - } - @computed get dataUrl() { return Cast(this.dataDoc[this.fieldKey], CsvField); } @@ -113,7 +116,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { @action changeViewHandler(e: React.MouseEvent) { e.preventDefault(); e.stopPropagation(); - this.rootDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; + this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; } render() { diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 313164691..2357b7c69 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; // import d3 import * as d3 from 'd3'; import { Doc, DocListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; @@ -32,7 +33,8 @@ export interface LineChartData { } export interface LineChartProps { rootDoc: Doc; - pairs: { x: number; y: number }[]; + axes: string[]; + pairs: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -67,8 +69,13 @@ export class LineChart extends React.Component { Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); } componentDidMount = () => { + this._disposers.chartdata = reaction( + () => this.props.axes.slice(), + axes => axes.length > 1 && this.generateChartData(), + { fireImmediately: true } + ); this._disposers.chartData = reaction( - () => ({ dataSet: this._lineChartData?.dataSet, w: this.props.width, h: this.props.height }), + () => ({ dataSet: this._lineChartData?.dataSet, axes: this.props.axes.slice(), w: this.props.width, h: this.props.height }), vals => { if (vals.dataSet) { this._rangeVals = minMaxRange(vals.dataSet); @@ -89,16 +96,15 @@ export class LineChart extends React.Component { }, { fireImmediately: true } ); - this.generateChartData(); }; @action generateChartData() { this._lineChartData = { - xLabel: 'x', - yLabel: 'y', + xLabel: this.props.axes[0], + yLabel: this.props.axes[1], // TODO: nda - add actual support for multiple sets of data - dataSet: [this.props.pairs?.map(pair => ({ x: pair.x, y: pair.y }))], + dataSet: [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))], }; } @@ -129,12 +135,17 @@ export class LineChart extends React.Component { // loop through and remove any annotations that no longer exist } + @action restoreView = (data: Doc) => { - const coords = Cast(data.presDataViz, listSpec('number'), null); - if ((coords && this._currSelected?.x !== coords[0]) || this._currSelected?.y !== coords[1]) { + const coords = Cast(data.presDataVizSelection, listSpec('number'), null); + if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) { this.setCurrSelected(coords[0], coords[1]); return true; } + if (this._currSelected) { + this._currSelected = undefined; + return true; + } return false; }; @@ -143,7 +154,8 @@ export class LineChart extends React.Component { const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), dataviz: this._currSelected ? [this._currSelected.x, this._currSelected.y] : undefined } }, this.props.dataDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); + anchor.presDataVizSelection = this._currSelected ? new List([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; }; @@ -277,9 +289,10 @@ export class LineChart extends React.Component { }; render() { + const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'; return (
- Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'} + {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Curr Selected: ${selectedPt}`}
); } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 28114036f..adefe90cd 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,28 +1,59 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../../Utils'; interface TableBoxProps { - pairs: { x: number; y: number }[]; + pairs: { [key: string]: any }[]; + selectAxes: (axes: string[]) => void; + axes: string[]; } +@observer export class TableBox extends React.Component { + @computed get columns() { + return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : []; + } render() { return (
- - + {this.columns.map(col => ( + + ))} {this.props.pairs?.map(p => { return ( - - + {this.columns.map(col => ( + + ))} ); })} diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 0e22ea3b1..3589a9065 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -492,10 +492,8 @@ export class PresBox extends ViewBoxBaseComponent() { changed = true; } } - if (!pinDataTypes && activeItem.presDataViz !== undefined) { - if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) { - changed = true; - } + if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) { + changed = true; } if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) { @@ -654,7 +652,6 @@ export class PresBox extends ViewBoxBaseComponent() { 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(pinProps.pinData.dataviz); if (pinProps.pinData.pannable) { pinDoc.presPanX = NumCast(targetDoc._panX); pinDoc.presPanY = NumCast(targetDoc._panY); -- cgit v1.2.3-70-g09d2 From e9633592cdd11df4d4d240dc42c254f60d16b572 Mon Sep 17 00:00:00 2001 From: geireann Date: Thu, 6 Apr 2023 11:57:09 -0400 Subject: more cleanup of lineChar --- .../nodes/DataVizBox/components/LineChart.tsx | 46 ++++------------------ 1 file changed, 7 insertions(+), 39 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 2357b7c69..e6a06a454 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -12,13 +12,6 @@ import { PinProps, PresBox } from '../../trails'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; -type minMaxRange = { - xMin: number | undefined; - xMax: number | undefined; - yMin: number | undefined; - yMax: number | undefined; -}; - export interface DataPoint { x: number; y: number; @@ -26,11 +19,6 @@ export interface DataPoint { interface SelectedDataPoint extends DataPoint { elem?: d3.Selection; } -export interface LineChartData { - xLabel: string; - yLabel: string; - dataSet: DataPoint[][]; -} export interface LineChartProps { rootDoc: Doc; axes: string[]; @@ -50,19 +38,11 @@ export interface LineChartProps { @observer export class LineChart extends React.Component { private _disposers: { [key: string]: IReactionDisposer } = {}; - @observable private _x: number = 0; - @observable private _y: number = 0; - @observable private _currSelected: SelectedDataPoint | undefined = undefined; - @observable private _lineChartData: LineChartData | undefined = undefined; - // create ref for the div private _lineChartRef: React.RefObject = React.createRef(); private _lineChartSvg: d3.Selection | undefined; - private _rangeVals: minMaxRange = { - xMin: undefined, - xMax: undefined, - yMin: undefined, - yMax: undefined, - }; + private _rangeVals: { xMin?: number; xMax?: number;yMin?: number;yMax?: number;}= {}; + @observable _currSelected: SelectedDataPoint | undefined = undefined; + @observable _lineChartData: DataPoint[][] | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates componentWillUnmount() { @@ -71,11 +51,13 @@ export class LineChart extends React.Component { componentDidMount = () => { this._disposers.chartdata = reaction( () => this.props.axes.slice(), - axes => axes.length > 1 && this.generateChartData(), + axes => {if (axes.length > 1) { + this._lineChartData = [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })).sort((a,b) => a.x < b.x ? 1:-1)] + }}, { fireImmediately: true } ); this._disposers.chartData = reaction( - () => ({ dataSet: this._lineChartData?.dataSet, axes: this.props.axes.slice(), w: this.props.width, h: this.props.height }), + () => ({ dataSet: this._lineChartData, axes: this.props.axes.slice(), w: this.props.width, h: this.props.height }), vals => { if (vals.dataSet) { this._rangeVals = minMaxRange(vals.dataSet); @@ -98,16 +80,6 @@ export class LineChart extends React.Component { ); }; - @action - generateChartData() { - this._lineChartData = { - xLabel: this.props.axes[0], - yLabel: this.props.axes[1], - // TODO: nda - add actual support for multiple sets of data - dataSet: [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))], - }; - } - // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that // gets called whenever the "data-annotations" fields gets updated @@ -252,8 +224,6 @@ export class LineChart extends React.Component { const xPos = d3.pointer(e)[0]; const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis const d0 = data[x0]; - this._x = d0.x; - this._y = d0.y; focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`); tooltip.transition().duration(300).style('opacity', 0.9); // TODO: nda - updating the inner html could be deadly cause injection attacks! @@ -268,8 +238,6 @@ export class LineChart extends React.Component { const xPos = d3.pointer(e)[0]; const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis const 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 }; -- cgit v1.2.3-70-g09d2 From 986c5bdc899954c4609f71405d3334147be721fe Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Apr 2023 10:42:16 -0400 Subject: experimetnal changes to datavizbox to allow brushing data items and better highlighting of selections. Also working on drawing link lines between chart aliases. --- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 11 ++- .../views/nodes/DataVizBox/components/Chart.scss | 20 ++++ .../nodes/DataVizBox/components/LineChart.tsx | 105 +++++++++++++++------ .../views/nodes/DataVizBox/components/TableBox.tsx | 98 +++++++++++++------ src/fields/Doc.ts | 1 + 5 files changed, 170 insertions(+), 65 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 0ce589a13..aaa8c3c53 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -13,7 +13,7 @@ import { LineChart } from './components/LineChart'; import { TableBox } from './components/TableBox'; import './DataVizBox.scss'; -enum DataVizView { +export enum DataVizView { TABLE = 'table', LINECHART = 'lineChart', } @@ -27,7 +27,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { // 2 ways of doing it // @observable private pairs: { [key: string]: number | string | undefined }[] = []; // @observable private pairs: { [key: string]: FieldResult }[] = []; - @observable private pairs: { [key: string]: string }[] = []; + @observable pairs: { [key: string]: string }[] = []; private _chartRenderer: LineChart | undefined; // // another way would be store a schema that defines the type of data we are expecting from an imported doc @@ -71,6 +71,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { const anchor = this._chartRenderer?.getAnchor(pinProps) ?? Docs.Create.TextanchorDocument({ + unrendered: true, // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ @@ -89,12 +90,12 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List(axes)); @computed get selectView() { - const width = NumCast(this.rootDoc._width) * 0.9; + const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; const margin = { top: 10, right: 50, bottom: 50, left: 50 }; // prettier-ignore switch (this.dataVizView) { - case DataVizView.TABLE: return ; + case DataVizView.TABLE: return ; case DataVizView.LINECHART: return (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />; } } @@ -136,7 +137,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { { passive: false } ) }> - + {this.selectView} ); diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 2d6c5f0f2..c8daf7c90 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -8,8 +8,28 @@ // change the color of the circle element to be red fill: red; } +.focus-selected, +.selected { + // change the color of the circle element to be red + fill: lightblue; + position: absolute; + transform-box: fill-box; + scale: 2; + transform-origin: center; +} +.focus { + fill: transparent; + outline: black solid 1px; + border-radius: 100%; +} +.focus-selected { + scale: 1; + outline: black solid 1px; + border-radius: 100%; +} .chart-container { display: flex; flex-direction: column; align-items: center; + cursor: default; } diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index e6a06a454..6a223e683 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -6,11 +6,15 @@ import * as d3 from 'd3'; import { Doc, DocListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; -import { Cast } from '../../../../../fields/Types'; +import { Cast, DocCast, NumCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { PinProps, PresBox } from '../../trails'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; +import { LinkManager } from '../../../../util/LinkManager'; +import { DocumentManager } from '../../../../util/DocumentManager'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { DataVizBox } from '../DataVizBox'; export interface DataPoint { x: number; @@ -40,7 +44,7 @@ export class LineChart extends React.Component { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject = React.createRef(); private _lineChartSvg: d3.Selection | undefined; - private _rangeVals: { xMin?: number; xMax?: number;yMin?: number;yMax?: number;}= {}; + private _rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number } = {}; @observable _currSelected: SelectedDataPoint | undefined = undefined; @observable _lineChartData: DataPoint[][] | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates @@ -51,9 +55,11 @@ export class LineChart extends React.Component { componentDidMount = () => { this._disposers.chartdata = reaction( () => this.props.axes.slice(), - axes => {if (axes.length > 1) { - this._lineChartData = [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })).sort((a,b) => a.x < b.x ? 1:-1)] - }}, + axes => { + if (axes.length > 1) { + this._lineChartData = [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1))]; + } + }, { fireImmediately: true } ); this._disposers.chartData = reaction( @@ -78,12 +84,38 @@ export class LineChart extends React.Component { }, { fireImmediately: true } ); + this._disposers.highlights = reaction( + () => ({ + selected: this._currSelected, + pairs: LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) + .filter(link => link.anchor1 !== this.props.rootDoc) // all links that are pointing to this node + .map(link => DocCast(link.anchor1)) // get the documents that are pointing to this node + .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes + .filter(dvb => dvb) + .map(dvb => dvb.pairs.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor + .lastElement(), + }), + ({ selected, pairs }) => { + this.clearAnnoations(); + selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); + pairs.forEach(pair => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + }, + { fireImmediately: true } + ); }; // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that + clearAnnoations = () => { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + element.classList.remove('highlight'); + element.classList.remove('selected'); + } + }; // gets called whenever the "data-annotations" fields gets updated - drawAnnotations(dataX: number, dataY: number) { + drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY // if it exists, then highlight it @@ -94,14 +126,13 @@ export class LineChart extends React.Component { const x = element.getAttribute('data-x'); const y = element.getAttribute('data-y'); if (x === dataX.toString() && y === dataY.toString()) { - element.classList.add('highlight'); + element.classList.add(selected ? 'selected' : 'highlight'); } // TODO: nda - this remove highlight code should go where we remove the links // } else { - // element.classList.remove('highlight'); // } } - } + }; removeAnnotations(dataX: number, dataY: number) { // loop through and remove any annotations that no longer exist @@ -115,7 +146,7 @@ export class LineChart extends React.Component { return true; } if (this._currSelected) { - this._currSelected = undefined; + this.setCurrSelected(); return true; } return false; @@ -123,9 +154,7 @@ export class LineChart extends React.Component { // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { - const anchor = Docs.Create.TextanchorDocument({ - title: 'line doc selection' + this._currSelected?.x, - }); + const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); anchor.presDataVizSelection = this._currSelected ? new List([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; @@ -140,7 +169,7 @@ export class LineChart extends React.Component { } setupTooltip() { - const tooltip = d3 + return d3 .select(this._lineChartRef.current) .append('div') .attr('class', 'tooltip') @@ -150,14 +179,15 @@ export class LineChart extends React.Component { .style('padding', '5px') .style('position', 'absolute') .style('font-size', '12px'); - return tooltip; } // TODO: nda - use this everyewhere we update currSelected? @action - setCurrSelected(x: number, y: number) { + setCurrSelected(x?: number, y?: number) { // TODO: nda - get rid of svg element in the list? - this._currSelected = { x, y }; + this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; + this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); + this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear, yScale: d3.ScaleLinear) { @@ -215,32 +245,29 @@ export class LineChart extends React.Component { // draw the datapoint circle this.drawDataPoints(data, 0, xScale, yScale); - const focus = svg.append('g').attr('class', 'focus').style('display', 'none'); - focus.append('circle').attr('r', 5).attr('class', 'circle'); + const higlightFocusPt = svg.append('g').style('display', 'none'); + higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); const tooltip = this.setupTooltip(); // add all the tooltipContent to the tooltip const mousemove = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis + const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; - focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`); - tooltip.transition().duration(300).style('opacity', 0.9); - // TODO: nda - updating the inner html could be deadly cause injection attacks! - tooltip - .html(() => `(${d0.x},${d0.y})`) // text content for tooltip - .style('pointer-events', 'none') - .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}px,${yScale(d0.y) + 30}px)`); + if (!d0) return; + + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); const onPointClick = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 25)); // shift x by -25 so that you can reach points on the left-side axis + const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; // find .circle-d1 with data-x = d0.x and data-y = d0.y const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); - this._currSelected = { x: d0.x, y: d0.y, elem: selected }; + this.setCurrSelected(d0.x, d0.y); + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); svg.append('rect') @@ -250,17 +277,33 @@ export class LineChart extends React.Component { .attr('fill', 'none') .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) .style('opacity', 0) - .on('mouseover', () => focus.style('display', null)) + .on('mouseover', () => higlightFocusPt.style('display', null)) .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) .on('mousemove', mousemove) .on('click', onPointClick); }; + private updateTooltip( + higlightFocusPt: d3.Selection, + xScale: d3.ScaleLinear, + d0: DataPoint, + yScale: d3.ScaleLinear, + tooltip: d3.Selection + ) { + higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'focus-selected' : 'focus'); + tooltip.transition().duration(300).style('opacity', 0.9); + // TODO: nda - updating the inner html could be deadly cause injection attacks! + tooltip + .html(() => `(${d0.x},${d0.y})`) // 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 (
- {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Curr Selected: ${selectedPt}`} + {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}
); } diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index adefe90cd..0d69ac890 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,12 +1,19 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../../Utils'; +import { AnimationSym, Doc } from '../../../../../fields/Doc'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { List } from '../../../../../fields/List'; +import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils'; +import { DragManager } from '../../../../util/DragManager'; +import { DocumentView } from '../../DocumentView'; +import { DataVizView } from '../DataVizBox'; interface TableBoxProps { pairs: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; axes: string[]; + docView?: () => DocumentView | undefined; } @observer @@ -20,39 +27,72 @@ export class TableBox extends React.Component {
xy + setupMoveUpEvents( + {}, + e, + returnFalse, + emptyFunction, + action(e => { + const newAxes = this.props.axes; + if (newAxes.includes(col)) { + newAxes.splice(newAxes.indexOf(col), 1); + } else if (newAxes.length >= 1) { + newAxes[1] = col; + } else { + newAxes[0] = col; + } + this.props.selectAxes(newAxes); + }) + ) + }> + {col} +
{p.x}{p.y}{p[col]}
- {this.columns.map(col => ( - - ))} + {this.columns + .filter(col => !col.startsWith('select')) + .map(col => { + const header = React.createRef(); + return ( + + ); + })} - {this.props.pairs?.map(p => { + {this.props.pairs?.map((p, i) => { return ( - + (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}> {this.columns.map(col => ( - + ))} ); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index cc024d83a..e89f5db52 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1335,6 +1335,7 @@ export namespace Doc { } export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { + if (linkDoc.anchor2 === anchorDoc || (linkDoc.anchor2 as Doc).annotationOn) return '2'; return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2'; } -- cgit v1.2.3-70-g09d2 From 893c48e17c3285613c9c3fa4e84d4d3317ee2772 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 7 Apr 2023 10:57:34 -0400 Subject: updated linechart highlighting --- .../views/nodes/DataVizBox/components/Chart.scss | 23 +++++++++++++--------- .../nodes/DataVizBox/components/LineChart.tsx | 6 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index c8daf7c90..56fcb1fcf 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -4,29 +4,34 @@ height: fit-content; } -.highlight { - // change the color of the circle element to be red - fill: red; -} -.focus-selected, +.hoverHighlight-selected, .selected { // change the color of the circle element to be red - fill: lightblue; + fill: transparent; + outline: lightblue solid 2px; + border-radius: 100%; position: absolute; transform-box: fill-box; - scale: 2; transform-origin: center; } -.focus { +.hoverHighlight { fill: transparent; outline: black solid 1px; border-radius: 100%; } -.focus-selected { +.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; +} .chart-container { display: flex; flex-direction: column; diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 6a223e683..308ef4cba 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -110,7 +110,7 @@ export class LineChart extends React.Component { const elements = document.querySelectorAll('.datapoint'); for (let i = 0; i < elements.length; i++) { const element = elements[i]; - element.classList.remove('highlight'); + element.classList.remove('brushed'); element.classList.remove('selected'); } }; @@ -126,7 +126,7 @@ export class LineChart extends React.Component { const x = element.getAttribute('data-x'); const y = element.getAttribute('data-y'); if (x === dataX.toString() && y === dataY.toString()) { - element.classList.add(selected ? 'selected' : 'highlight'); + element.classList.add(selected ? 'selected' : 'brushed'); } // TODO: nda - this remove highlight code should go where we remove the links // } else { @@ -290,7 +290,7 @@ export class LineChart extends React.Component { yScale: d3.ScaleLinear, tooltip: d3.Selection ) { - higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'focus-selected' : 'focus'); + 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 -- cgit v1.2.3-70-g09d2 From 68c6c36af823255824a6b0692e8c33618c2d7ca2 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 10 Apr 2023 11:32:44 -0400 Subject: fixed brushing of fonticon boxes with dropdowns. made line charts use computed values instead of observables --- .../collectionFreeForm/CollectionFreeFormView.tsx | 4 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 14 +++- .../views/nodes/DataVizBox/components/Chart.scss | 2 +- .../nodes/DataVizBox/components/LineChart.tsx | 89 ++++++++++++---------- src/client/views/nodes/button/FontIconBox.tsx | 33 ++++++-- .../nodes/button/colorDropdown/ColorDropdown.tsx | 57 +++++++------- src/fields/Doc.ts | 2 +- 7 files changed, 120 insertions(+), 81 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9fe8f5f49..9d4a788db 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -664,8 +664,8 @@ export class CollectionFreeFormView extends CollectionSubView() { // 2 ways of doing it // @observable private pairs: { [key: string]: number | string | undefined }[] = []; // @observable private pairs: { [key: string]: FieldResult }[] = []; - @observable pairs: { [key: string]: string }[] = []; + static pairSet = new ObservableMap(); + @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 @@ -93,6 +96,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; const margin = { top: 10, right: 50, bottom: 50, left: 50 }; + if (!this.pairs) return 'no data'; // prettier-ignore switch (this.dataVizView) { case DataVizView.TABLE: return ; @@ -109,8 +113,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { } 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 && (this.pairs = res)))); + .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), res)))); } // handle changing the view using a button @@ -121,7 +127,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { } render() { - return this.pairs.length == 0 ? ( + return !this.pairs?.length ? (
Loading...
) : (
{ private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject = React.createRef(); private _lineChartSvg: d3.Selection | undefined; - private _rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number } = {}; @observable _currSelected: SelectedDataPoint | undefined = undefined; - @observable _lineChartData: DataPoint[][] | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + @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( - () => this.props.axes.slice(), - axes => { - if (axes.length > 1) { - this._lineChartData = [this.props.pairs?.map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1))]; - } - }, - { fireImmediately: true } - ); this._disposers.chartData = reaction( - () => ({ dataSet: this._lineChartData, axes: this.props.axes.slice(), w: this.props.width, h: this.props.height }), - vals => { - if (vals.dataSet) { - this._rangeVals = minMaxRange(vals.dataSet); - this.drawChart(vals.dataSet); + () => ({ dataSet: this._lineChartData, w: this.props.width, h: this.props.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 } @@ -87,18 +101,13 @@ export class LineChart extends React.Component { this._disposers.highlights = reaction( () => ({ selected: this._currSelected, - pairs: LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) - .filter(link => link.anchor1 !== this.props.rootDoc) // all links that are pointing to this node - .map(link => DocCast(link.anchor1)) // get the documents that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(), + incomingSelected: this.incomingSelected, }), - ({ selected, pairs }) => { - this.clearAnnoations(); + ({ 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); - pairs.forEach(pair => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); }, { fireImmediately: true } ); @@ -106,7 +115,7 @@ export class LineChart extends React.Component { // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that - clearAnnoations = () => { + clearAnnotations = () => { const elements = document.querySelectorAll('.datapoint'); for (let i = 0; i < elements.length; i++) { const element = elements[i]; @@ -207,12 +216,12 @@ export class LineChart extends React.Component { } // TODO: nda - can use d3.create() to create html element instead of appending - drawChart = (dataSet: DataPoint[][]) => { + 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 } = this._rangeVals; + const { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } @@ -226,16 +235,16 @@ export class LineChart extends React.Component { const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') - .attr('width', `${this.width + margin.right + margin.left}`) - .attr('height', `${this.height + margin.top + margin.bottom}`) + .attr('width', `${width + margin.right + margin.left}`) + .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'), this.height, xScale); - yGrid(svg.append('g'), this.width, yScale); - xAxisCreator(svg.append('g'), this.height, xScale); - yAxisCreator(svg.append('g'), this.width, yScale); + 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]; diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 8410fda18..8eacfbc51 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -161,7 +161,13 @@ export class FontIconBox extends DocComponent() {
); return ( -
e.stopPropagation()} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> +
e.stopPropagation()} + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> {checkResult} {label} {this.rootDoc.dropDownOpen ? dropdown : null} @@ -198,7 +204,10 @@ export class FontIconBox extends DocComponent() { e.stopPropagation(); e.preventDefault(); }} - onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> setValue(Number(e.target.value))))} />
setValue(Number(checkResult) + 1))}> @@ -215,6 +224,7 @@ export class FontIconBox extends DocComponent() { onClick={e => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; + Doc.UnBrushAllDocs(); }} />
@@ -237,7 +247,10 @@ export class FontIconBox extends DocComponent() {
(this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -316,7 +329,14 @@ export class FontIconBox extends DocComponent() {
(this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen) : undefined}> + onClick={ + dropdown + ? () => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + } + : undefined + }> {dropdown ? null : }
{text && text[0].toUpperCase() + text.slice(1)}
{label} @@ -335,6 +355,7 @@ export class FontIconBox extends DocComponent() { onClick={e => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; + Doc.UnBrushAllDocs(); }} />
@@ -379,6 +400,7 @@ export class FontIconBox extends DocComponent() { style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(e => { this.colorPickerClosed = !this.colorPickerClosed; + setTimeout(() => Doc.UnBrushAllDocs()); e.stopPropagation(); })} onPointerDown={e => e.stopPropagation()}> @@ -397,6 +419,7 @@ export class FontIconBox extends DocComponent() { e.preventDefault(); e.stopPropagation(); this.colorPickerClosed = true; + Doc.UnBrushAllDocs(); })} />
@@ -816,7 +839,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), }], ['fillColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'), + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"), setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), setMode: () => SetActiveFillColor(StrCast(value)), }], diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx index 7f414ddbb..74c3c563c 100644 --- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx +++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx @@ -12,7 +12,7 @@ export class ColorDropdown extends Component { const active: string = StrCast(this.props.rootDoc.dropDownOpen); const script: string = StrCast(this.props.rootDoc.script); - const scriptCheck: string = script + "(undefined, true)"; + const scriptCheck: string = script + '(undefined, true)'; const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; const stroke: boolean = false; @@ -24,24 +24,21 @@ export class ColorDropdown extends Component { // strokeIcon = (
); // } - const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', - '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', - '#FFFFFF', '#f1efeb']; + const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb']; - const colorBox = (func: (color: ColorState) => void) => ; - const label = !this.props.label || !FontIconBox.GetShowLabels() ? (null) : -
- {this.props.label} -
; + const colorBox = (func: (color: ColorState) => void) => ; + const label = + !this.props.label || !FontIconBox.GetShowLabels() ? null : ( +
+ {this.props.label} +
+ ); - const dropdownCaret =
- -
; + const dropdownCaret = ( +
+ +
+ ); const click = (value: ColorState) => { const hex: string = value.hex; @@ -51,26 +48,30 @@ export class ColorDropdown extends Component { } }; return ( -
this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen} + onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)} onPointerDown={e => e.stopPropagation()}> -
+
{label} {/* {dropdownCaret} */} - {this.props.rootDoc.dropDownOpen ? + {this.props.rootDoc.dropDownOpen ? (
-
e.stopPropagation()} - onClick={e => e.stopPropagation()}> +
e.stopPropagation()} onClick={e => e.stopPropagation()}> {colorBox(click)}
-
{ e.stopPropagation(); this.props.rootDoc.dropDownOpen = false; }} /> +
{ + e.stopPropagation(); + this.props.rootDoc.dropDownOpen = false; + }} + />
- : null} + ) : null}
); } -} \ No newline at end of file +} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e89f5db52..1db56cbdd 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1394,7 +1394,7 @@ export namespace Doc { }); } export function UnBrushAllDocs() { - brushManager.BrushedDoc.clear(); + runInAction(() => brushManager.BrushedDoc.clear()); } export function getDocTemplate(doc?: Doc) { -- cgit v1.2.3-70-g09d2 From 6520bb8594b6cee9a4df09b25adf7aa590ef29e4 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 10 Apr 2023 13:49:28 -0400 Subject: fixed linechart margins --- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 2 +- src/client/views/nodes/DataVizBox/components/LineChart.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 8593c8994..2666787c5 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -95,7 +95,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { @computed get selectView() { const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; - const margin = { top: 10, right: 50, bottom: 50, left: 50 }; + const margin = { top: 10, right: 10, bottom: 10, left: 10 }; if (!this.pairs) return 'no data'; // prettier-ignore switch (this.dataVizView) { diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index bc9f0be77..ecb06f64f 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -235,8 +235,8 @@ export class LineChart extends React.Component { const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') - .attr('width', `${width + margin.right + margin.left}`) - .attr('height', `${height + margin.top + margin.bottom}`) + .attr('width', `${width - margin.right - margin.left}`) + .attr('height', `${height - margin.top - margin.bottom}`) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); -- cgit v1.2.3-70-g09d2 From 3cb7f85b23eb0ae3a432bbe15b8a2cda37290ce2 Mon Sep 17 00:00:00 2001 From: geireann Date: Mon, 10 Apr 2023 15:32:06 -0400 Subject: fixed linechart margins --- package-lock.json | 8 ++++---- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 2 +- src/client/views/nodes/DataVizBox/components/LineChart.tsx | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx') diff --git a/package-lock.json b/package-lock.json index 83bb8cd9c..4b83d07f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10498,14 +10498,14 @@ "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz", "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=", "requires": { - "image-size": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", + "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", "readable-stream": "^1.0.33", "tryit": "^1.0.1" }, "dependencies": { "image-size": { - "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1", - "from": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1" + "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", + "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1" }, "isarray": { "version": "0.0.1", @@ -21004,7 +21004,7 @@ "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", "optional": true, "requires": { "memory-pager": "^1.0.2" diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 2666787c5..eb25d3264 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -95,7 +95,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent() { @computed get selectView() { 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: 10, bottom: 10, left: 10 }; + const margin = { top: 10, right: 25, bottom: 50, left:25}; if (!this.pairs) return 'no data'; // prettier-ignore switch (this.dataVizView) { diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index ecb06f64f..777bf2f66 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -74,7 +74,7 @@ export class LineChart extends React.Component { } componentDidMount = () => { this._disposers.chartData = reaction( - () => ({ dataSet: this._lineChartData, w: this.props.width, h: this.props.height }), + () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }), ({ dataSet, w, h }) => { if (dataSet) { this.drawChart([dataSet], this.rangeVals, w, h); @@ -227,16 +227,16 @@ export class LineChart extends React.Component { } // creating the x and y scales - const xScale = scaleCreatorNumerical(xMin, xMax, 0, this.width); - const yScale = scaleCreatorNumerical(0, yMax, this.height, 0); + 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.right - margin.left}`) - .attr('height', `${height - margin.top - margin.bottom}`) + .attr('width', `${width +margin.left + margin.right}`) + .attr('height', `${height + margin.top + margin.bottom }`) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); @@ -281,7 +281,7 @@ export class LineChart extends React.Component { svg.append('rect') .attr('class', 'overlay') - .attr('width', this.width) + .attr('width', width) .attr('height', this.height + margin.top + margin.bottom) .attr('fill', 'none') .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) -- cgit v1.2.3-70-g09d2
- setupMoveUpEvents( - {}, - e, - returnFalse, - emptyFunction, - action(e => { - const newAxes = this.props.axes; - if (newAxes.includes(col)) { - newAxes.splice(newAxes.indexOf(col), 1); - } else if (newAxes.length >= 1) { - newAxes[1] = col; - } else { - newAxes[0] = col; - } - this.props.selectAxes(newAxes); - }) - ) - }> - {col} - { + 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([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} +
{p[col]}{p[col]}