aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/Histogram.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx120
1 files changed, 29 insertions, 91 deletions
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()