aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/LineChart.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/LineChart.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx290
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>
+ );
+ }
+}