diff options
author | bobzel <zzzman@gmail.com> | 2023-04-06 00:14:24 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2023-04-06 00:14:24 -0400 |
commit | a8343cad405a146fdff8fc2d66ef41fdaefb8bda (patch) | |
tree | 853c9b24f6bec83e46240fd9c39e9487e8eb6650 /src/client/views/nodes/DataVizBox/components/LineChart.tsx | |
parent | 99fba6d6e99da0154d40d2e3e87e120d8e6f2810 (diff) |
more simplification/cleanup of dataviz
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 187 |
1 files changed, 88 insertions, 99 deletions
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<d3.BaseType, unknown, SVGGElement, unknown>; } +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<ChartProps> implements Chart { - private _dataReactionDisposer: IReactionDisposer | undefined = undefined; - private _heightReactionDisposer: IReactionDisposer | undefined = undefined; - private _widthReactionDisposer: IReactionDisposer | undefined; +export class LineChart extends React.Component<LineChartProps> { + 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<HTMLDivElement> = React.createRef(); + private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; private _rangeVals: minMaxRange = { xMin: undefined, xMax: undefined, @@ -45,43 +63,47 @@ export class LineChart extends React.Component<ChartProps> 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<SVGGElement, unknown, null, undefined> | undefined; - - // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that - - // write the getanchor function that gets whatever I want as the link anchor - + 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<ChartProps> 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 `<b>(${data.x},${data.y})</b>`; - } - - @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<ChartProps> 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<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { - 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<ChartProps> 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<ChartProps> 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<ChartProps> 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<ChartProps> implements Chart { const mousemove = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; - const x0 = bisect(data, xScale.invert(xPos - 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(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip .style('pointer-events', 'none') .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}px,${yScale(d0.y) + 30}px)`); }); @@ -260,7 +254,7 @@ export class LineChart extends React.Component<ChartProps> 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<ChartProps> 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 ( - <div ref={this._chartRef} className="chart-container"> + <div ref={this._lineChartRef} className="chart-container"> <span>Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}</span> </div> ); |