aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsrichman333 <sarah_n_richman@brown.edu>2023-08-08 16:15:36 -0400
committersrichman333 <sarah_n_richman@brown.edu>2023-08-08 16:15:36 -0400
commit318d56e1dff94204b100f5636e1a3288724aaffc (patch)
treeb89705e59e3fc4ed4177ddde7361de0e60fb32c5 /src
parent5614478b54b754665faff41c9a09b9bd0535e2f5 (diff)
comments + cleanups
Diffstat (limited to 'src')
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx47
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx120
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx155
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx7
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts1
5 files changed, 78 insertions, 252 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 9a4546900..e71739231 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -26,45 +26,20 @@ export enum DataVizView {
@observer
export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(DataVizBox, fieldKey);
- }
- // says we have an object and any string
- // 2 ways of doing it
- // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
- // @observable private pairs: { [key: string]: FieldResult }[] = [];
+ // all data
static pairSet = new ObservableMap<string, { [key: string]: string }[]>();
@computed.struct get pairs() {
return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href);
}
- private _chartRenderer: LineChart | Histogram | PieChart | undefined;
- // // another way would be store a schema that defines the type of data we are expecting from an imported doc
-
- // method1() {
- // this.pairs[0].x = 3;
- // }
-
- // method() {
- // // this.pairs[0].x = 3;
- // // go through the pairs
- // const x = this.pairs[0].x;
- // if (typeof x == 'number') {
- // let x1 = Number(x);
- // // let x1 = NumCast(x);
- // }
- // }
- // could use field result
- // [key: string]: FieldResult;
- // instead of numeric x,y in there,
-
- // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
+ private _chartRenderer: LineChart | Histogram | PieChart | undefined;
+ // current displayed chart type
@computed get dataVizView(): DataVizView {
return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
}
- @action
+ @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
restoreView = (data: Doc) => {
const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView);
const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes)));
@@ -75,7 +50,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
Object.keys(this.layoutDoc).map(key => {
if (key.startsWith('histogram-title') || key.startsWith('lineChart-title') || key.startsWith('pieChart-title')){ this.layoutDoc['_'+key] = data[key]; }
})
-
const func = () => this._chartRenderer?.restoreView(data);
if (changedView || changedAxes) {
setTimeout(func, 100);
@@ -83,7 +57,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
return func() ?? false;
};
-
getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
const anchor = !pinProps
? this.rootDoc
@@ -93,7 +66,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
/*put in some options*/
});
-
anchor.presDataVizView = this.dataVizView;
anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
anchor.selected = Field.Copy(this.layoutDoc.selected);
@@ -103,7 +75,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
Object.keys(this.layoutDoc).map(key => {
if (key.startsWith('histogram-title') || key.startsWith('lineChart-title') || key.startsWith('pieChart-title')){ anchor[key] = this.layoutDoc[key]; }
})
-
this.addDocument(anchor);
return anchor;
};
@@ -113,6 +84,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes));
+ // toggles for user to decide which chart type to view the data in
@computed get selectView() {
const width = this.props.PanelWidth() * 0.9;
const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
@@ -125,6 +97,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
case DataVizView.PIECHART: return <PieChart layoutDoc={this.layoutDoc} 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} />;
}
}
+
@computed get dataUrl() {
return Cast(this.dataDoc[this.fieldKey], CsvField);
}
@@ -141,16 +114,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res))));
}
- // handle changing the view using a button
- @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
- e.preventDefault();
- e.stopPropagation();
- this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
- }
-
render() {
if (!this.layoutDoc._dataVizView) this.layoutDoc._dataVizView = this.dataVizView;
return !this.pairs?.length ? (
+ // displays how to get data into the DataVizBox if its empty
<div className="start-message">
To create a DataViz box, either import / drag a CSV file into your canvas
or copy a data table and use the command 'ctrl + t' to bring the data table
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index 26c40045c..0b35f2856 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -44,12 +44,12 @@ export class Histogram extends React.Component<HistogramProps> {
private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis
private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency
private maxBins = 15; // maximum number of bins that is readable on a normal sized doc
- @observable _currSelected: any | undefined = undefined;
- private curBarSelected: any = undefined;
- private selectedData: any = undefined;
- private hoverOverData: any = undefined;
- // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+ @observable _currSelected: any | undefined = undefined; // Object of selected bar
+ private curBarSelected: any = undefined; // histogram bin of selected bar
+ private selectedData: any = undefined; // Selection of selected bar
+ private hoverOverData: any = undefined; // Selection of bar being hovered over
+ // filters all data to just display selected data if brushed (created from an incoming link)
@computed get _histogramData() {
var guids = StrListCast(this.props.layoutDoc.rowGuids);
if (this.props.axes.length < 1) return [];
@@ -60,7 +60,6 @@ export class Histogram extends React.Component<HistogramProps> {
?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
.map(pair => ({ [ax0]: (pair[this.props.axes[0]])}))
};
-
var ax0 = this.props.axes[0];
var ax1 = this.props.axes[1];
if (/\d/.test(this.props.pairs[0][ax0])) { this.numericalXData = true;}
@@ -69,6 +68,7 @@ export class Histogram extends React.Component<HistogramProps> {
?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
.map(pair => ({ [ax0]: (pair[this.props.axes[0]]), [ax1]: (pair[this.props.axes[1]]) }))
}
+
@computed get defaultGraphTitle(){
var ax0 = this.props.axes[0];
var ax1 = (this.props.axes.length>1)? this.props.axes[1] : undefined;
@@ -77,21 +77,13 @@ export class Histogram extends React.Component<HistogramProps> {
}
else return ax1 + " by " + ax0 + " Histogram";
}
+
@computed get incomingLinks() {
return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
- .filter(link => {
- return link.link_anchor_1 == this.props.rootDoc.draggedFrom}) // get links where this chart doc is the target of the link
+ .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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();
- }
+
@computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
if (this.numericalXData){
const data = this.data(this._histogramData);
@@ -99,6 +91,7 @@ export class Histogram extends React.Component<HistogramProps> {
}
return {xMin:0, xMax:0, yMin:0, yMax:0}
}
+
componentWillUnmount() {
Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
}
@@ -108,79 +101,14 @@ export class Histogram extends React.Component<HistogramProps> {
({ dataSet, w, h }) => {
if (dataSet!.length>0) {
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) => {};
-
// 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({
@@ -188,7 +116,6 @@ export class Histogram extends React.Component<HistogramProps> {
title: 'histogram doc selection' + this._currSelected,
});
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
- // anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected]) : undefined;
return anchor;
};
@@ -200,6 +127,7 @@ export class Histogram extends React.Component<HistogramProps> {
return this.props.width - this.props.margin.left - this.props.margin.right;
}
+ // cleans data by converting numerical data to numbers and taking out empty cells
data = (dataSet: any) => {
var validData = dataSet.filter((d: { [x: string]: unknown; }) => {
var valid = true;
@@ -216,21 +144,24 @@ export class Histogram extends React.Component<HistogramProps> {
return data;
}
+ // outlines the bar selected / hovered over
highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => {
var sameAsCurrent: boolean;
var barCounter = -1;
const selected = svg.selectAll('.histogram-bar').filter((d: any) => {
- barCounter++;
+ barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
if ((barCounter*eachRectWidth ) <= pointerX && pointerX <= ((barCounter+1)*eachRectWidth)){
var showSelected = this.numericalYData? this._histogramData.filter((data: { [x: string]: any; }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d[0])[0]
: histDataSet.filter((data: { [x: string]: any; }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d[0])[0];
if (this.numericalXData){
+ // calculating frequency
if (d[0] && d[1] && d[0]!=d[1]){
showSelected = {[xAxisTitle]: (d3.min(d) + " to " + d3.max(d)), frequency: d.length}
}
else if (!this.numericalYData) showSelected = {[xAxisTitle]: showSelected[xAxisTitle], frequency: d.length}
}
if (changeSelectedVariables){
+ // for when a bar is selected - not just hovered over
sameAsCurrent = this._currSelected?
(showSelected[xAxisTitle]==this._currSelected![xAxisTitle]
&& showSelected[yAxisTitle]==this._currSelected![yAxisTitle])
@@ -249,12 +180,12 @@ export class Histogram extends React.Component<HistogramProps> {
}
}
+ // draws the histogram
drawChart = (dataSet: any, width: number, height: number) => {
d3.select(this._histogramRef.current).select('svg').remove();
d3.select(this._histogramRef.current).select('.tooltip').remove();
var data = this.data(dataSet);
-
var xAxisTitle = Object.keys(dataSet[0])[0]
var yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency';
let uniqueArr: unknown[] = [...new Set(data)]
@@ -263,6 +194,8 @@ export class Histogram extends React.Component<HistogramProps> {
if (numBins>this.maxBins) numBins = this.maxBins;
var startingPoint = this.numericalXData? this.rangeVals.xMin! : 0;
var endingPoint = this.numericalXData? this.rangeVals.xMax! : numBins;
+
+ // converts data into Objects
var histDataSet = dataSet.filter((d: { [x: string]: unknown; }) => {
var valid = true;
Object.keys(dataSet[0]).map(key => {
@@ -289,6 +222,7 @@ export class Histogram extends React.Component<HistogramProps> {
histDataSet = histStringDataSet
}
+ // initial graph and binning data for histogram
var svg = (this._histogramSvg = d3
.select(this._histogramRef.current)
.append("svg")
@@ -298,7 +232,6 @@ export class Histogram extends React.Component<HistogramProps> {
.append("g")
.attr("transform",
"translate(" + this.props.margin.left + "," + this.props.margin.top + ")"));
-
var x = d3.scaleLinear()
.domain(this.numericalXData? [startingPoint!, endingPoint!] : [0, numBins])
.range([0, width ]);
@@ -314,6 +247,8 @@ export class Histogram extends React.Component<HistogramProps> {
.range([0, Number.isInteger(this.rangeVals.xMin!)? (width-eachRectWidth) : width ])
var xAxis;
+ // more calculations based on bins
+ // x-axis
if (!this.numericalXData) { // reorganize if the data is strings rather than numbers
// uniqueArr.sort()
histDataSet.sort()
@@ -331,7 +266,7 @@ export class Histogram extends React.Component<HistogramProps> {
bins.forEach(d => d.x0 = d.x0!)
xAxis = d3.axisBottom(x)
.ticks(bins.length-1)
- .tickFormat( i => uniqueArr[i])
+ .tickFormat( i => uniqueArr[i.valueOf()] as string)
.tickPadding(10)
x.range([0, width-eachRectWidth])
x.domain([0, bins.length-1])
@@ -343,10 +278,10 @@ export class Histogram extends React.Component<HistogramProps> {
xAxis = d3.axisBottom(x)
.ticks(numBins-1)
}
+ // y-axis
const maxFrequency = this.numericalYData? d3.max(histDataSet, function(d: any) {
return Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''))})
: d3.max(bins, function(d) { return d.length; })
-
var y = d3.scaleLinear()
.range([height, 0]);
y.domain([0, +maxFrequency!]);
@@ -363,7 +298,8 @@ export class Histogram extends React.Component<HistogramProps> {
svg.append("g")
.attr("transform", "translate(" + translateXAxis + ", " + height + ")")
.call(xAxis)
-
+
+ // click/hover
const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
const onHover = action((e: any) => {
const selected = this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)
@@ -378,12 +314,11 @@ export class Histogram extends React.Component<HistogramProps> {
const selectedData = this.selectedData;
svg.selectAll('rect').attr("class", function(d: any) { return ((hoverOverBar && hoverOverBar[0]==d[0]) || selectedData && selectedData[0]==d[0])? 'histogram-bar hover' : 'histogram-bar'; })
}
-
svg.on('click', onPointClick)
.on('mouseover', onHover)
.on('mouseout', mouseOut)
- var selected = this.selectedData;
+ // axis titles
svg.append("text")
.attr("transform", "translate(" + (width/2) + " ," + (height+40) + ")")
.style("text-anchor", "middle")
@@ -395,6 +330,9 @@ export class Histogram extends React.Component<HistogramProps> {
.style("text-anchor", "middle")
.text(yAxisTitle);
d3.format('.0f')
+
+ // draw bars
+ var selected = this.selectedData;
svg.selectAll("rect")
.data(bins)
.enter()
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 0fd4f6b54..d4570dee2 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -40,12 +40,12 @@ export class PieChart extends React.Component<PieChartProps> {
private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef();
private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
private byCategory: boolean = true; // whether the data is organized by category or by specified number percentages/ratios
- @observable _currSelected: any | undefined = undefined;
- private curSliceSelected: any = undefined;
- private selectedData: any = undefined;
- private hoverOverData: any = undefined;
- // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+ @observable _currSelected: any | undefined = undefined; // Object of selected slice
+ private curSliceSelected: any = undefined; // d3 data of selected slice
+ private selectedData: any = undefined; // Selection of selected slice
+ private hoverOverData: any = undefined; // Selection of slice being hovered over
+ // filters all data to just display selected data if brushed (created from an incoming link)
@computed get _piechartData() {
var guids = StrListCast(this.props.layoutDoc.rowGuids);
if (this.props.axes.length < 1) return [];
@@ -56,7 +56,6 @@ export class PieChart extends React.Component<PieChartProps> {
?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
.map(pair => ({ [ax0]: (pair[this.props.axes[0]])}))
};
-
var ax0 = this.props.axes[0];
var ax1 = this.props.axes[1];
if (/\d/.test(this.props.pairs[0][ax0])) { this.byCategory = false; }
@@ -64,6 +63,7 @@ export class PieChart extends React.Component<PieChartProps> {
?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
.map(pair => ({ [ax0]: (pair[this.props.axes[0]]), [ax1]: (pair[this.props.axes[1]]) }))
}
+
@computed get defaultGraphTitle(){
var ax0 = this.props.axes[0];
var ax1 = (this.props.axes.length>1)? this.props.axes[1] : undefined;
@@ -72,28 +72,13 @@ export class PieChart extends React.Component<PieChartProps> {
}
else return ax1 + " by " + ax0 + " Pie Chart";
}
+
@computed get incomingLinks() {
return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
- .filter(link => {
- return link.link_anchor_1 == this.props.rootDoc.draggedFrom}) // get links where this chart doc is the target of the link
+ .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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();
- }
- @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
- if (!this.byCategory){
- const data = this.data(this._piechartData);
- return {xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin:0, yMax:0}
- }
- return {xMin:0, xMax:0, yMin:0, yMax:0}
- }
+
componentWillUnmount() {
Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
}
@@ -103,79 +88,14 @@ export class PieChart extends React.Component<PieChartProps> {
({ dataSet, w, h }) => {
if (dataSet!.length>0) {
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) => {};
-
// 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({
@@ -183,7 +103,6 @@ export class PieChart extends React.Component<PieChartProps> {
title: 'piechart doc selection' + this._currSelected,
});
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
- // anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected]) : undefined;
return anchor;
};
@@ -195,15 +114,7 @@ export class PieChart extends React.Component<PieChartProps> {
return this.props.width - this.props.margin.left - this.props.margin.right;
}
- // 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));
- }
-
+ // cleans data by converting numerical data to numbers and taking out empty cells
data = (dataSet: any) => {
var validData = dataSet.filter((d: { [x: string]: unknown; }) => {
var valid = true;
@@ -220,15 +131,16 @@ export class PieChart extends React.Component<PieChartProps> {
return data;
}
+ // outlines the slice selected / hovered over
highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => {
var index = -1;
var sameAsCurrent: boolean;
const selected = svg.selectAll('.slice').filter((d: any) => {
index++;
- var p1 = [0,0];
- var p3 = [arc.centroid(d)[0]*2, arc.centroid(d)[1]*2];
- var p2 = [radius*Math.sin(d.startAngle), -radius*Math.cos(d.startAngle)];
- var p4 = [radius*Math.sin(d.endAngle), -radius*Math.cos(d.endAngle)];
+ var p1 = [0,0]; // center of pie
+ var p3 = [arc.centroid(d)[0]*2, arc.centroid(d)[1]*2]; // outward peak of arc
+ var p2 = [radius*Math.sin(d.startAngle), -radius*Math.cos(d.startAngle)]; // start of arc
+ var p4 = [radius*Math.sin(d.endAngle), -radius*Math.cos(d.endAngle)]; // end of arc
// draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge
var lineCrossCount = 0;
@@ -241,10 +153,10 @@ export class PieChart extends React.Component<PieChartProps> {
if (pointer[0] <= (pointer[1]-p3[1])*(p4[0]-p3[0])/(p4[1]-p3[1])+p3[0]) lineCrossCount++; }
if (Math.min(p4[1], p1[1])<=pointer[1] && pointer[1]<=Math.max(p4[1], p1[1])){
if (pointer[0] <= (pointer[1]-p4[1])*(p1[0]-p4[0])/(p1[1]-p4[1])+p4[0]) lineCrossCount++; }
- if (lineCrossCount % 2 != 0) {
+ if (lineCrossCount % 2 != 0) { // inside the slice of it crosses an odd number of edges
var showSelected = this.byCategory? pieDataSet[index] : this._piechartData[index];
-
if (changeSelectedVariables){
+ // for when a bar is selected - not just hovered over
sameAsCurrent = (this._currSelected)?
(showSelected[Object.keys(showSelected)[0]]==this._currSelected![Object.keys(showSelected)[0]]
&& showSelected[Object.keys(showSelected)[1]]==this._currSelected![Object.keys(showSelected)[1]])
@@ -264,6 +176,7 @@ export class PieChart extends React.Component<PieChartProps> {
}
}
+ // draws the pie chart
drawChart = (dataSet: any, width: number, height: number) => {
d3.select(this._piechartRef.current).select('svg').remove();
d3.select(this._piechartRef.current).select('.tooltip').remove();
@@ -271,18 +184,8 @@ export class PieChart extends React.Component<PieChartProps> {
var percentField = Object.keys(dataSet[0])[0]
var descriptionField = Object.keys(dataSet[0])[1]!
var radius = Math.min(width, height-this.props.margin.top-this.props.margin.bottom) /2
- var svg = (this._piechartSvg = d3
- .select(this._piechartRef.current)
- .append("svg")
- .attr("class", "graph")
- .attr("width", width + this.props.margin.right + this.props.margin.left)
- .attr("height", height + this.props.margin.top + this.props.margin.bottom)
- .append("g"));
-
- let g = svg.append("g")
- .attr("transform",
- "translate(" + (width/2 + this.props.margin.left) + "," + height/2 + ")");
+ // converts data into Objects
var data = this.data(dataSet);
var pieDataSet = dataSet.filter((d: { [x: string]: unknown; }) => {
var valid = true;
@@ -293,12 +196,12 @@ export class PieChart extends React.Component<PieChartProps> {
});
if (this.byCategory){
let uniqueCategories = [...new Set(data)]
- var pieStringDataSet: { frequency: number, [percentField]: string }[] = [];
+ var pieStringDataSet: { frequency: number }[] = [];
for (let i=0; i<uniqueCategories.length; i++){
pieStringDataSet.push({frequency: 0, [percentField]: uniqueCategories[i]})
}
for (let i=0; i<data.length; i++){
- let sliceData = pieStringDataSet.filter(each => each[percentField]==data[i])
+ let sliceData = pieStringDataSet.filter((each: any) => each[percentField]==data[i])
sliceData[0].frequency = sliceData[0].frequency + 1;
}
pieDataSet = pieStringDataSet
@@ -309,11 +212,23 @@ export class PieChart extends React.Component<PieChartProps> {
var trackDuplicates : {[key: string]: any} = {};
data.forEach((eachData: any) => !trackDuplicates[eachData]? trackDuplicates[eachData] = 0: null)
+ // initial chart
+ var svg = (this._piechartSvg = d3
+ .select(this._piechartRef.current)
+ .append("svg")
+ .attr("class", "graph")
+ .attr("width", width + this.props.margin.right + this.props.margin.left)
+ .attr("height", height + this.props.margin.top + this.props.margin.bottom)
+ .append("g"));
+ let g = svg.append("g")
+ .attr("transform",
+ "translate(" + (width/2 + this.props.margin.left) + "," + height/2 + ")");
var pie = d3.pie();
var arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
+ // click/hover
const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet));
const onHover = action((e: any) => {
const selected = this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet)
@@ -331,6 +246,7 @@ export class PieChart extends React.Component<PieChartProps> {
|| ((hoverOverSlice && d.startAngle==hoverOverSlice.startAngle && d.endAngle==hoverOverSlice.endAngle)))? 'slice hover' : 'slice'; })
}
+ // drawing the slices
var selected = this.selectedData;
var arcs = g.selectAll("arc")
.data(pie(data))
@@ -359,11 +275,12 @@ export class PieChart extends React.Component<PieChartProps> {
function(d) {
return (selected && d.startAngle==selected.startAngle && d.endAngle==selected.endAngle)? 'slice hover' : 'slice';
}: function(d) {return 'slice'})
- .attr("d", arc)
+ .attr('d', arc)
.on('click', onPointClick)
.on('mouseover', onHover)
.on('mouseout', mouseOut);
+ // adding labels
trackDuplicates = {};
data.forEach((eachData: any) => !trackDuplicates[eachData]? trackDuplicates[eachData] = 0: null)
arcs.append("text")
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 38dd62d8d..277ee83ec 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -32,6 +32,7 @@ interface TableBoxProps {
@observer
export class TableBox extends React.Component<TableBoxProps> {
+ // filters all data to just display selected data if brushed (created from an incoming link)
@computed get _tableData() {
if (this.incomingLinks.length! <= 0) return this.props.pairs;
var guids = StrListCast(this.props.layoutDoc.rowGuids);
@@ -52,6 +53,7 @@ export class TableBox extends React.Component<TableBoxProps> {
return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header!='' && header!=undefined) : [];
}
+ // updates the 'selected' field to no longer include rows that aren't in the table
filterSelectedRowsDown() {
if (!this.props.layoutDoc.selected) this.props.layoutDoc.selected = new List<string>();
const selected = Cast(this.props.layoutDoc.selected, listSpec("string"), null);
@@ -88,7 +90,7 @@ export class TableBox extends React.Component<TableBoxProps> {
setupMoveUpEvents(
{},
e,
- e => {
+ e => { // dragging off a column to create a brushed DataVizBox
const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
const targetCreator = (annotationOn: Doc | undefined) => {
const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!);
@@ -144,6 +146,7 @@ export class TableBox extends React.Component<TableBoxProps> {
if (containsData){
return (
<tr key={i} className="table-row" onClick={action(e => {
+ // selecting a row
const selected = Cast(this.props.layoutDoc.selected, listSpec("string"), null);
if (selected.includes(guid)) selected.splice(selected.indexOf(guid), 1);
else {
@@ -153,6 +156,7 @@ export class TableBox extends React.Component<TableBoxProps> {
background: StrListCast(this.props.layoutDoc.selected).includes(guid) ? 'lightgrey' : '' }}>
{this.columns.map(col => (
(this.props.layoutDoc.selected)?
+ // each cell
<td key={this.columns.indexOf(col)} style={{border: '1px solid black'}}>
{p[col]}
</td>
@@ -168,6 +172,7 @@ export class TableBox extends React.Component<TableBoxProps> {
);
}
else return (
+ // when it is a brushed table and the incoming table doesn't have any rows selected
<div className='chart-container'>
Selected rows of data from the incoming DataVizBox to display.
</div>
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
index e1ff6f8eb..10bfb0c64 100644
--- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never
};
export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => {
- console.log('x axis creator being called');
g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15));
};