import * as React from 'react'; import * as d3 from 'd3'; import { copyTransforms, RefinerProps } from './utils'; import { debounce } from 'lodash'; interface NumericalState { minValue?: number; maxValue?: number; minDisplay?: string; maxDisplay?: string; minWarn?: boolean; maxWarn?: boolean; isMaxFocused?: boolean; isMinFocused?: boolean; minInput?: number; maxInput?: number; } /** * Column Component to add filter/refinement functionality to a numerical table column. */ export class ColumnNumericalRefinerComponent extends React.Component< RefinerProps, NumericalState > { transformUpdateMinDebounced; transformUpdateMaxDebounced; numberFormatter; min; max; constructor(props) { super(props); if (props.config.numberFormatter) { this.numberFormatter = props.config.numberFormatter; } else { this.numberFormatter = d3.format('.2s'); } this.min = this.props?.data?.extent?.min; this.max = this.props?.data?.extent?.max; this.state = { minDisplay: '', maxDisplay: '', minWarn: false, maxWarn: false, isMaxFocused: false, isMinFocused: false, minInput: undefined, maxInput: undefined, }; // debounce updating the transforms with input value const delay = this.props.transformsDelay === undefined ? 1000 : this.props.transformsDelay; const transformUpdateMinHandler = () => { this.updateTransforms('minRange', this.state.minInput); }; this.transformUpdateMinDebounced = debounce( transformUpdateMinHandler, delay ); const transformUpdateMaxHandler = () => { this.updateTransforms('maxRange', this.state.maxInput); }; this.transformUpdateMaxDebounced = debounce( transformUpdateMaxHandler, delay ); } updateIsFilteredState() { const hasMinInput = this.state.minInput !== undefined, hasMaxInput = this.state.maxInput !== undefined, isFiltered = hasMinInput || hasMaxInput; this.props.setIsFiltered(isFiltered); } updateTransforms(type, value) { let config = this.props.config, newTransforms, refinementTransform; // make a complete copy of the transforms newTransforms = copyTransforms(this.props.transforms); if ( Object.prototype.hasOwnProperty.call( newTransforms.filters, config.columnName ) ) { refinementTransform = newTransforms.filters[config.columnName]; } else { refinementTransform = { type: 'range', column: config.columnName, }; } refinementTransform[type] = value; newTransforms.filters[config.columnName] = refinementTransform; this.props.setTransforms(newTransforms); } clearInputs = () => { this.setState( { minDisplay: '', maxDisplay: '', minWarn: false, maxWarn: false, isMaxFocused: false, isMinFocused: false, minInput: undefined, maxInput: undefined, }, () => { this.updateIsFilteredState(); } ); this.transformUpdateMinDebounced(); this.transformUpdateMaxDebounced(); }; onMinFocus = (isFocused) => { let minInput, minDisplay; if (!isFocused) { minInput = this.state.minInput; if (!isNaN(minInput)) { if (minInput < this.props?.data?.extent?.min) { minInput = this.props?.data?.extent?.min; } else if ( !isNaN(this.state.maxInput) && minInput > this.state.maxInput ) { minInput = this.state.maxInput; } else if (minInput > this.props?.data?.extent?.max) { minInput = this.props?.data?.extent?.max; } minDisplay = this.numberFormatter(minInput); this.setState( { minDisplay: minDisplay, minInput: minInput, isMinFocused: isFocused, minWarn: false, }, () => { this.updateIsFilteredState(); } ); this.transformUpdateMinDebounced(); } else { this.setState( { isMinFocused: isFocused, minWarn: false, }, () => { this.updateIsFilteredState(); } ); } } else { this.setState( { isMinFocused: isFocused, }, () => { this.updateIsFilteredState(); } ); } }; onMaxFocus = (isFocused) => { let maxInput, maxDisplay; if (!isFocused) { maxInput = this.state.maxInput; if (!isNaN(maxInput)) { if (maxInput > this.props?.data?.extent?.max) { maxInput = this.props?.data?.extent?.max; } else if ( !isNaN(this.state.minInput) && maxInput < this.state.minInput ) { maxInput = this.state.minInput; } else if (maxInput < this.props?.data?.extent?.min) { maxInput = this.props?.data?.extent?.min; } maxDisplay = this.numberFormatter(maxInput); this.setState( { maxDisplay: maxDisplay, maxInput: maxInput, isMaxFocused: isFocused, maxWarn: false, }, () => { this.updateIsFilteredState(); } ); this.transformUpdateMaxDebounced(); } else { this.setState( { isMaxFocused: isFocused, maxWarn: false, }, () => { this.updateIsFilteredState(); } ); } } else { this.setState( { isMaxFocused: isFocused, }, () => { this.updateIsFilteredState(); } ); } }; onMaxInput = (evt) => { const v: number = parseFloat(evt.target.value); let maxInput, maxDisplay, maxWarn = false; if (!isNaN(v)) { maxInput = v; maxWarn = maxInput < this.props?.data?.extent?.min || maxInput < this.state.minInput || maxInput > this.props?.data?.extent?.max; maxDisplay = this.numberFormatter(v); } else { maxInput = undefined; maxDisplay = 'Max'; maxWarn = true; } // debounce updating the transforms with value if (!maxWarn || maxInput == undefined) { this.transformUpdateMaxDebounced(); } this.setState( { maxInput: maxInput, maxDisplay: maxDisplay, maxWarn: maxWarn, }, () => { this.updateIsFilteredState(); } ); }; onMinInput = (evt) => { const v: number = parseFloat(evt.target.value); let minInput, minDisplay, minWarn = false; if (!isNaN(v)) { minInput = v; minWarn = minInput < this.props?.data?.extent?.min || minInput > this.props?.data?.extent?.max || minInput > this.state.maxInput; minDisplay = this.numberFormatter(v); } else { minInput = undefined; minDisplay = 'Min'; minWarn = true; } // debounce updating the transforms with value if (!minWarn || minInput == undefined) { this.transformUpdateMinDebounced(); } this.setState( { minInput: minInput, minDisplay: minDisplay, minWarn: minWarn, }, () => { this.updateIsFilteredState(); } ); }; /** * Render Column Component */ render() { const baseClasses: Array = [ 'form-control', 'mmui-form-control', 'mmui-range-refiner-input', ]; let minInputClasses: string = baseClasses.join(' '); if (this.state.minWarn) { minInputClasses += ' is-invalid'; } let maxInputClasses: string = baseClasses.join(' '); if (this.state.maxWarn) { maxInputClasses += ' is-invalid'; } let minInput, maxInput; if (this.state.isMinFocused) { minInput = ( ); } else { minInput = ( ); } if (this.state.isMaxFocused) { maxInput = ( ); } else { maxInput = ( ); } let defaultLabel; if (this.props.displayDefaultLabel) { if ( this.min != this.props?.data?.extent?.min || this.max != this.props?.data?.extent?.max ) { defaultLabel = (

Default: {this.numberFormatter(this.min)} to {this.numberFormatter(this.max)}

); } } return ( {defaultLabel}
{minInput}
{maxInput}
); } }