diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/LineChart.tsx | 290 |
1 files changed, 290 insertions, 0 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx new file mode 100644 index 000000000..d893b3e12 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -0,0 +1,290 @@ +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { DataPoint } from '../ChartBox'; +// import d3 +import * as d3 from 'd3'; +import { minMaxRange, createLineGenerator, xGrid, yGrid, drawLine, xAxisCreator, yAxisCreator, scaleCreatorNumerical, scaleCreatorCategorical } from '../utils/D3Utils'; +import { Docs } from '../../../../documents/Documents'; +import { Doc, DocListCast } from '../../../../../fields/Doc'; +import { Chart, ChartProps } from '../ChartInterface'; +import './Chart.scss'; + +type minMaxRange = { + xMin: number | undefined; + xMax: number | undefined; + yMin: number | undefined; + yMax: number | undefined; +}; + +interface SelectedDataPoint { + x: number; + y: number; + elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; +} + +@observer +export class LineChart extends React.Component<ChartProps> implements Chart { + private _dataReactionDisposer: IReactionDisposer | undefined = undefined; + private _heightReactionDisposer: IReactionDisposer | undefined = undefined; + private _widthReactionDisposer: IReactionDisposer | undefined; + @observable private _x: number = 0; + @observable private _y: number = 0; + @observable private _currSelected: SelectedDataPoint | undefined = undefined; + // create ref for the div + private _chartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _rangeVals: minMaxRange = { + xMin: undefined, + xMax: undefined, + yMin: undefined, + yMax: undefined, + }; + // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + + private _chartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; + + // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that + + // write the getanchor function that gets whatever I want as the link anchor + + componentDidMount = () => { + this._dataReactionDisposer = reaction( + () => this.props.chartData, + chartData => { + this._rangeVals = minMaxRange(chartData.data); + this.drawChart(); + }, + { fireImmediately: true } + ); + // DocumentDecorations.Instance.Interacting + this._heightReactionDisposer = reaction(() => this.props.height, this.drawChart.bind(this)); + this._widthReactionDisposer = reaction(() => this.props.width, this.drawChart.bind(this)); + reaction( + () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), + annotations => { + // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way + // could be blue colored to make it look like anchor + console.log(annotations); + // this.drawAnnotations() + // loop through annotations and draw them + annotations.forEach(a => { + this.drawAnnotations(Number(a.x), Number(a.y)); + }); + // this.drawAnnotations(annotations.x, annotations.y); + }, + { fireImmediately: true } + ); + + this.props.setCurrChart(this); + }; + + // gets called whenever the "data-annotations" fields gets updated + drawAnnotations(dataX: number, dataY: number) { + // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements + // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY + // if it exists, then highlight it + // if it doesn't exist, then remove the highlight + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const x = element.getAttribute('data-x'); + const y = element.getAttribute('data-y'); + if (x === dataX.toString() && y === dataY.toString()) { + element.classList.add('highlight'); + } + // TODO: nda - this remove highlight code should go where we remove the links + // } else { + // element.classList.remove('highlight'); + // } + } + } + + removeAnnotations(dataX: number, dataY: number) { + // loop through and remove any annotations that no longer exist + } + + _getAnchor() { + // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element) + + // TODO: nda - look at pdfviewer get anchor for args + const doc = Docs.Create.TextanchorDocument({ + /*put in some options*/ + title: 'line doc selection' + this._currSelected?.x, + }); + // access curr selected from the charts + doc.x = this._currSelected?.x; + doc.y = this._currSelected?.y; + doc.chartType = 'line'; + return doc; + // have some other function in code that + } + + componentWillUnmount() { + if (this._dataReactionDisposer) { + this._dataReactionDisposer(); + } + if (this._heightReactionDisposer) { + this._heightReactionDisposer(); + } + if (this._widthReactionDisposer) { + this._widthReactionDisposer(); + } + } + + tooltipContent(data: DataPoint) { + return `<b>x: ${data.x} y: ${data.y}</b>`; + } + + @computed get height(): number { + return this.props.height - this.props.margin.top - this.props.margin.bottom; + } + + @computed get width(): number { + return this.props.width - this.props.margin.left - this.props.margin.right; + } + + setupTooltip() { + const tooltip = d3 + .select(this._chartRef.current) + .append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + .style('background', '#fff') + .style('border', '1px solid #ccc') + .style('padding', '5px') + .style('position', 'absolute') + .style('font-size', '12px'); + return tooltip; + } + + // TODO: nda - use this everyewhere we update currSelected? + @action + setCurrSelected(x: number, y: number) { + // TODO: nda - get rid of svg element in the list? + this._currSelected = { x: x, y: y }; + } + + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { + if (this._chartSvg) { + const circleClass = '.circle-' + idx; + this._chartSvg + .selectAll(circleClass) + .data(data) + .join('circle') // enter append + .attr('class', `${circleClass} datapoint`) + .attr('r', '3') // radius + .attr('cx', d => xScale(d.x)) + .attr('cy', d => yScale(d.y)) + .attr('data-x', d => d.x) + .attr('data-y', d => d.y); + } + } + + // TODO: nda - can use d3.create() to create html element instead of appending + drawChart() { + const { chartData, margin } = this.props; + const data = chartData.data[0]; + // clearing tooltip and the current chart + d3.select(this._chartRef.current).select('svg').remove(); + d3.select(this._chartRef.current).select('.tooltip').remove(); + + // TODO: nda - refactor code so that it only recalculates min max and things related to data on data update + + const { xMin, xMax, yMin, yMax } = this._rangeVals; + // const svg = d3.select(this._chartRef.current).append(this.svgContainer.html()); + // adding svg + this._chartSvg = d3 + .select(this._chartRef.current) + .append('svg') + .attr('width', `${this.width + margin.right + margin.left}`) + .attr('height', `${this.height + margin.top + margin.bottom}`) + .append('g') + .attr('transform', `translate(${margin.left}, ${margin.top})`); + + const svg = this._chartSvg; + + if (xMin == undefined || xMax == undefined || yMin == undefined || yMax == undefined) { + // TODO: nda - error handle + return; + } + + // creating the x and y scales + const xScale = scaleCreatorNumerical(xMin, xMax, 0, this.width); + const yScale = scaleCreatorNumerical(0, yMax, this.height, 0); + + // create a line function that takes in the data.data.x and data.data.y + // TODO: nda - fix the types for the d here + const lineGen = createLineGenerator(xScale, yScale); + + // create x and y grids + xGrid(svg.append('g'), this.height, xScale); + yGrid(svg.append('g'), this.width, yScale); + xAxisCreator(svg.append('g'), this.height, xScale); + yAxisCreator(svg.append('g'), this.width, yScale); + + // draw the line + drawLine(svg.append('path'), data, lineGen); + + // draw the datapoint circle + this.drawDataPoints(data, 0, xScale, yScale); + + const focus = svg.append('g').attr('class', 'focus').style('display', 'none'); + focus.append('circle').attr('r', 5).attr('class', 'circle'); + const tooltip = this.setupTooltip(); + // add all the tooltipContent to the tooltip + const mousemove = action((e: any) => { + const bisect = d3.bisector((d: DataPoint) => d.x).left; + const xPos = d3.pointer(e)[0]; + const x0 = bisect(data, xScale.invert(xPos)); + const d0 = data[x0]; + this._x = d0.x; + this._y = d0.y; + focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`); + // TODO: nda - implement tooltips + tooltip.transition().duration(300).style('opacity', 0.9); + // TODO: nda - updating the inner html could be deadly cause injection attacks! + tooltip.html(() => this.tooltipContent(d0)).style('transform', `translate(${xScale(d0.x) - (this.width + margin.left + margin.right) + 30}px,${yScale(d0.y) + 30}px)`); + }); + + const onPointClick = action((e: any) => { + const bisect = d3.bisector((d: DataPoint) => d.x).left; + const xPos = d3.pointer(e)[0]; + const x0 = bisect(data, xScale.invert(xPos)); + const d0 = data[x0]; + this._x = d0.x; + this._y = d0.y; + // find .circle-d1 with data-x = d0.x and data-y = d0.y + const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); + this._currSelected = { x: d0.x, y: d0.y, elem: selected }; + console.log('Getting here'); + // this.drawAnnotations(this._x, this._y); + // this.props.getAnchor(); + console.log(this._currSelected); + }); + + this._chartSvg + .append('rect') + .attr('class', 'overlay') + .attr('width', this.width) + .attr('height', this.height + margin.top + margin.bottom) + .attr('fill', 'none') + .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) + .style('opacity', 0) + .on('mouseover', () => { + focus.style('display', null); + }) + .on('mouseout', () => { + tooltip.transition().duration(300).style('opacity', 0); + }) + .on('mousemove', mousemove) + .on('click', onPointClick); + } + + render() { + return ( + <div ref={this._chartRef} className="chart-container"> + <span>Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}</span> + </div> + ); + } +} |