Exemple #1
0
    def _create_buttons(self, box):
        """Create radio buttons"""
        def intspin():
            s = QSpinBox(self)
            s.setMinimum(2)
            s.setMaximum(10)
            s.setFixedWidth(60)
            s.setAlignment(Qt.AlignRight)
            s.setContentsMargins(0, 0, 0, 0)
            return s, s.valueChanged

        def widthline(validator):
            s = QLineEdit(self)
            s.setFixedWidth(60)
            s.setAlignment(Qt.AlignRight)
            s.setValidator(validator)
            s.setContentsMargins(0, 0, 0, 0)
            return s, s.textChanged

        def manual_cut_editline(text="", enabled=True) -> QLineEdit:
            edit = QLineEdit(
                text=text,
                placeholderText="e.g. 0.0, 0.5, 1.0",
                toolTip='<p style="white-space:pre">' +
                        'Enter cut points as a comma-separate list of \n'
                        'strictly increasing numbers e.g. 0.0, 0.5, 1.0).</p>',
                enabled=enabled,
            )
            edit.setValidator(IncreasingNumbersListValidator())
            edit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)

            @edit.textChanged.connect
            def update():
                validator = edit.validator()
                if validator is not None and edit.text().strip():
                    state, _, _ = validator.validate(edit.text(), 0)
                else:
                    state = QValidator.Acceptable
                palette = edit.palette()
                colors = {
                    QValidator.Intermediate: (Qt.yellow, Qt.black),
                    QValidator.Invalid: (Qt.red, Qt.black),
                }.get(state, None)
                if colors is None:
                    palette = QPalette()
                else:
                    palette.setColor(QPalette.Base, colors[0])
                    palette.setColor(QPalette.Text, colors[1])

                cr = edit.cursorRect()
                p = edit.mapToGlobal(cr.bottomRight())
                edit.setPalette(palette)
                if state != QValidator.Acceptable and edit.isVisible():
                    validator.show_tip(edit, p, edit.toolTip(),
                                       textFormat=Qt.RichText)
                else:
                    validator.show_tip(edit, p, "")
            return edit, edit.textChanged

        children = []

        def button(id_, *controls, stretch=True):
            layout = QHBoxLayout()
            desc = Options[id_]
            button = QRadioButton(desc.label)
            button.setToolTip(desc.tooltip)
            self.button_group.addButton(button, id_)
            layout.addWidget(button)
            if controls:
                if stretch:
                    layout.addStretch(1)
                for c, signal in controls:
                    layout.addWidget(c)
                    if signal is not None:
                        @signal.connect
                        def arg_changed():
                            self.button_group.button(id_).setChecked(True)
                            self.update_hints(id_)

            children.append(layout)
            button_box.layout().addLayout(layout)
            return (*controls, (None, ))[0][0]

        button_box = gui.vBox(box)
        button_box.layout().setSpacing(0)
        button_box.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred))
        self.button_group = QButtonGroup(self)
        self.button_group.idClicked.connect(self.update_hints)

        button(Methods.Keep)
        button(Methods.Remove)

        self.binning_spin = button(Methods.Binning, intspin())
        validator = QDoubleValidator()
        validator.setBottom(0)
        self.width_line = button(Methods.FixedWidth, widthline(validator))

        self.width_time_unit = u = QComboBox(self)
        u.setContentsMargins(0, 0, 0, 0)
        u.addItems([unit + "(s)" for unit in time_units])
        validator = QIntValidator()
        validator.setBottom(1)
        self.width_time_line = button(Methods.FixedWidthTime,
                                      widthline(validator),
                                      (u, u.currentTextChanged))

        self.freq_spin = button(Methods.EqualFreq, intspin())
        self.width_spin = button(Methods.EqualWidth, intspin())
        button(Methods.MDL)

        self.copy_to_custom = FixedSizeButton(
            text="CC", toolTip="Copy the current cut points to manual mode")
        self.copy_to_custom.clicked.connect(self._copy_to_manual)
        self.threshold_line = button(Methods.Custom,
                                     manual_cut_editline(),
                                     (self.copy_to_custom, None),
                                     stretch=False)
        button(Methods.Default)
        maxheight = max(w.sizeHint().height() for w in children)
        for w in children:
            w.itemAt(0).widget().setFixedHeight(maxheight)
        button_box.layout().addStretch(1)
Exemple #2
0
    def __init__(self):
        super().__init__()

        #: input data
        self.data = None
        self.class_var = None
        #: Current variable discretization state
        self.var_state = {}
        #: Saved variable discretization settings (context setting)
        self.saved_var_states = {}

        self.method = Methods.Default
        self.k = 5
        self.cutpoints = ()

        box = gui.vBox(self.controlArea, self.tr("Default Discretization"))
        self._default_method_ = 0
        self.default_bbox = rbox = gui.radioButtons(
            box, self, "_default_method_", callback=self._default_disc_changed)
        self.default_button_group = bg = rbox.findChild(QButtonGroup)
        bg.buttonClicked[int].connect(self.set_default_method)

        rb = gui.hBox(rbox)
        self.left = gui.vBox(rb)
        right = gui.vBox(rb)
        rb.layout().setStretch(0, 1)
        rb.layout().setStretch(1, 1)
        self.options = [
            (Methods.Default, self.tr("Default")),
            (Methods.Leave, self.tr("Leave numeric")),
            (Methods.MDL, self.tr("Entropy-MDL discretization")),
            (Methods.EqualFreq, self.tr("Equal-frequency discretization")),
            (Methods.EqualWidth, self.tr("Equal-width discretization")),
            (Methods.Remove, self.tr("Remove numeric variables")),
            (Methods.Custom, self.tr("Manual")),
        ]

        for id_, opt in self.options[1:]:
            t = gui.appendRadioButton(rbox, opt)
            bg.setId(t, id_)
            t.setChecked(id_ == self.default_method)
            [right, self.left][opt.startswith("Equal")].layout().addWidget(t)

        def _intbox(parent, attr, callback):
            box = gui.indentedBox(parent)
            s = gui.spin(
                box, self, attr, minv=2, maxv=10, label="Num. of intervals:",
                callback=callback)
            s.setMaximumWidth(60)
            s.setAlignment(Qt.AlignRight)
            gui.rubber(s.box)
            return box.box

        self.k_general = _intbox(self.left, "default_k",
                                 self._default_disc_changed)
        self.k_general.layout().setContentsMargins(0, 0, 0, 0)

        def manual_cut_editline(text="", enabled=True) -> QLineEdit:
            edit = QLineEdit(
                text=text,
                placeholderText="e.g. 0.0, 0.5, 1.0",
                toolTip="Enter fixed discretization cut points (a comma "
                        "separated list of strictly increasing numbers e.g. "
                        "0.0, 0.5, 1.0).",
                enabled=enabled,
            )
            @edit.textChanged.connect
            def update():
                validator = edit.validator()
                if validator is not None:
                    state, _, _ = validator.validate(edit.text(), 0)
                else:
                    state = QValidator.Acceptable
                palette = edit.palette()
                colors = {
                    QValidator.Intermediate: (Qt.yellow, Qt.black),
                    QValidator.Invalid: (Qt.red, Qt.black),
                }.get(state, None)
                if colors is None:
                    palette = QPalette()
                else:
                    palette.setColor(QPalette.Base, colors[0])
                    palette.setColor(QPalette.Text, colors[1])

                cr = edit.cursorRect()
                p = edit.mapToGlobal(cr.bottomRight())
                edit.setPalette(palette)
                if state != QValidator.Acceptable and edit.isVisible():
                    show_tip(edit, p, edit.toolTip(), textFormat=Qt.RichText)
                else:
                    show_tip(edit, p, "")
            return edit

        self.manual_cuts_edit = manual_cut_editline(
            text=", ".join(map(str, self.default_cutpoints)),
            enabled=self.default_method == Methods.Custom,
        )

        def set_manual_default_cuts():
            text = self.manual_cuts_edit.text()
            self.default_cutpoints = tuple(
                float(s.strip()) for s in text.split(",") if s.strip())
            self._default_disc_changed()
        self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts)

        validator = IncreasingNumbersListValidator()
        self.manual_cuts_edit.setValidator(validator)
        ibox = gui.indentedBox(right, orientation=Qt.Horizontal)
        ibox.layout().addWidget(self.manual_cuts_edit)

        right.layout().addStretch(10)
        self.left.layout().addStretch(10)

        self.connect_control(
            "default_cutpoints",
            lambda values: self.manual_cuts_edit.setText(", ".join(map(str, values)))
        )
        vlayout = QHBoxLayout()
        box = gui.widgetBox(
            self.controlArea, "Individual Attribute Settings",
            orientation=vlayout, spacing=8
        )

        # List view with all attributes
        self.varview = ListViewSearch(
            selectionMode=QListView.ExtendedSelection,
            uniformItemSizes=True,
        )
        self.varview.setItemDelegate(DiscDelegate())
        self.varmodel = itemmodels.VariableListModel()
        self.varview.setModel(self.varmodel)
        self.varview.selectionModel().selectionChanged.connect(
            self._var_selection_changed
        )

        vlayout.addWidget(self.varview)
        # Controls for individual attr settings
        self.bbox = controlbox = gui.radioButtons(
            box, self, "method", callback=self._disc_method_changed
        )
        vlayout.addWidget(controlbox)
        self.variable_button_group = bg = controlbox.findChild(QButtonGroup)
        for id_, opt in self.options[:5]:
            b = gui.appendRadioButton(controlbox, opt)
            bg.setId(b, id_)

        self.k_specific = _intbox(controlbox, "k", self._disc_method_changed)

        gui.appendRadioButton(controlbox, "Remove attribute", id=Methods.Remove)
        b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom)

        self.manual_cuts_specific = manual_cut_editline(
            text=", ".join(map(str, self.cutpoints)),
            enabled=self.method == Methods.Custom
        )
        self.manual_cuts_specific.setValidator(validator)
        b.toggled[bool].connect(self.manual_cuts_specific.setEnabled)

        def set_manual_cuts():
            text = self.manual_cuts_specific.text()
            points = [t for t in text.split(",") if t.split()]
            self.cutpoints = tuple(float(t) for t in points)
            self._disc_method_changed()
        self.manual_cuts_specific.editingFinished.connect(set_manual_cuts)

        self.connect_control(
            "cutpoints",
            lambda values: self.manual_cuts_specific.setText(", ".join(map(str, values)))
        )
        ibox = gui.indentedBox(controlbox, orientation=Qt.Horizontal)
        self.copy_current_to_manual_button = b = FixedSizeButton(
            text="CC", toolTip="Copy the current cut points to manual mode",
            enabled=False
        )
        b.clicked.connect(self._copy_to_manual)
        ibox.layout().addWidget(self.manual_cuts_specific)
        ibox.layout().addWidget(b)

        gui.rubber(controlbox)
        controlbox.setEnabled(False)
        bg.button(self.method)
        self.controlbox = controlbox

        gui.auto_apply(self.buttonsArea, self, "autosend")

        self._update_spin_positions()
Exemple #3
0
class OWDiscretize(widget.OWWidget):
    # pylint: disable=too-many-instance-attributes
    name = "Discretize"
    description = "Discretize numeric variables"
    category = "Transform"
    icon = "icons/Discretize.svg"
    keywords = ["bin", "categorical", "nominal", "ordinal"]
    priority = 2130

    class Inputs:
        data = Input("Data", Table, doc="Input data table")

    class Outputs:
        data = Output("Data", Table, doc="Table with categorical features")

    settings_version = 3

    #: Default setting (key DefaultKey) and specific settings for variables;
    # if variable is not in the dict, it uses default
    var_hints: Dict[KeyType, VarHint] = Setting(
        {DefaultKey: DefaultHint}, schema_only=True)
    autosend = Setting(True)

    want_main_area = False

    def __init__(self):
        super().__init__()

        #: input data
        self.data = None
        #: Cached discretized variables
        self.discretized_vars: Dict[KeyType, DiscreteVariable] = {}

        # Indicates that buttons, spins, edit and combos are being changed
        # programmatically (when interface is changed due to selection change),
        # so this should not trigger update of hints and invalidation of
        # discretization in `self.discretized_vars`.
        self.__interface_update = False

        box = gui.hBox(self.controlArea, True, spacing=8)
        self._create_var_list(box)
        self._create_buttons(box)
        gui.auto_apply(self.buttonsArea, self, "autosend")
        gui.rubber(self.buttonsArea)
        self.varview.select_default()

    def _create_var_list(self, box):
        """Create list view with variables"""
        # If we decide to not elide, remove the `uniformItemSize` argument
        self.varview = ListViewSearch(
            selectionMode=QListView.ExtendedSelection, uniformItemSizes=True)
        self.varview.setModel(
            DiscDomainModel(
                valid_types=(ContinuousVariable, TimeVariable),
                order=DiscDomainModel.MIXED
            ))
        self.varview.selectionModel().selectionChanged.connect(
            self._var_selection_changed)
        self.varview.default_view.selectionModel().selectionChanged.connect(
            self._default_selected)
        self._update_default_model()
        box.layout().addWidget(self.varview)

    def _create_buttons(self, box):
        """Create radio buttons"""
        def intspin():
            s = QSpinBox(self)
            s.setMinimum(2)
            s.setMaximum(10)
            s.setFixedWidth(60)
            s.setAlignment(Qt.AlignRight)
            s.setContentsMargins(0, 0, 0, 0)
            return s, s.valueChanged

        def widthline(validator):
            s = QLineEdit(self)
            s.setFixedWidth(60)
            s.setAlignment(Qt.AlignRight)
            s.setValidator(validator)
            s.setContentsMargins(0, 0, 0, 0)
            return s, s.textChanged

        def manual_cut_editline(text="", enabled=True) -> QLineEdit:
            edit = QLineEdit(
                text=text,
                placeholderText="e.g. 0.0, 0.5, 1.0",
                toolTip='<p style="white-space:pre">' +
                        'Enter cut points as a comma-separate list of \n'
                        'strictly increasing numbers e.g. 0.0, 0.5, 1.0).</p>',
                enabled=enabled,
            )
            edit.setValidator(IncreasingNumbersListValidator())
            edit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)

            @edit.textChanged.connect
            def update():
                validator = edit.validator()
                if validator is not None and edit.text().strip():
                    state, _, _ = validator.validate(edit.text(), 0)
                else:
                    state = QValidator.Acceptable
                palette = edit.palette()
                colors = {
                    QValidator.Intermediate: (Qt.yellow, Qt.black),
                    QValidator.Invalid: (Qt.red, Qt.black),
                }.get(state, None)
                if colors is None:
                    palette = QPalette()
                else:
                    palette.setColor(QPalette.Base, colors[0])
                    palette.setColor(QPalette.Text, colors[1])

                cr = edit.cursorRect()
                p = edit.mapToGlobal(cr.bottomRight())
                edit.setPalette(palette)
                if state != QValidator.Acceptable and edit.isVisible():
                    validator.show_tip(edit, p, edit.toolTip(),
                                       textFormat=Qt.RichText)
                else:
                    validator.show_tip(edit, p, "")
            return edit, edit.textChanged

        children = []

        def button(id_, *controls, stretch=True):
            layout = QHBoxLayout()
            desc = Options[id_]
            button = QRadioButton(desc.label)
            button.setToolTip(desc.tooltip)
            self.button_group.addButton(button, id_)
            layout.addWidget(button)
            if controls:
                if stretch:
                    layout.addStretch(1)
                for c, signal in controls:
                    layout.addWidget(c)
                    if signal is not None:
                        @signal.connect
                        def arg_changed():
                            self.button_group.button(id_).setChecked(True)
                            self.update_hints(id_)

            children.append(layout)
            button_box.layout().addLayout(layout)
            return (*controls, (None, ))[0][0]

        button_box = gui.vBox(box)
        button_box.layout().setSpacing(0)
        button_box.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred))
        self.button_group = QButtonGroup(self)
        self.button_group.idClicked.connect(self.update_hints)

        button(Methods.Keep)
        button(Methods.Remove)

        self.binning_spin = button(Methods.Binning, intspin())
        validator = QDoubleValidator()
        validator.setBottom(0)
        self.width_line = button(Methods.FixedWidth, widthline(validator))

        self.width_time_unit = u = QComboBox(self)
        u.setContentsMargins(0, 0, 0, 0)
        u.addItems([unit + "(s)" for unit in time_units])
        validator = QIntValidator()
        validator.setBottom(1)
        self.width_time_line = button(Methods.FixedWidthTime,
                                      widthline(validator),
                                      (u, u.currentTextChanged))

        self.freq_spin = button(Methods.EqualFreq, intspin())
        self.width_spin = button(Methods.EqualWidth, intspin())
        button(Methods.MDL)

        self.copy_to_custom = FixedSizeButton(
            text="CC", toolTip="Copy the current cut points to manual mode")
        self.copy_to_custom.clicked.connect(self._copy_to_manual)
        self.threshold_line = button(Methods.Custom,
                                     manual_cut_editline(),
                                     (self.copy_to_custom, None),
                                     stretch=False)
        button(Methods.Default)
        maxheight = max(w.sizeHint().height() for w in children)
        for w in children:
            w.itemAt(0).widget().setFixedHeight(maxheight)
        button_box.layout().addStretch(1)

    def _update_default_model(self):
        """Update data in the model showing default settings"""
        model = self.varview.default_view.model()
        model.setData(model.index(0), self.var_hints[DefaultKey], Qt.UserRole)

    def _set_mdl_button(self):
        """Disable MDL discretization for data with non-discrete class"""
        mdl_button = self.button_group.button(Methods.MDL)
        if self.data is None or self.data.domain.has_discrete_class:
            mdl_button.setEnabled(True)
        else:
            if mdl_button.isChecked():
                self._check_button(Methods.Keep, True)
            mdl_button.setEnabled(False)

    def _check_button(self, method_id: Methods, checked: bool):
        """Checks the given button"""
        self.button_group.button(method_id).setChecked(checked)

    def _uncheck_all_buttons(self):
        """Uncheck all radio buttons"""
        group = self.button_group
        button = group.checkedButton()
        if button is not None:
            group.setExclusive(False)
            button.setChecked(False)
            group.setExclusive(True)

    def _set_radio_enabled(self, method_id: Methods, value: bool):
        """Enable/disable radio button and related controls"""
        if self.button_group.button(method_id).isChecked() and not value:
            self._uncheck_all_buttons()
        self.button_group.button(method_id).setEnabled(value)
        for control_name in Options[method_id].controls:
            getattr(self, control_name).setEnabled(value)

    def _get_values(self, method_id: Methods) -> Tuple[Union[int, float, str]]:
        """Return parameters from controls pertaining to the given method"""
        controls = Options[method_id].controls
        values = []
        for control_name in controls:
            control = getattr(self, control_name)
            if isinstance(control, QSpinBox):
                values.append(control.value())
            elif isinstance(control, QComboBox):
                values.append(control.currentIndex())
            else:
                values.append(control.text())
        return tuple(values)

    def _set_values(self, method_id: Methods,
                    values: Tuple[Union[str, int, float]]):
        """
        Set controls pertaining to the given method to parameters from hint
        """
        controls = Options[method_id].controls
        for control_name, value in zip(controls, values):
            control = getattr(self, control_name)
            if isinstance(control, QSpinBox):
                control.setValue(value)
            elif isinstance(control, QComboBox):
                control.setCurrentIndex(value)
            else:
                control.setText(value)

    def varkeys_for_selection(self) -> List[KeyType]:
        """
        Return list of KeyType's for selected variables (for indexing var_hints)

        If 'Default settings' are selected, this returns DefaultKey
        """
        model = self.varview.model()
        varkeys = [variable_key(model[index.row()])
                   for index in self.varview.selectionModel().selectedRows()]
        return varkeys or [DefaultKey]  # default settings are selected

    def update_hints(self, method_id: Methods):
        """
        Callback for radio buttons and for controls regulating parameters

        This function:
        - updates `var_hints` for all selected methods
        - invalidates (removes) `discretized_vars` for affected variables
        - calls _update_discretizations to compute and commit new discretization
        - calls deferred commit

        Data for list view models is updated in _update_discretizations
        """
        if self.__interface_update:
            return

        method_id = Methods(method_id)
        args = self._get_values(method_id)
        keys = self.varkeys_for_selection()
        if method_id == Methods.Default:
            for key in keys:
                if key in self.var_hints:
                    del self.var_hints[key]
        else:
            self.var_hints.update(dict.fromkeys(keys, VarHint(method_id, args)))
        if keys == [DefaultKey]:
            invalidate = set(self.discretized_vars) - set(self.var_hints)
        else:
            invalidate = keys
        for key in invalidate:
            del self.discretized_vars[key]

        if keys == [DefaultKey]:
            self._update_default_model()
        self._update_discretizations()
        self.commit.deferred()

    def _update_discretizations(self):
        """
        Compute invalidated (missing) discretizations

        Also set data for list view models for all invalidated variables
        """
        if self.data is None:
            return

        default_hint = self.var_hints[DefaultKey]
        model = self.varview.model()
        for index, var in enumerate(model):
            key = variable_key(var)
            if key in self.discretized_vars:
                continue  # still valid
            var_hint = self.var_hints.get(key)
            points, dvar = self._discretize_var(var, var_hint or default_hint)
            self.discretized_vars[key] = dvar
            values = getattr(dvar, "values", ())
            model.setData(model.index(index),
                          DiscDesc(var_hint, points, values),
                          Qt.UserRole)

    def _discretize_var(self, var: ContinuousVariable, hint: VarHint) \
        -> Tuple[str, Optional[Variable]]:
        """
        Discretize using method and data in the hint.

        Returns a description (list of points or error/warning) and a
        - discrete variable
        - same variable (if kept numeric)
        - None (if removed or errored)
        """
        if isinstance(var, TimeVariable):
            if hint.method_id in (Methods.FixedWidth, Methods.Custom):
                return ": <keep, time var>", var
        else:
            if hint.method_id == Methods.FixedWidthTime:
                return ": <keep, not time>", var

        function = Options[hint.method_id].function
        dvar = function(self.data, var, *hint.args)
        if isinstance(dvar, str):
            return f" <{dvar}>", None  # error
        if dvar is None:
            return "", None  # removed
        elif dvar is var:
            return "", var  # no transformation
        thresholds = dvar.compute_value.points
        if len(thresholds) == 0:
            return " <removed>", None
        return ": " + ", ".join(map(var.repr_val, thresholds)), dvar

    def _copy_to_manual(self):
        """
        Callback for 'CC' button

        Sets selected variables' method to "Custom" and copies thresholds
        to their VarHints. Variables that are not discretized (for any reason)
        are skipped.

        Discretizations are invalidated and then updated
        (`_update_discretizations`).

        If all selected variables have the same thresholds, it copies it to
        the line edit. Otherwise it unchecks all radio buttons to keep the
        interface consistent.
        """
        varkeys = self.varkeys_for_selection()
        texts = set()
        for key in varkeys:
            dvar = self.discretized_vars.get(key)
            fmt = self.data.domain[key[0]].repr_val
            if isinstance(dvar, DiscreteVariable):
                text = ", ".join(map(fmt, dvar.compute_value.points))
                texts.add(text)
                self.var_hints[key] = VarHint(Methods.Custom, (text, ))
                del self.discretized_vars[key]
        try:
            self.__interface_update = True
            if len(texts) == 1:
                self.threshold_line.setText(texts.pop())
            else:
                self._uncheck_all_buttons()
        finally:
            self.__interface_update = False
        self._update_discretizations()
        self.commit.deferred()

    def _default_selected(self, selected):
        """Callback for selecting 'Default setting'"""
        if not selected:
            # Prevent infinite recursion (with _var_selection_changed)
            return
        self.varview.selectionModel().clearSelection()
        self._update_interface()

        set_enabled = self._set_radio_enabled
        set_enabled(Methods.Default, False)
        set_enabled(Methods.FixedWidth, True)
        set_enabled(Methods.FixedWidthTime, True)
        set_enabled(Methods.Custom, True)
        self.copy_to_custom.setEnabled(False)

    def _var_selection_changed(self, _):
        """Callback for changed selection in listview with variables"""
        selected = self.varview.selectionModel().selectedIndexes()
        if not selected:
            # Prevent infinite recursion (with _default_selected)
            return
        self.varview.default_view.selectionModel().clearSelection()
        self._update_interface()

        set_enabled = self._set_radio_enabled
        vars_ = [self.data.domain[name]
                 for name, _ in self.varkeys_for_selection()]
        no_time = not any(isinstance(var, TimeVariable) for var in vars_)
        all_time = all(isinstance(var, TimeVariable) for var in vars_)
        set_enabled(Methods.Default, True)
        set_enabled(Methods.FixedWidth, no_time)
        set_enabled(Methods.Custom, no_time)
        self.copy_to_custom.setEnabled(no_time)
        set_enabled(Methods.FixedWidthTime, all_time)

    def _update_interface(self):
        """
        Update the user interface according to selection

        - If VarHints for all selected variables are the same, check the
          corresponding radio button and fill the corresponding controls;
        - otherwise, uncheck all radios.
        """
        if self.__interface_update:
            return

        try:
            self.__interface_update = True
            keys = self.varkeys_for_selection()
            mset = list(unique_everseen(map(self.var_hints.get, keys)))
            if len(mset) != 1:
                self._uncheck_all_buttons()
                return

            if mset == [None]:
                method_id, args = Methods.Default, ()
            else:
                method_id, args = mset.pop()
            self._check_button(method_id, True)
            self._set_values(method_id, args)
        finally:
            self.__interface_update = False

    @Inputs.data
    def set_data(self, data: Optional[Table]):
        self.discretized_vars = {}
        self.data = data
        self.varview.model().set_domain(None if data is None else data.domain)
        self._update_discretizations()
        self._update_default_model()
        self.varview.select_default()
        self._set_mdl_button()
        self.commit.now()

    @gui.deferred
    def commit(self):
        if self.data is None:
            self.Outputs.data.send(None)
            return

        def part(variables: List[Variable]) -> List[Variable]:
            return [dvar
                    for dvar in (self.discretized_vars.get(variable_key(v), v)
                                 for v in variables)
                    if dvar]

        d = self.data.domain
        domain = Domain(part(d.attributes), part(d.class_vars), part(d.metas))
        output = self.data.transform(domain)
        self.Outputs.data.send(output)

    def send_report(self):
        dmodel = self.varview.default_view.model()
        desc = dmodel.data(dmodel.index(0))
        self.report_items((tuple(desc.split(": ", maxsplit=1)), ))
        model = self.varview.model()
        reported = []
        for row in range(model.rowCount()):
            name = model[row].name
            desc = model.data(model.index(row), Qt.UserRole)
            if desc.hint is not None:
                name = f"{name} ({format_desc(desc.hint)})"
            reported.append((name, ', '.join(desc.values)))
        self.report_items("Variables", reported)

    @classmethod
    def migrate_settings(cls, settings, version):
        if version is None or version < 2:
            # was stored as int indexing Methods (but offset by 1)
            default = settings.pop("default_method", 0)
            default = Methods(default + 1)
            settings["default_method_name"] = default.name

        if version is None or version < 3:
            method_name = settings.pop("default_method_name",
                                       DefaultHint.method_id.name)
            k = settings.pop("default_k", 3)
            cut_points = settings.pop("default_cutpoints", ())

            method_id = getattr(Methods, method_name)
            if method_id in (Methods.EqualFreq, Methods.EqualWidth):
                args = (k, )
            elif method_id == Methods.Custom:
                args = (cut_points, )
            else:
                args = ()
            default_hint = VarHint(method_id, args)
            var_hints = {DefaultKey: default_hint}
            for context in settings.pop("context_settings", []):
                values = context.values
                if "saved_var_states" not in values:
                    continue
                var_states, _ = values.pop("saved_var_states")
                for (tpe, name), dstate in var_states.items():
                    key = (name, tpe == 4)  # time variable == 4
                    method = dstate.method
                    method_name = type(method).__name__.replace("Leave", "Keep")
                    if method_name == "Default":
                        continue
                    if method_name == "Custom":
                        args = (", ".join(f"{x:g}" for x in method.points), )
                    else:
                        args = tuple(method)
                    var_hints[key] = VarHint(getattr(Methods, method_name), args)
            settings["var_hints"] = var_hints