import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ChartData, 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'; interface LineChartProps { data: ChartData; width: number; height: number; dataDoc: Doc; fieldKey: string; // returns linechart component but should be generic chart setCurrChart: (chart: LineChart | undefined) => void; getAnchor: () => Doc; margin: { top: number; right: number; bottom: number; left: number; }; } 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 { 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, }; // 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._rangeVals = minMaxRange(this.props.data.data); // this.drawChart(); this._dataReactionDisposer = reaction( () => this.props.data, data => { this._rangeVals = minMaxRange(data.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 }, { fireImmediately: true } ); setTimeout(() => { this.props.setCurrChart(this); }); } // gets called whenever the "data-annotations" fields gets updated drawAnnotations() {} _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; } @computed get svgContainer() { const { margin } = this.props; const svg = d3 .create('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})`); return svg; } // TODO: nda - can use d3.create() to create html element instead of appending drawChart() { const { data, margin } = this.props; console.log(this.height, this.width); // clearing tooltip 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 const svg = 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})`); 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.data, lineGen); // draw the datapoint circles svg.selectAll('.circle-d1') .data(data.data) .join('circle') // enter append .attr('class', 'circle-d1') .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); const focus = svg.append('g').attr('class', 'focus').style('display', 'none'); focus.append('circle').attr('r', 5).attr('class', 'circle'); 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'); // add all the tooltipContent to the tooltip // @action const mousemove = action((e: any) => { const bisect = d3.bisector((d: DataPoint) => d.x).left; const xPos = d3.pointer(e)[0]; const x0 = bisect(data.data, xScale.invert(xPos)); const d0 = data.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! // 0 = 30px => -597px (-this.width - margin.left - margin.right) // 1 = tooltip.html(() => this.tooltipContent(d0)).style('transform', `translate(${xScale(d0.x) - (this.width + margin.left + margin.right)}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.data, xScale.invert(xPos)); const d0 = data.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); }); 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('mousemove', mousemove) .on('click', onPointClick); } render() { return (
x: {this._x} y: {this._y} Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}
); } }