aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/components/Histogram.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-05-05 12:37:09 -0400
committerbobzel <zzzman@gmail.com>2025-05-05 12:37:09 -0400
commit3a733aa0fd24517e83649824dec0fc8bcc0bde43 (patch)
treeac01848cdab3b83582c0b7ab6f3d2b1c8187a24f /src/client/views/nodes/DataVizBox/components/Histogram.tsx
parente058d227ccbce47c86b0fa558adb01dfccaf4d60 (diff)
parentd4659e2bd3ddb947683948083232c26fb1227f39 (diff)
Merge branch 'master' into joanne-tutorialagent
Diffstat (limited to 'src/client/views/nodes/DataVizBox/components/Histogram.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx472
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}
/>
&nbsp;
@@ -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}