diff --git a/hiplot/experiment.py b/hiplot/experiment.py index 8925fc3f..bb11cc8d 100644 --- a/hiplot/experiment.py +++ b/hiplot/experiment.py @@ -191,6 +191,9 @@ class Experiment(_DictSerializable): :ivar datapoints: All the measurements we have. One datapoint corresponds to one line in the parallel plot and to one line in the table. :ivar parameters_definition: Characteristics of the columns (ordering, type, etc...) + :ivar colormap: Colormap to use + :ivar colorby: Default column to color by + :ivar weightcolumn: If rows have different weights, use this column as the weight (default to 1 if not specified) :Example: @@ -212,6 +215,7 @@ def __init__(self, self.parameters_definition = parameters_definition if parameters_definition is not None else defaultdict(ValueDef) self.colormap = colormap if colormap is not None else "interpolateTurbo" self.colorby: tp.Optional[str] = None + self.weightcolumn: tp.Optional[str] = None self._display_data: tp.Dict[str, tp.Dict[str, tp.Any]] = {} self._compress: bool = False @@ -394,6 +398,7 @@ def _asdict(self) -> tp.Dict[str, tp.Any]: "parameters_definition": {k: v._asdict() for k, v in self.parameters_definition.items()}, "colormap": self.colormap, "colorby": self.colorby, + "weightcolumn": self.weightcolumn, "display_data": self._display_data, } if self._compress: diff --git a/hiplot/fetchers_demo.py b/hiplot/fetchers_demo.py index cf24bb10..b7b48cb3 100644 --- a/hiplot/fetchers_demo.py +++ b/hiplot/fetchers_demo.py @@ -296,6 +296,20 @@ def demo_first_value_nan() -> hip.Experiment: ]) +def demo_weighted_rows() -> hip.Experiment: + experiment = hip.Experiment.from_iterable([ + {'w': 1.0, 'a': 1, 'b': 1}, + {'w': 2.0, 'a': 2, 'b': 1}, + {'w': -2.0, 'a': 2, 'b': 1}, + {'w': math.inf, 'a': 2, 'b': 2}, + {'w': 'not_a_number', 'a': 2, 'b': 3}, + {'w': None, 'a': 3, 'b': 3}, + {'a': 4, 'b': 3}, + ]) + experiment.weightcolumn = "w" + return experiment + + README_DEMOS: t.Dict[str, t.Callable[[], hip.Experiment]] = { "demo": demo, "demo_big": lambda: demo(1000), @@ -318,4 +332,5 @@ def demo_first_value_nan() -> hip.Experiment: "demo_force_constant_pplot": demo_force_constant_pplot, "demo_color_interpolate_inverse": demo_color_interpolate_inverse, "demo_first_value_nan": demo_first_value_nan, + "demo_weighted_rows": demo_weighted_rows, } diff --git a/src/component.tsx b/src/component.tsx index c5b85a29..54a80506 100644 --- a/src/component.tsx +++ b/src/component.tsx @@ -536,6 +536,7 @@ export class HiPlot extends React.Component {
) => void; @@ -33,13 +34,12 @@ interface HeaderBarProps extends IDatasets, HiPlotDataControlProps { interface HeaderBarState { isTextareaFocused: boolean; hasTutorial: boolean; + selectedPct: string; + selectedPctWeighted: string; }; export class HeaderBar extends React.Component { dataProviderRef = React.createRef(); - selected_count_ref: React.RefObject = React.createRef(); - selected_pct_ref: React.RefObject = React.createRef(); - total_count_ref: React.RefObject = React.createRef(); controls_root_ref: React.RefObject = React.createRef(); constructor(props: HeaderBarProps) { @@ -47,23 +47,51 @@ export class HeaderBar extends React.Component { this.state = { isTextareaFocused: false, hasTutorial: false, + selectedPct: '???', + selectedPctWeighted: '???', }; } recomputeMetrics() { - if (!this.selected_count_ref.current) { + const newSelectedPct = (100 * this.props.rows_selected.length / this.props.rows_filtered.length).toPrecision(3); + if (newSelectedPct != this.state.selectedPct) { + this.setState({ + selectedPct: (100 * this.props.rows_selected.length / this.props.rows_filtered.length).toPrecision(3) + }); + } + } + recomputeSelectedWeightedSum() { + if (!this.props.weightColumn) { + this.setState({ + selectedPctWeighted: '???', + }); return; } - const selected_count = this.props.rows_selected.length; - const total_count = this.props.rows_filtered.length; - this.selected_count_ref.current.innerText = '' + selected_count; - this.selected_pct_ref.current.innerText = '' + (100 * selected_count / total_count).toPrecision(3); - this.total_count_ref.current.innerText = '' + total_count; + const getWeight = function(dp: Datapoint): number { + const w = parseFloat(dp[this.props.weightColumn]); + return !isNaN(w) && isFinite(w) && w > 0.0 ? w : 1.0; + }.bind(this); + var totalWeightFiltered = 0.0, totalWeightSelected = 0.0; + this.props.rows_filtered.forEach(function(dp: Datapoint) { + totalWeightFiltered += getWeight(dp); + }); + this.props.rows_selected.forEach(function(dp: Datapoint) { + totalWeightSelected += getWeight(dp); + }); + const pctage = (100 * totalWeightSelected / totalWeightFiltered); + console.assert(!isNaN(pctage), {"pctage": pctage, "totalWeightFiltered": totalWeightFiltered, "totalWeightSelected": totalWeightSelected}); + this.setState({ + selectedPctWeighted: pctage.toPrecision(3) + }); } componentDidMount() { this.recomputeMetrics(); + this.recomputeSelectedWeightedSum(); } - componentDidUpdate() { + componentDidUpdate(prevProps: HeaderBarProps, prevState: HeaderBarState): void { this.recomputeMetrics(); + if (prevProps.weightColumn != this.props.weightColumn || this.props.rows_selected != prevProps.rows_selected || this.props.rows_filtered != prevProps.rows_filtered) { + this.recomputeSelectedWeightedSum(); + } } onToggleTutorial() { this.setState(function(prevState, prevProps) { @@ -107,9 +135,15 @@ export class HeaderBar extends React.Component {
- Selected: ?? - /?? ( - ??%) + Selected: {this.props.rows_selected.length} + /{this.props.rows_filtered.length} ( + {!this.props.weightColumn && + {this.state.selectedPct}% + } + {this.props.weightColumn && + {this.state.selectedPctWeighted}% weighted + } + )
diff --git a/src/types.ts b/src/types.ts index 1f465e56..29723676 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,6 +48,7 @@ export interface HiPlotExperiment { // Mirror of python `hip.Experiment` parameters_definition?: {[key: string]: HiPlotValueDef}, colormap?: string; colorby?: string; + weightcolumn?: string; display_data?: {[key: string]: {[key2: string]: any}}, }