aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx8
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx326
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx4
3 files changed, 336 insertions, 2 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 0fe24fe8d..38edf2dd9 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -12,10 +12,12 @@ import { PinProps } from '../trails';
import { LineChart } from './components/LineChart';
import { TableBox } from './components/TableBox';
import './DataVizBox.scss';
+import { Histogram } from './components/Histogram';
export enum DataVizView {
TABLE = 'table',
LINECHART = 'lineChart',
+ HISTOGRAM = 'histogram'
}
@observer
@@ -100,6 +102,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
switch (this.dataVizView) {
case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>;
case DataVizView.LINECHART: return <LineChart ref={r => (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} />;
+ case DataVizView.HISTOGRAM: return <Histogram height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
}
}
@computed get dataUrl() {
@@ -142,7 +145,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ passive: false }
)
}>
- <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
+ {/* <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button> */}
+ <button onClick={e => this.layoutDoc._dataVizView = DataVizView.TABLE}> TABLE </button>
+ <button onClick={e => this.layoutDoc._dataVizView = DataVizView.LINECHART}> LINE CHART </button>
+ <button onClick={e => this.layoutDoc._dataVizView = DataVizView.HISTOGRAM}> HISTOGRAM </button>
{this.selectView}
</div>
);
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
new file mode 100644
index 000000000..cb0b8cd9a
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -0,0 +1,326 @@
+import { observer } from "mobx-react";
+import { Doc, DocListCast } from "../../../../../fields/Doc";
+import * as React from 'react';
+import * as d3 from 'd3';
+import { IReactionDisposer, action, computed, observable, reaction } from "mobx";
+import { LinkManager } from "../../../../util/LinkManager";
+import { Cast, DocCast, StrCast } from "../../../../../fields/Types";
+import { useMemo } from "react";
+import { DataPoint, SelectedDataPoint } from "./LineChart";
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from "../utils/D3Utils";
+import { DocumentManager } from "../../../../util/DocumentManager";
+import { Id } from "../../../../../fields/FieldSymbols";
+import { DataVizBox } from "../DataVizBox";
+import { listSpec } from "../../../../../fields/Schema";
+import { PinProps, PresBox } from "../../trails";
+import { Docs } from "../../../../documents/Documents";
+import { List } from "../../../../../fields/List";
+import './Chart.scss';
+import { isNaN } from "lodash";
+
+export interface HistogramProps {
+ rootDoc: Doc;
+ axes: string[];
+ pairs: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class Histogram extends React.Component<HistogramProps> {
+
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ @observable _currSelected: SelectedDataPoint | 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 _histogramData() {
+ if (this.props.axes.length < 1) return [];
+ if (this.props.axes.length < 2) {
+ var ax0 = this.props.axes[0];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .map(pair => ({ [ax0]: (pair[this.props.axes[0]])}))
+ };
+ var ax0 = this.props.axes[0];
+ var ax1 = this.props.axes[1];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .map(pair => ({ [ax0]: (pair[this.props.axes[0]]), [ax1]: (pair[this.props.axes[1]]) }))
+ // .sort((a, b) => (a[ax0] < b[ax0] ? -1 : 1));
+ 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.link_anchor_1 !== this.props.rootDoc) // get links where this chart doc is the target of the link
+ .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+ @computed get incomingSelected() {
+ // return selected x and y axes
+ // otherwise, use the selection of whatever is linked to us
+ 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: { [x: string]: any; }) => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor
+ .lastElement();
+ }
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._histogramData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet) {
+ this.drawChart(dataSet, 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 }
+ );
+ 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
+ // 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._disposers.highlights = reaction(
+ () => ({
+ selected: this._currSelected,
+ incomingSelected: this.incomingSelected,
+ }),
+ ({ 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);
+ incomingSelected?.forEach((pair: any) => 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
+
+ clearAnnotations = () => {
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ element.classList.remove('brushed');
+ element.classList.remove('selected');
+ }
+ };
+ // gets called whenever the "data_annotations" fields gets updated
+ 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
+ // 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(selected ? 'selected' : 'brushed');
+ }
+ // TODO: nda - this remove highlight code should go where we remove the links
+ // } else {
+ // }
+ }
+ };
+
+ removeAnnotations(dataX: number, dataY: number) {
+ // loop through and remove any annotations that no longer exist
+ }
+
+ @action
+ restoreView = (data: Doc) => {
+ 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.setCurrSelected();
+ return true;
+ }
+ return false;
+ };
+
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({
+ //
+ title: 'line doc selection' + this._currSelected?.x,
+ });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
+ anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
+ return anchor;
+ };
+
+ @computed get height() {
+ return this.props.height - this.props.margin.top - this.props.margin.bottom;
+ }
+
+ @computed get width() {
+ return this.props.width - this.props.margin.left - this.props.margin.right;
+ }
+
+ setupTooltip() {
+ return d3
+ .select(this._histogramRef.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');
+ }
+
+ // 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 !== 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<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
+ if (this._histogramSvg) {
+ const circleClass = '.circle-' + idx;
+ this._histogramSvg
+ .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);
+ }
+ }
+
+ drawChart = (dataSet: any, width: number, height: number) => {
+
+ var validData = dataSet.filter((d: { [x: string]: unknown; }) => {
+ var valid = true;
+ Object.keys(dataSet[0]).map(key => {
+ if (!d[key] || Number.isNaN(d[key])) valid = false;
+ })
+ return valid;
+ })
+ var field = Object.keys(dataSet[0])[0]
+ const data = validData.map((d: { [x: string]: any; }) => {
+ if (+d[field]) {return +d[field] }
+ return d[field]
+ })
+
+ let uniqueArr = [...new Set(data)]
+ var numBins = uniqueArr.length
+ if (+d3.max(data)!) numBins = +d3.max(data)!
+ // var numBins = (+d3.max(data)!)? +d3.max(data)! : 3;
+
+ const svg = d3.select(this._histogramRef.current)
+ .append("svg")
+ .attr("width", width + this.props.margin.right + this.props.margin.left)
+ .attr("height", height + this.props.margin.top + this.props.margin.bottom)
+ .append("g")
+ .attr("transform",
+ "translate(" + this.props.margin.left + "," + this.props.margin.top + ")");
+
+ var x = d3.scaleLinear()
+ .domain([0, numBins])
+ .range([0, width]);
+ var xAxis = d3.axisBottom(x)
+ .ticks(numBins)
+ var translateXAxis = 0;
+
+ var histogram = d3.histogram()
+ .value(function(d) {return d})
+ .domain([0, numBins])
+ .thresholds(x.ticks(numBins))
+ var bins = histogram(data)
+ if (!+d3.max(data)!) { // if the data is strings rather than numbers
+ uniqueArr.sort()
+ for (let i=0; i<data.length; i++){
+ var index = 0
+ for (let j=0; j<uniqueArr.length; j++){
+ if (uniqueArr[j] == data[i]){
+ index = j;
+ }
+ }
+ bins[index].push(data[i])
+ }
+ bins.forEach(d => d.x0 = d.x0!+1)
+ xAxis = d3.axisBottom(x)
+ .ticks(numBins)
+ .tickValues([0, 1, 2, 3, 4])
+ .tickFormat( i => uniqueArr[i])
+ .tickPadding(10)
+ translateXAxis = (width/numBins) / 2;
+ }
+
+ var y = d3.scaleLinear()
+ .range([height, 0]);
+ y.domain([0, d3.max(bins, function(d) { return d.length; })!]);
+ var yAxis = d3.axisLeft(y)
+ .ticks(d3.max(bins, function(d) { return d.length; })!)
+ svg.append("g")
+ .call(yAxis);
+ svg.append("g")
+ .attr("transform", "translate(" + translateXAxis + ", " + height + ")")
+ .call(xAxis)
+
+ d3.format('.0f')
+
+ svg.selectAll("rect")
+ .data(bins)
+ .enter()
+ .append("rect")
+ .attr("x", 1)
+ .attr("transform", function(d) { return "translate(" + x(d.x0! - 1) + "," + y(d.length) + ")"; })
+ // .attr("width", function(d) { return x(d.x1!) - x(d.x0! ) ; })
+ .attr("width", width/(numBins))
+ .attr("height", function(d) { return height - y(d.length); })
+ .attr("style", "outline: thin solid black;")
+ .style("fill", "#69b3a2")
+ };
+
+ render() {
+
+ return (
+ this.props.axes.length >= 1 ? (
+ <div ref={this._histogramRef} className="chart-container">
+ <span> {`Selected: ${Object.keys(this._histogramData[0])[0]}`}</span>
+ </div>
+ ) : <span className="chart-container"> {'first use table view to select an axis to plot'}</span>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 6b564b0c9..289cecb6b 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -20,7 +20,7 @@ export interface DataPoint {
x: number;
y: number;
}
-interface SelectedDataPoint extends DataPoint {
+export interface SelectedDataPoint extends DataPoint {
elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
}
export interface LineChartProps {
@@ -60,6 +60,8 @@ export class LineChart extends React.Component<LineChartProps> {
.map(link => DocCast(link.link_anchor_1)); // then return the source of the link
}
@computed get incomingSelected() {
+ // return selected x and y axes
+ // otherwise, use the selection of whatever is linked to us
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)