diff options
author | srichman333 <sarah_n_richman@brown.edu> | 2023-07-26 16:27:12 -0400 |
---|---|---|
committer | srichman333 <sarah_n_richman@brown.edu> | 2023-07-26 16:27:12 -0400 |
commit | 3a177316c1f104da538ed4a53c6a442c6f3fcdc4 (patch) | |
tree | 961b95ecadd466ff8f17b38b002ac24d713aa113 /src | |
parent | 27bf26fe688baabff93ab6ebb6b75af4043d29d3 (diff) |
histogram reorganized to accommodate more data types
Diffstat (limited to 'src')
4 files changed, 86 insertions, 65 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5ef033e35..426eaa14d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1143,7 +1143,7 @@ export namespace Docs { } export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { - return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc); + return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', type: 'dataviz', ...options }, undefined, undefined, undefined, overwriteDoc); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index c07c85f7e..5fbe92563 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -38,6 +38,9 @@ export class Histogram extends React.Component<HistogramProps> { private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef(); private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; private numericalData: boolean = false; + 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; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates @@ -45,7 +48,7 @@ export class Histogram extends React.Component<HistogramProps> { if (this.props.axes.length < 1) return []; if (this.props.axes.length < 2) { var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.pairs[0][ax0])){ this.numericalData = true } + if (/\d/.test(this.props.pairs[0][ax0])){ this.numericalXData = true } 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]])})) @@ -53,7 +56,8 @@ export class Histogram extends React.Component<HistogramProps> { var ax0 = this.props.axes[0]; var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.pairs[0][ax0])) { this.numericalData = true;} + if (/\d/.test(this.props.pairs[0][ax0])) { this.numericalXData = true;} + if (/\d/.test(this.props.pairs[0][ax1]) && this.props.pairs.length < this.maxBins) { this.numericalYData = true;} 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]]) })) @@ -79,7 +83,7 @@ export class Histogram extends React.Component<HistogramProps> { .lastElement(); } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { - if (this.numericalData){ + if (this.numericalXData){ const data = this.data(this._histogramData); return {xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin:0, yMax:0} } @@ -182,7 +186,7 @@ export class Histogram extends React.Component<HistogramProps> { getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ // - title: 'line doc selection' + this._currSelected?.x, + title: 'histogram 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; @@ -227,9 +231,9 @@ export class Histogram extends React.Component<HistogramProps> { }) return valid; }) - var field = dataSet[0]? Object.keys(dataSet[0])[0]: undefined; + var field = dataSet[0]? Object.keys(dataSet[0])[0] : undefined; const data = validData.map((d: { [x: string]: any; }) => { - if (this.numericalData) { return +d[field!].replace(/\$/g, '').replace(/\%/g, '') } + if (this.numericalXData) { return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') } return d[field!] }) return data; @@ -239,26 +243,43 @@ export class Histogram extends React.Component<HistogramProps> { d3.select(this._histogramRef.current).select('svg').remove(); d3.select(this._histogramRef.current).select('.tooltip').remove(); - var field = Object.keys(dataSet[0])[0] - const data = this.data(dataSet); + var data = this.data(dataSet); let uniqueArr: unknown[] = [...new Set(data)] - var numBins = uniqueArr.length - var startingPoint = 0; - var endingPoint = numBins; - var translateXAxis = 0; - if (this.numericalData) { - if (Number.isInteger(this.rangeVals.xMin!)){ - numBins = this.rangeVals.xMax! - this.rangeVals.xMin! + 1; + var numBins = this.numericalXData? (this.rangeVals.xMax! - this.rangeVals.xMin! + 1) : uniqueArr.length + var translateXAxis = !this.numericalXData || numBins<this.maxBins ? width/(numBins+1)/2 : 0; + if (numBins>this.maxBins) numBins = this.maxBins; + var startingPoint = this.numericalXData? this.rangeVals.xMin! : 0; + var endingPoint = this.numericalXData? this.rangeVals.xMax! : numBins; + var xAxisTitle = Object.keys(dataSet[0])[0] + var yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + var histDataSet = 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; + }); + if (!this.numericalXData) { + var histStringDataSet: { frequency: any, label: any }[] = []; + if (this.numericalYData){ + for (let i=0; i<dataSet.length; i++){ + histStringDataSet.push({frequency: dataSet[i][yAxisTitle], label: dataSet[i][xAxisTitle]}) + } + } + else{ + for (let i=0; i<uniqueArr.length; i++){ + histStringDataSet.push({frequency: 0, label: uniqueArr[i]}) + } + for (let i=0; i<data.length; i++){ + let sliceData = histStringDataSet.filter(each => each.label==data[i]) + sliceData[0].frequency = sliceData[0].frequency + 1; + } } - startingPoint = this.rangeVals.xMin!; - endingPoint = this.rangeVals.xMax!; - if (numBins>15) numBins = 15; - else translateXAxis = width/(numBins+1) / 2; + histDataSet = histStringDataSet } - else translateXAxis = width/(numBins+1) / 2; - const svg = (this._histogramSvg = d3 + var svg = (this._histogramSvg = d3 .select(this._histogramRef.current) .append("svg") .attr("class", "graph") @@ -268,32 +289,24 @@ export class Histogram extends React.Component<HistogramProps> { .attr("transform", "translate(" + this.props.margin.left + "," + this.props.margin.top + ")")); - var x: any; - if (this.numericalData){ - x = d3.scaleLinear() - .domain([startingPoint!, endingPoint!]) + var x = d3.scaleLinear() + .domain(this.numericalXData? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width ]); - } - else { - x = d3.scaleLinear() - .domain([0, numBins]) - .range([0, width]); - } - var histogram = d3.histogram() .value(function(d) {return d}) .domain([startingPoint!, endingPoint!]) .thresholds(x.ticks(numBins-1)) var bins = histogram(data) var eachRectWidth = width/(bins.length) - var graphStartingPoint = bins[0].x1! - (bins[1].x1! - bins[1].x0!) + var graphStartingPoint = bins[0].x1? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0; bins[0].x0 = graphStartingPoint; x = x.domain([graphStartingPoint, endingPoint]) .range([0, Number.isInteger(this.rangeVals.xMin!)? (width-eachRectWidth) : width ]) var xAxis; - if (!this.numericalData) { // if the data is strings rather than numbers - uniqueArr.sort() + if (!this.numericalXData) { // reorganize if the data is strings rather than numbers + // uniqueArr.sort() + histDataSet.sort() for (let i=0; i<data.length; i++){ var index = 0 for (let j=0; j<uniqueArr.length; j++){ @@ -303,23 +316,27 @@ export class Histogram extends React.Component<HistogramProps> { } bins[index].push(data[i]) } + bins.pop(); + eachRectWidth = width/(bins.length) bins.forEach(d => d.x0 = d.x0!) xAxis = d3.axisBottom(x) - .ticks(numBins-1) + .ticks(bins.length-1) .tickFormat( i => uniqueArr[i]) .tickPadding(10) + x.range([0, width-eachRectWidth]) + x.domain([0, bins.length-1]) translateXAxis = eachRectWidth / 2; } else { xAxis = d3.axisBottom(x) .ticks(numBins-1) } - - const maxFrequency = d3.max(bins, function(d) { return d.length; }) + const maxFrequency = this.numericalYData? d3.max(histDataSet, function(d) {return d.frequency.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!]); + y.domain([0, +maxFrequency!]); var yAxis = d3.axisLeft(y) .ticks(maxFrequency!) svg.append("g") @@ -337,16 +354,14 @@ export class Histogram extends React.Component<HistogramProps> { var left = this.numericalData? d.x0-1: d.x0; var right = (this.numericalData && d.x0!=d.x1)? d.x1-1: d.x1; if ((left*eachRectWidth ) <= pointerX && pointerX <= (right*eachRectWidth )){ - // var showSelected = !this.numericalData? dataSet[index] : this.props.pairs[index]; - var showSelected = dataSet[index] - showSelected['frequency'] = d.length; - console.log('showSelected', showSelected) - console.log('current', this._currSelected) + var showSelected = histDataSet[index] + var selectedDisplay = {[xAxisTitle]: showSelected.label, [yAxisTitle]: showSelected.frequency} + // showSelected['frequency'] = d.length; 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]]) + (selectedDisplay[xAxisTitle]==this._currSelected![xAxisTitle] + && selectedDisplay[yAxisTitle]==this._currSelected![yAxisTitle]) : false; - this._currSelected = sameAsCurrent? undefined: showSelected; + this._currSelected = sameAsCurrent? undefined: selectedDisplay; return true } return false; @@ -358,30 +373,37 @@ export class Histogram extends React.Component<HistogramProps> { } if (!sameAsCurrent!) selected.attr('class', 'histogram-bar hover'); }); - svg.on('click', onPointClick); - // axis titles svg.append("text") .attr("transform", "translate(" + (width/2) + " ," + (height+40) + ")") .style("text-anchor", "middle") - .text(field); + .text(xAxisTitle); svg.append("text") - .attr("transform", "rotate(-90)") + .attr("transform", "rotate(-90)" + " " + "translate( 0, " + -10 + ")") .attr("x", -(height/2)) .attr("y", -20) .style("text-anchor", "middle") - .text('frequency'); - + .text(yAxisTitle); d3.format('.0f') - svg.selectAll("rect") .data(bins) .enter() .append("rect") - .attr("transform", function(d) { return "translate(" + x(d.x0!) + "," + y(d.length) + ")"; }) + .attr("transform", this.numericalYData? + function (d) { + var eachData = histDataSet.filter((data: { label: number; }) => {return data.label==d[0]}) + var length = eachData[0].frequency.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); + return "translate(" + x(d.x0!) + "," + y(length) + ")"; + } + : function(d) { return "translate(" + x(d.x0!) + "," + y(d.length) + ")"; }) + .attr("height", this.numericalYData? + function(d) { + var eachData = histDataSet.filter((data: { label: number; }) => {return data.label==d[0]}) + var length = eachData[0].frequency.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); + return height-y(length)} + : function(d) { return height - y(d.length)}) .attr("width", eachRectWidth) - .attr("height", function(d) { return height - y(d.length); }) .attr("class", 'histogram-bar') }; diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 637ed0ead..91baf095d 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -310,7 +310,7 @@ export class LineChart extends React.Component<LineChartProps> { .style("text-anchor", "middle") .text(this.props.axes[0]); svg.append("text") - .attr("transform", "rotate(-90)") + .attr("transform", "rotate(-90)" + " " + "translate( 0, " + -10 + ")") .attr("x", -(height/2)) .attr("y", -20) .attr("height", 20) diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 37c435411..fc24e5821 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -182,7 +182,7 @@ export class PieChart extends React.Component<PieChartProps> { getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ // - title: 'line doc selection' + this._currSelected?.x, + title: 'piechart 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; @@ -260,12 +260,6 @@ export class PieChart extends React.Component<PieChartProps> { }) return valid; }); - - var pie = d3.pie(); - var arc = d3.arc() - .innerRadius(0) - .outerRadius(radius); - if (this.byCategory){ let uniqueCategories = [...new Set(data)] var pieStringDataSet: { frequency: any, label: any }[] = []; @@ -282,6 +276,11 @@ export class PieChart extends React.Component<PieChartProps> { var trackDuplicates : {[key: string]: any} = {}; data.forEach((eachData: any) => !trackDuplicates[eachData]? trackDuplicates[eachData] = 0: null) + var pie = d3.pie(); + var arc = d3.arc() + .innerRadius(0) + .outerRadius(radius); + const onPointClick = action((e: any) => { // check the 4 'corners' of each slice and see if the pointer is within those bounds to get the slice the user clicked on const pointer = d3.pointer(e); |