diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/components/Histogram.tsx | 472 |
1 files changed, 190 insertions, 282 deletions
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 5a9442d2f..a7c4a00b0 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,27 +1,28 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ColorPicker, EditableText, IconButton, Size, Type } from '@dash/components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, makeObservable, observable } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; -import { listSpec } from '../../../../../fields/Schema'; -import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; -import { PinProps, PinDocView } from '../../../PinFuncs'; +import { PinDocView, PinProps } from '../../../PinFuncs'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; import './Chart.scss'; +export interface HistogramData { + [key: string]: string | number; +} export interface HistogramProps { - Document: Doc; + Doc: Doc; layoutDoc: Doc; axes: string[]; - titleCol: string; - records: { [key: string]: any }[]; + records: HistogramData[]; width: number; height: number; dataDoc: Doc; @@ -39,64 +40,85 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _histogramRef: HTMLDivElement | null = null; private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - 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; // Object of selected bar - private curBarSelected: any = undefined; // histogram bin of selected bar for when just one bar is selected - private selectedData: any[] = []; // array of selected bars - private hoverOverData: any = undefined; // Selection of bar being hovered over - - constructor(props: any) { + 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 + private _selectedBars: HistogramData[] = []; + @observable private _currSelected: { [key: string]: string | number; frequency: number } | undefined = undefined; + + constructor(props: HistogramProps) { super(props); makeObservable(this); } + @computed get xAxis() { + return this._props.axes[0]; + } + + @computed get yAxis() { + return this._props.axes[1]; + } + + @computed get Doc() { + return this._props.Doc; + } + @computed get layoutDoc() { + return this._props.layoutDoc; + } + @computed get _tableDataIds() { return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); } // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) - @computed get _tableData() { + + @computed get _tableData(): Record<string, string | number>[] { return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]); } - // filters all data to just display selected data if brushed (created from an incoming link) - @computed get _histogramData() { + + @computed get _histogramData(): HistogramData[] { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { - const ax0 = this._props.axes[0]; - if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { - this.numericalXData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) { + this._numericalXData = true; } - return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); + return this._tableData.map(record => ({ [this.xAxis]: record[this.xAxis] })); } - const [ax0, ax1] = this._props.axes; - if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { - this.numericalXData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) { + this._numericalXData = true; } - if (!/[A-Za-z-:]/.test(this._props.records[0][ax1])) { - this.numericalYData = true; + if (!/[A-Za-z-:]/.test(this._props.records[0][this.yAxis] as string)) { + this._numericalYData = true; } - return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); + return this._tableData.map(record => ({ + [this.xAxis]: record[this.xAxis], + [this.yAxis]: record[this.yAxis], + })); } - @computed get defaultGraphTitle() { - const [ax0, ax1] = this._props.axes; - if (this._props.axes.length < 2 || !ax1 || !/\d/.test(this._props.records[0][ax1]) || !this.numericalYData) { - return ax0 + ' Histogram'; + @computed get defaultGraphTitle(): string { + if (!this.yAxis || !/\d/.test(this._props.records[0][this.yAxis] as string) || !this._numericalYData) { + return this.xAxis + ' Histogram'; } - return ax0 + ' by ' + ax1 + ' Histogram'; + return this.xAxis + ' by ' + this.yAxis + ' Histogram'; } @computed get parentViz() { - return DocCast(this._props.Document.dataViz_parentViz); + return DocCast(this._props.Doc.dataViz_parentViz); + } + + @computed get defaultBarColor() { + return StrCast(this.layoutDoc.dataViz_histogram_defaultColor, '#69b3a2')!; + } + @computed get barColors() { + return StrListCast(this.layoutDoc.dataViz_histogram_barColors); + } + @computed get selectedBins() { + return NumListCast(this.layoutDoc.dataViz_histogram_selectedBins); } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { - 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 }; - } - return { xMin: 0, xMax: 0, yMin: 0, yMax: 0 }; + const data = this._numericalXData ? this.data(this._histogramData) : [0]; + return { xMin: Math.min(...data), xMax: Math.max(...data), yMin: 0, yMax: 0 }; } componentWillUnmount() { @@ -104,21 +126,31 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } componentDidMount() { // restore selected bars - const svg = this._histogramSvg; - if (svg) { - const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_histogram_selectedData); - svg.selectAll('rect').attr('class', (d: any) => { - let selected = false; - selectedDataBars.forEach(eachSelectedData => { - if (d[0] === eachSelectedData) selected = true; - }); - if (selected) { - this.selectedData.push(d); - return 'histogram-bar hover'; - } - return 'histogram-bar'; - }); - } + this._histogramSvg?.selectAll('rect').attr('class', dIn => { + const d = dIn as HistogramData; + if (this.selectedBins?.some(selBin => d[0] === selBin)) { + this._selectedBars.push(d); + return 'histogram-bar hover'; + } + return 'histogram-bar'; + }); + // setup filters to watch selections and filter toggle + this._disposers.selection = reaction( + () => ({ filter: this.layoutDoc.dataViz_filterSelection, hists: this._selectedBars.slice(), cur: this._currSelected }), + ({ filter, hists }) => { + this.layoutDoc.dataViz_selectedRows = !filter + ? undefined + : new List<number>( + this._tableDataIds.filter(rowID => + hists.some(h => { + const val = this._props.records[rowID][this.xAxis]; + return val == h.x0 || (+val >= +h.x0 && +val <= +h.x1); + }) + ) + ); + }, + { fireImmediately: true } + ); } // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) @@ -126,7 +158,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const anchor = Docs.Create.ConfigDocument({ title: 'histogram doc selection' + this._currSelected, }); - PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Doc); return anchor; }; @@ -139,110 +171,92 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } // cleans data by converting numerical data to numbers and taking out empty cells - data = (dataSet: any) => { - const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); + data = (dataSet: HistogramData[]): number[] => { + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number))); const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; return !field ? [] - : validData.map((d: { [x: string]: any }) => - !this.numericalXData // - ? d[field] - : +d[field!].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + : validData.map(d => + !this._numericalXData // + ? (d[field] as number) + : +d[field].toString().replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') ); }; + barLabel = (d: d3.Bin<number, number> | HistogramData) => '' + (Array.isArray(d) ? d[0] : d[0]); + // outlines the bar selected / hovered over - highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => { + highlightSelectedBar = (changeSelectedVariables: boolean, svg: d3.Selection<SVGGElement, unknown, null, undefined>, eachRectWidth: number, pointerX: number, xAxisTitle: string, yAxisTitle: string, histDataSet: HistogramData[]) => { let barCounter = -1; - const selected = svg.selectAll('.histogram-bar').filter((d: any) => { + let hoverOverBar: HistogramData | undefined; + svg.selectAll('.histogram-bar').filter(dIn => { + const d = dIn as HistogramData; barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over - if (d.length && barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) { - let showSelected = this.numericalYData - ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(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 (d.length && (barCounter * eachRectWidth <= pointerX + 1 || (!barCounter && pointerX <= 0)) && pointerX - 1 <= (barCounter + 1) * eachRectWidth) { if (changeSelectedVariables) { // for when a bar is selected - not just hovered over - let sameAsAny = false; - const selectedDataBars = Cast(this._props.layoutDoc.dataViz_histogram_selectedData, listSpec('number'), null); - this.selectedData.forEach(eachData => { - if (!sameAsAny) { - let match = true; - Object.keys(d).forEach(key => { - if (d[key] !== eachData[key]) match = false; - }); - if (match) { - sameAsAny = true; - const index = this.selectedData.indexOf(eachData); - this.selectedData.splice(index, 1); - selectedDataBars.splice(index, 1); - this._currSelected = undefined; - } - } - }); - if (!sameAsAny) { - this.selectedData.push(d); - selectedDataBars.push(d[0]); - this._currSelected = this.selectedData.length > 1 ? undefined : showSelected; + const alreadySelected = this._selectedBars.findIndex(eachData => !Object.keys(d).some(key => d[key] !== eachData[key])); + if (alreadySelected !== -1) { + this._selectedBars.splice(alreadySelected, 1); + this.selectedBins?.splice(alreadySelected, 1); + } else { + this._selectedBars.push(d); + this.selectedBins?.push(d[0] as number); } + const showSelectedLabel = (dataset: HistogramData[]) => { + const datum = dataset.lastElement(); + const datumNum = datum as unknown as number[]; + const showSelectedStart = this._numericalYData + ? this._histogramData.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0] + : histDataSet.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]; - // for filtering child dataviz docs - if (this._props.layoutDoc.dataViz_filterSelection) { - const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); - this._tableDataIds.forEach(rowID => { - let match = false; - for (let i = 0; i < d.length; i++) { - console.log('Compare: ' + this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') + ' = ' + d[i]); - if (this._props.records[rowID][xAxisTitle].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[i]) match = true; - } - if (match && !selectedRows?.includes(rowID)) - selectedRows?.push(rowID); // adding to filtered rows - else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows - }); - } - } else this.hoverOverData = d; + const selectionLabel = dataset.length > 1 + ? dataset.map(dd => this.barLabel(dd)).join('::') + : !this._numericalXData + ? this.barLabel(d) + : datum[0] !== undefined && datum[1] && datum[0] !== datum[1] + ? d3.min(datumNum) + ' to ' + d3.max(datumNum) + : !this._numericalYData + ? showSelectedStart?.[xAxisTitle] + : this.barLabel(d); // prettier-ignore + return { [xAxisTitle]: selectionLabel, frequency: dataset.length > 1 ? Number.NaN : datum.length } as { [key: string]: string | number; frequency: number }; + }; + this._currSelected = this._selectedBars.length ? showSelectedLabel(this._selectedBars) : undefined; + } else hoverOverBar = d; return true; } return false; }); - if (changeSelectedVariables) { - if (this._currSelected) this.curBarSelected = selected; - else this.curBarSelected = undefined; - } + return hoverOverBar; }; // draws the histogram - drawChart = (dataSet: any, width: number, height: number) => { + drawChart = (dataSet: HistogramData[], width: number, height: number) => { if (dataSet?.length <= 0) return; d3.select(this._histogramRef).select('svg').remove(); d3.select(this._histogramRef).select('.tooltip').remove(); const data = this.data(dataSet); const xAxisTitle = Object.keys(dataSet[0])[0]; - const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + const yAxisTitle = this._numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; const uniqueArr: unknown[] = [...new Set(data)]; - let numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; - let translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; - if (numBins > this.maxBins) numBins = this.maxBins; - const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; - const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; + let numBins = this._numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; + let translateXAxis = !this._numericalXData || numBins < this._maxBins ? width / (numBins + 1) / 2 : 0; + if (numBins > this._maxBins) numBins = this._maxBins; + const startingPoint = this._numericalXData ? this.rangeVals.xMin! : 0; + const endingPoint = this._numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as any))); - if (!this.numericalXData) { - const histStringDataSet: { [x: string]: unknown }[] = []; - if (this.numericalYData) { + let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number))); + if (!this._numericalXData) { + const histStringDataSet: { [x: string]: number }[] = []; + if (this._numericalYData) { for (let i = 0; i < dataSet.length; i++) { - histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] }); + histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle] as number, [xAxisTitle]: dataSet[i][xAxisTitle] as number }); } } else { for (let i = 0; i < uniqueArr.length; i++) { - histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] }); + histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] as number }); } for (let i = 0; i < data.length; i++) { const barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]); @@ -263,12 +277,12 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')')); let x = d3 .scaleLinear() - .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) + .domain(this._numericalXData ? [startingPoint!, endingPoint!] : [0, numBins]) .range([0, width]); const histogram = d3 - .histogram() + .bin() .value(d => d) - .domain([startingPoint!, endingPoint!]) + .domain([startingPoint, endingPoint]) .thresholds(x.ticks(numBins)); const bins = histogram(data); let eachRectWidth = width / bins.length; @@ -279,7 +293,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // more calculations based on bins // x-axis - if (!this.numericalXData) { + if (!this._numericalXData) { // reorganize to match data if the data is strings rather than numbers // uniqueArr.sort() histDataSet.sort(); @@ -294,9 +308,6 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } bins.pop(); eachRectWidth = width / bins.length; - bins.forEach(d => { - d.x0 = d.x0!; - }); xAxis = d3 .axisBottom(x) .ticks(bins.length > 1 ? bins.length - 1 : 1) @@ -329,15 +340,15 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { x.range([0, width - eachRectWidth]); } // y-axis - const maxFrequency = this.numericalYData ? - d3.max(histDataSet, (d: any) => (d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '') - .replace(/%/g, '').replace(/</g, '')) : 0)) : + const maxFrequency = this._numericalYData ? + d3.max(histDataSet, d => (d[yAxisTitle] ? + Number(StrCast(d[yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) : 0)) : d3.max(bins, d => d.length); // prettier-ignore const y = d3.scaleLinear().range([height, 0]); - y.domain([0, +maxFrequency!]); + y.domain([0, maxFrequency ?? 0]); const yAxis = d3.axisLeft(y).ticks(maxFrequency!); - if (this.numericalYData) { - const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0); + if (this._numericalYData) { + const yScale = scaleCreatorNumerical(0, maxFrequency ?? 0, height, 0); yAxisCreator(svg.append('g'), width, yScale); } else { svg.append('g').call(yAxis); @@ -347,29 +358,14 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { .call(xAxis); // click/hover + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateHighlights = (hoverOverBar?: HistogramData) => svg.selectAll('rect').attr('class', (d: any) => 'histogram-bar' + (hoverOverBar?.[0] == d[0] || this._selectedBars.some(hist => d[0] === hist[0]) ? ' hover' : '')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); - const onHover = action((e: any) => { - this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); - // eslint-disable-next-line no-use-before-define - updateHighlights(); - }); - const mouseOut = action(() => { - this.hoverOverData = undefined; - // eslint-disable-next-line no-use-before-define - updateHighlights(); - }); - const updateHighlights = () => { - const hoverOverBar = this.hoverOverData; - const { selectedData } = this; - svg.selectAll('rect').attr('class', (d: any) => { - let selected = false; - selectedData.forEach(eachSelectedData => { - if (d[0] === eachSelectedData[0]) selected = true; - }); - return (hoverOverBar && hoverOverBar[0] == d[0]) || selected ? 'histogram-bar hover' : 'histogram-bar'; - }); - }; - svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mouseEnter = (e: any) => updateHighlights(this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); + svg.on('click', onPointClick).on('pointerenter', mouseEnter).on('pointerleave', updateHighlights); // axis titles svg.append('text') @@ -385,138 +381,54 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { d3.format('.0f'); // draw bars - const selected = this.selectedData; + const selected = this._selectedBars; svg.selectAll('rect') .data(bins) .enter() .append('rect') - .attr( - 'transform', - this.numericalYData + .attr('transform', this._numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const eachData = histDataSet.filter((hData: HistogramData) => hData[xAxisTitle] == d[0]); const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')'; } - : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')' - ) - .attr( - 'height', - this.numericalYData + : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')') + .attr('height', this._numericalYData ? d => { - const eachData = histDataSet.filter((hData: { [x: string]: number }) => hData[xAxisTitle] == d[0]); + const eachData = histDataSet.filter(hData => hData[xAxisTitle] == d[0]); const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0; - return height - y(Number(length)); + return height - y(+length); } - : d => height - y(d.length) - ) + : d => height - y(d.length)) .attr('width', eachRectWidth) - .attr('class', selected ? d => (selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') - .attr('fill', d => { - let barColor; - const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; - else { - const range = StrCast(each[0]).split(' to '); - // eslint-disable-next-line prefer-destructuring - if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; - } - }); - return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); - }); - }; - - @action changeSelectedColor = (color: string) => { - this.curBarSelected.attr('fill', color); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - - const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); - barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); - barColors.push(StrCast(barName + '::' + color)); + .attr('class', selected ? d => (selected?.[0]?.x0 == d.x0 ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar') + .attr('fill', d => this.barColors?.map(bar => bar.split('::')).find(([barLabel]) => barLabel === this.barLabel(d))?.[1] ?? this.defaultBarColor); // prettier-ignore }; - @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this._props.layoutDoc.dataViz_histogram_defaultColor); - const barName = StrCast(this._currSelected[this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - - const barColors = Cast(this._props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); - barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); - }; - - // reloads the bar colors and selected bars - updateSavedUI = () => { - const svg = this._histogramSvg; - if (svg) { - // bar color - svg.selectAll('rect').attr('fill', (d: any) => { - let barColor; - const barColors = StrListCast(this._props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; - else { - const range = StrCast(each[0]).split(' to '); - // eslint-disable-next-line prefer-destructuring - if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; - } - }); - return barColor ? StrCast(barColor) : StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor); - }); - } + @action changeSelectedColor = (color: string, erase?: boolean) => { + if (!this.barColors) this.layoutDoc.dataViz_histogram_barColors = new List<string>(); + this._selectedBars.map(this.barLabel).forEach(barName => { + this.barColors.forEach(bar => bar.split('::')[0] === barName && this.barColors.splice(this.barColors.indexOf(bar), 1)); + !erase && this.barColors.push(barName + '::' + color); + }); }; render() { - this.updateSavedUI(); - this._histogramData; - let curSelectedBarName = ''; - let titleAccessor: any = 'dataViz_histogram_title'; - if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0]; - if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; - if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); - if (!this._props.layoutDoc.dataViz_histogram_selectedData) this._props.layoutDoc.dataViz_histogram_selectedData = new List<string>(); - let selected = 'none'; - if (this._currSelected) { - curSelectedBarName = StrCast(this._currSelected![this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')); - selected = '{ '; - Object.keys(this._currSelected).forEach(key => { - key // - ? (selected += key + ': ' + this._currSelected[key] + ', ') - : ''; - }); - selected = selected.substring(0, selected.length - 2) + ' }'; - if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) { - selected += '\n' + this._props.titleCol + ': '; - this._tableData.forEach(each => { - if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) { - if (this._props.axes[1]) { - if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', '; - } else selected += each[this._props.titleCol] + ', '; - } - }); - selected = selected.slice(0, -1).slice(0, -1); - } - } - let selectedBarColor; - const barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.forEach(each => { - // eslint-disable-next-line prefer-destructuring - each[0] === curSelectedBarName && (selectedBarColor = each[1]); - }); + if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>(); + + const titleAccessor = 'dataViz_histogram_title ' + this.xAxis + (this.yAxis ? '-' + this._props.axes[1] : ''); + const selected = !this._currSelected ? 'none' : '{ ' + Object.keys(this._currSelected).map(key => key ? key + ': ' + this._currSelected?.[key]:'').join(", ") + ' }'; // prettier-ignore + const curSelectedBarName = this._selectedBars.length && this.barLabel(this._selectedBars.lastElement()); //.[this.xAxis]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, ''); + const selectedBarColor = this.barColors?.map(bar => bar.split('::'))?.find(([barLabel]) => barLabel === curSelectedBarName)?.[1]; if (this._histogramData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> <div className="graph-title"> <EditableText - val={StrCast(this._props.layoutDoc[titleAccessor])} + val={StrCast(this.layoutDoc[titleAccessor], this.defaultGraphTitle)} setVal={undoable( - action(val => { - this._props.layoutDoc[titleAccessor] = val as string; - }), + action(val => (this.layoutDoc[titleAccessor] = val)), 'Change Graph Title' )} color="black" @@ -528,13 +440,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { tooltip="Change Default Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={StrCast(this._props.layoutDoc.dataViz_histogram_defaultColor)} - setFinalColor={undoable(color => { - this._props.layoutDoc.dataViz_histogram_defaultColor = color; - }, 'Change Default Bar Color')} - setSelectedColor={undoable(color => { - this._props.layoutDoc.dataViz_histogram_defaultColor = color; - }, 'Change Default Bar Color')} + selectedColor={this.defaultBarColor} + setFinalColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setSelectedColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} size={Size.XSMALL} /> </div> @@ -552,9 +460,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { tooltip="Change Bar Color" type={Type.SEC} icon={<FaFillDrip />} - selectedColor={selectedBarColor || this.curBarSelected.attr('fill')} - setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} - setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')} + selectedColor={selectedBarColor} + setFinalColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')} + setSelectedColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')} size={Size.XSMALL} /> @@ -564,7 +472,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { color="black" type={Type.SEC} tooltip="Revert to the default bar color" // - onClick={undoable(this.eraseSelectedColor, 'Change Selected Bar Color')} + onClick={undoable(() => this.changeSelectedColor(this.defaultBarColor, true), 'Change Selected Bar Color')} /> </div> ) : null} |