def set_marker_color(self, attr, update=True): try: self._color_attr = variable = self.data.domain[attr] if len(self.data) == 0: raise Exception except Exception: self._color_attr = None self._legend_colors = [] else: if variable.is_continuous: self._raw_color_values = values = self.data.get_column_view(variable)[0].astype(float) self._scaled_color_values = scale(values) self._colorgen = ContinuousPaletteGenerator(*variable.colors) min = np.nanmin(values) self._legend_colors = (['c', self._legend_values(variable, [min, np.nanmax(values)]), [color_to_hex(i) for i in variable.colors if i]] if not np.isnan(min) else []) elif variable.is_discrete: _values = np.asarray(self.data.domain[attr].values) __values = self.data.get_column_view(variable)[0].astype(np.uint16) self._raw_color_values = _values[__values] # The joke's on you self._scaled_color_values = __values self._colorgen = ColorPaletteGenerator(len(variable.colors), variable.colors) self._legend_colors = ['d', self._legend_values(variable, range(len(_values))), list(_values), [color_to_hex(self._colorgen.getRGB(i)) for i in range(len(_values))]] finally: if update: self.redraw_markers_overlay_image(new_image=True)
def recompute_heatmap(self, points): if self.model is None or self.data is None: self.exposeObject('model_predictions', {}) self.evalJS('draw_heatmap()') return latlons = np.array(points) table = Table(Domain([self.lat_attr, self.lon_attr]), latlons) try: predictions = self.model(table) except Exception as e: self._owwidget.Error.model_error(e) return else: self._owwidget.Error.model_error.clear() class_var = self.model.domain.class_var is_regression = class_var.is_continuous if is_regression: predictions = scale(np.round(predictions, 7)) # Avoid small errors kwargs = dict( extrema=self._legend_values(class_var, [np.nanmin(predictions), np.nanmax(predictions)])) else: colorgen = ColorPaletteGenerator(len(class_var.values), class_var.colors) predictions = colorgen.getRGB(predictions) kwargs = dict( legend_labels=self._legend_values(class_var, range(len(class_var.values))), full_labels=list(class_var.values), colors=[color_to_hex(colorgen.getRGB(i)) for i in range(len(class_var.values))]) self.exposeObject('model_predictions', dict(data=predictions, **kwargs)) self.evalJS('draw_heatmap()')
def test_to_dict(self): desc = owcolor.DiscAttrDesc(self.var) self.assertEqual(desc.to_dict(), {}) desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict()) self.assertEqual(warns, []) self.assertIsNone(desc2.new_name) self.assertIsNone(desc2.new_values) self.assertIsNone(desc2.new_colors) desc.name = "y" self.assertEqual(desc.to_dict(), {"rename": "y"}) desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict()) self.assertEqual(warns, []) self.assertEqual(desc2.new_name, "y") self.assertIsNone(desc2.new_values) self.assertIsNone(desc2.new_colors) desc.set_value(1, "b2") desc.set_color(1, [1, 2, 3]) desc.set_color(2, [2, 3, 4]) self.assertEqual( desc.to_dict(), { "rename": "y", "renamed_values": { "b": "b2" }, "colors": { "a": color_to_hex(desc.colors[0]), "b": "#010203", "c": "#020304" } }) desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict()) self.assertEqual(warns, []) cols = list(desc.colors) cols[1:] = [[1, 2, 3], [2, 3, 4]] np.testing.assert_equal(desc2.colors, cols) self.assertEqual(desc2.values, ("a", "b2", "c")) desc2, warns = owcolor.DiscAttrDesc.from_dict( self.var, { "rename": "y", "renamed_values": { "b": "b2", "d": "x" }, # d is redundant "colors": { "b": "#010203", "c": "#020304", "d": "#123456" } # d is redundant and must be ignored }) self.assertEqual(warns, []) cols = list(desc.colors) cols[1:] = [[1, 2, 3], [2, 3, 4]] np.testing.assert_equal(desc2.colors, cols) self.assertEqual(desc2.values, ("a", "b2", "c"))
def aggregate(self): if self.latlon is None or self.attr not in self.data.domain: self.clear(caches=False) return attr = self.data.domain[self.attr] if attr.is_discrete and self.agg_func not in self.AGG_FUNCS_DISCRETE: self.Error.aggregation_discrete(', '.join(map(str.lower, self.AGG_FUNCS_DISCRETE))) self.Warning.logarithmic_nonpositive.clear() self.clear(caches=False) return else: self.Error.aggregation_discrete.clear() try: regions, adm0, result, self.map.bounds = \ self.get_grouped(self.lat_attr, self.lon_attr, self.admin, self.attr, self.agg_func) except ValueError: # This might happen if widget scheme File→Choropleth, and # some attr is selected in choropleth, and then the same attr # is set to string attr in File and dataset reloaded. # Our "dataflow" arch can suck my balls return # Only show discrete values that are contained in aggregated results discrete_values = [] if attr.is_discrete and not self.agg_func.startswith('Count'): subset = sorted(result.drop_duplicates().dropna().astype(int)) discrete_values = np.array(attr.values)[subset].tolist() discrete_colors = np.array(attr.colors)[subset].tolist() result.replace(subset, list(range(len(subset))), inplace=True) self.result_min_nonpositive = attr.is_continuous and result.min() <= 0 force_quantization = self.color_quantization.startswith('log') and self.result_min_nonpositive self.Warning.logarithmic_nonpositive(shown=force_quantization) repr_time = isinstance(attr, TimeVariable) and self.agg_func not in self.AGG_FUNCS_CANT_TIME self.map.exposeObject( 'results', dict(discrete=discrete_values, colors=[color_to_hex(i) for i in (discrete_colors if discrete_values else ((0, 0, 255), (255, 255, 0)) if attr.is_discrete else attr.colors[:-1])], # ??? regions=list(adm0), attr=attr.name, have_nonpositive=self.result_min_nonpositive or bool(discrete_values), values=result.to_dict(), repr_vals=result.map(attr.repr_val).to_dict() if repr_time else {}, minmax=([result.min(), result.max()] if attr.is_discrete and not discrete_values else [attr.repr_val(result.min()), attr.repr_val(result.max())] if repr_time or not discrete_values else []))) self.map.evalJS('replot();')
def test_data(self): super().test_data() model = self.model self.assertIsNone(model.data(model.index(0, 4))) index = model.index(1, 2) self.assertEqual(model.data(index, Qt.DisplayRole), "e") self.assertEqual(model.data(index, Qt.EditRole), "e") font = model.data(index, Qt.FontRole) self.assertTrue(font is None or not font.bold()) var_colors = self.descs[1].var.colors[1] color = model.data(index, Qt.DecorationRole) np.testing.assert_equal(color.getRgb()[:3], var_colors) color = model.data(index, owcolor.ColorRole) np.testing.assert_equal(color, var_colors) self.assertEqual(model.data(index, Qt.ToolTipRole), color_to_hex(var_colors)) self.assertIsNone(model.data(model.index(0, 4))) index = model.index(2, 5) self.assertEqual(model.data(index, Qt.DisplayRole), "k") self.assertEqual(model.data(index, Qt.EditRole), "k") font = model.data(index, Qt.FontRole) self.assertTrue(font is None or not font.bold()) var_colors = self.descs[2].var.colors[4] color = model.data(index, Qt.DecorationRole) np.testing.assert_equal(color.getRgb()[:3], var_colors) color = model.data(index, owcolor.ColorRole) np.testing.assert_equal(color, var_colors) self.assertEqual(model.data(index, Qt.ToolTipRole), color_to_hex(var_colors)) self.descs[2].set_value(4, "foo") self.assertEqual(model.data(index, Qt.DisplayRole), "foo")
def to_dict(self): d = super().to_dict() if self.new_values is not None: d["renamed_values"] = \ {k: v for k, v in zip(self.var.values, self.new_values) if k != v} if self.new_colors is not None: d["colors"] = { value: color_to_hex(color) for value, color in zip(self.var.values, self.colors)} return d
def _update_js_markers(self, visible, in_subset): self._visible = visible latlon = self._latlon_data self.exposeObject('latlon_data', dict(data=latlon[visible])) self.exposeObject( 'jittering_offsets', self._jittering_offsets[visible] if self._jittering_offsets is not None else []) self.exposeObject( 'selected_markers', dict(data=(self._selected_indices[visible] if self. _selected_indices is not None else 0))) self.exposeObject('in_subset', in_subset.astype(np.int8)) if not self._color_attr: self.exposeObject('color_attr', dict()) else: colors = [ color_to_hex(rgb) for rgb in self._colorgen.getRGB( self._scaled_color_values[visible]) ] self.exposeObject( 'color_attr', dict(name=str(self._color_attr), values=colors, raw_values=self._raw_color_values[visible])) if not self._label_attr: self.exposeObject('label_attr', dict()) else: self.exposeObject( 'label_attr', dict(name=str(self._label_attr), values=self._label_values[visible])) if not self._shape_attr: self.exposeObject('shape_attr', dict()) else: self.exposeObject( 'shape_attr', dict(name=str(self._shape_attr), values=self._shape_values[visible], raw_values=self._raw_shape_values[visible])) if not self._size_attr: self.exposeObject('size_attr', dict()) else: self.exposeObject( 'size_attr', dict(name=str(self._size_attr), values=self._sizes[visible], raw_values=self._raw_sizes[visible])) self.evalJS(''' window.latlon_data = latlon_data.data; window.selected_markers = selected_markers.data; add_markers(latlon_data); ''')
def _update_js_markers(self, visible): self._visible = visible data = Table(Domain([self.lat_attr, self.lon_attr]), self.data) self.exposeObject('latlon_data', dict(data=data.X[visible])) self.exposeObject( 'selected_markers', dict(data=(self._selected_indices[visible] if self. _selected_indices is not None else 0))) if not self._color_attr: self.exposeObject('color_attr', dict()) else: colors = [ color_to_hex(rgb) for rgb in self._colorgen.getRGB( self._scaled_color_values[visible]) ] self.exposeObject( 'color_attr', dict(name=str(self._color_attr), values=colors, raw_values=self._raw_color_values[visible])) if not self._label_attr: self.exposeObject('label_attr', dict()) else: self.exposeObject( 'label_attr', dict(name=str(self._label_attr), values=self._label_values[visible])) if not self._shape_attr: self.exposeObject('shape_attr', dict()) else: self.exposeObject( 'shape_attr', dict(name=str(self._shape_attr), values=self._shape_values[visible], raw_values=self._raw_shape_values[visible])) if not self._size_attr: self.exposeObject('size_attr', dict()) else: self.exposeObject( 'size_attr', dict(name=str(self._size_attr), values=self._sizes[visible], raw_values=self._raw_sizes[visible])) self.evalJS(''' window.latlon_data = latlon_data.data; window.selected_markers = selected_markers.data add_markers(latlon_data); ''')
def test_colors(self): var = DiscreteVariable.make("a", values=("F", "M")) self.assertIsNone(var._colors) self.assertEqual(var.colors.shape, (2, 3)) self.assertFalse(var.colors.flags.writeable) var.colors = np.arange(6).reshape((2, 3)) np.testing.assert_almost_equal(var.colors, [[0, 1, 2], [3, 4, 5]]) self.assertEqual(var.attributes["colors"], { "F": "#000102", "M": "#030405" }) self.assertFalse(var.colors.flags.writeable) with self.assertRaises(ValueError): var.colors[0] = [42, 41, 40] var = DiscreteVariable.make("x", values=("A", "B")) var.attributes["colors"] = {"A": "#0a0b0c", "B": "#0d0e0f"} np.testing.assert_almost_equal(var.colors, [[10, 11, 12], [13, 14, 15]]) # Backward compatibility with list-like attributes var = DiscreteVariable.make("x", values=("A", "B")) var.attributes["colors"] = ["#0a0b0c", "#0d0e0f"] np.testing.assert_almost_equal(var.colors, [[10, 11, 12], [13, 14, 15]]) # Test ncolors adapts to nvalues var = DiscreteVariable.make('foo', values=('d', 'r')) self.assertEqual(len(var.colors), 2) var.add_value('e') self.assertEqual(len(var.colors), 3) var.add_value('k') self.assertEqual(len(var.colors), 4) # Missing colors are retrieved from palette var = DiscreteVariable.make("x", values=("A", "B", "C")) palette = LimitedDiscretePalette(3).palette var.attributes["colors"] = { "C": color_to_hex(palette[0]), "B": "#0D0E0F" } np.testing.assert_almost_equal(var.colors, [palette[1], [13, 14, 15], palette[0]]) # Variable with many values var = DiscreteVariable("x", values=tuple(f"v{i}" for i in range(1020))) self.assertEqual(len(var.colors), 1020)
def _update_js_markers(self, visible, in_subset): self._visible = visible latlon = np.c_[self.data.get_column_view(self.lat_attr)[0], self.data.get_column_view(self.lon_attr)[0]] self.exposeObject('latlon_data', dict(data=latlon[visible])) self.exposeObject('jittering_offsets', self._jittering_offsets[visible] if self._jittering_offsets is not None else []) self.exposeObject('selected_markers', dict(data=(self._selected_indices[visible] if self._selected_indices is not None else 0))) self.exposeObject('in_subset', in_subset.astype(np.int8)) if not self._color_attr: self.exposeObject('color_attr', dict()) else: colors = [color_to_hex(rgb) for rgb in self._colorgen.getRGB(self._scaled_color_values[visible])] self.exposeObject('color_attr', dict(name=str(self._color_attr), values=colors, raw_values=self._raw_color_values[visible])) if not self._label_attr: self.exposeObject('label_attr', dict()) else: self.exposeObject('label_attr', dict(name=str(self._label_attr), values=self._label_values[visible])) if not self._shape_attr: self.exposeObject('shape_attr', dict()) else: self.exposeObject('shape_attr', dict(name=str(self._shape_attr), values=self._shape_values[visible], raw_values=self._raw_shape_values[visible])) if not self._size_attr: self.exposeObject('size_attr', dict()) else: self.exposeObject('size_attr', dict(name=str(self._size_attr), values=self._sizes[visible], raw_values=self._raw_sizes[visible])) self.evalJS(''' window.latlon_data = latlon_data.data; window.selected_markers = selected_markers.data add_markers(latlon_data); ''')
def data(self, index, role=Qt.DisplayRole): # pylint: disable=too-many-return-statements row, col = index.row(), index.column() if col == 0: return super().data(index, role) desc = self.attrdescs[row] if col > len(desc.var.values): return None if role in (Qt.DisplayRole, Qt.EditRole): return desc.values[col - 1] color = desc.colors[col - 1] if role == Qt.DecorationRole: return QColor(*color) if role == Qt.ToolTipRole: return color_to_hex(color) if role == ColorRole: return color return None
def test_set_data(self): super().test_set_data() model = self.model emit = Mock() try: model.dataChanged.connect(emit) index = model.index(2, 5) self.assertEqual(model.data(index, Qt.DisplayRole), "k") self.assertEqual(model.data(index, Qt.EditRole), "k") self.assertFalse(model.setData(index, "foo", Qt.DisplayRole)) emit.assert_not_called() self.assertEqual(model.data(index, Qt.DisplayRole), "k") self.assertTrue(model.setData(index, "foo", Qt.EditRole)) emit.assert_called() emit.reset_mock() self.assertEqual(model.data(index, Qt.DisplayRole), "foo") self.assertEqual(self.descs[2].values, ("g", "h", "i", "j", "foo")) new_color = [0, 1, 2] self.assertTrue(model.setData(index, new_color + [255], ColorRole)) emit.assert_called() emit.reset_mock() color = model.data(index, Qt.DecorationRole) rgb = [color.red(), color.green(), color.blue()] self.assertEqual(rgb, new_color) color = model.data(index, owcolor.ColorRole) self.assertEqual(list(color), new_color) self.assertEqual(model.data(index, Qt.ToolTipRole), color_to_hex(new_color)) np.testing.assert_equal(self.descs[2].colors[4], rgb) finally: model.dataChanged.disconnect(emit)
def set_color(self, i, color): self.colors = self.colors self._colors.flags.writeable = True self._colors[i, :] = color self._colors.flags.writeable = False self.attributes["colors"][i] = color_to_hex(color)
def colors(self, value): self._colors = value self._colors.flags.writeable = False self.attributes["colors"] = [color_to_hex(col) for col in value]
def colors(self, value): col1, col2, black = self._colors = value self.attributes["colors"] = \ [color_to_hex(col1), color_to_hex(col2), black]
def setSeries(self, timeseries, attr, xdim, ydim, fagg): if timeseries is None or not attr: self.clear() return if isinstance(xdim, str) and xdim.isdigit(): xdim = [str(i) for i in range(1, int(xdim) + 1)] if isinstance(ydim, str) and ydim.isdigit(): ydim = [str(i) for i in range(1, int(ydim) + 1)] if isinstance(xdim, DiscreteVariable): xcol = timeseries.get_column_view(xdim)[0] xvals, xfunc = xdim.values, lambda i, _: xdim.repr_val(xcol[i]) else: xvals, xfunc = xdim.value if isinstance(ydim, DiscreteVariable): ycol = timeseries.get_column_view(ydim)[0] yvals, yfunc = ydim.values, lambda i, _: ydim.repr_val(ycol[i]) else: yvals, yfunc = ydim.value attr = attr[0] values = timeseries.get_column_view(attr)[0] time_values = [ fromtimestamp(i, tz=timeseries.time_variable.timezone) for i in timeseries.time_values ] if not yvals: yvals = sorted( set( yfunc(i, v) for i, v in enumerate(time_values) if v is not None)) if not xvals: xvals = sorted( set( xfunc(i, v) for i, v in enumerate(time_values) if v is not None)) indices = defaultdict(list) for i, tval in enumerate(time_values): if tval is not None: indices[(xfunc(i, tval), yfunc(i, tval))].append(i) if self._owwidget.invert_date_order: yvals = yvals[::-1] series = [] aggvals = [] self.indices = [] xname = self.AxesCategories.name_it(xdim) yname = self.AxesCategories.name_it(ydim) for yval in yvals: data = [] series.append(dict(name=yname(yval), data=data)) self.indices.append([]) for xval in xvals: inds = indices.get((xval, yval), ()) self.indices[-1].append(inds) point = dict(y=1) data.append(point) if inds: try: aggval = np.round(fagg(values[inds]), 4) except ValueError: aggval = np.nan else: aggval = np.nan if isinstance(aggval, Number) and np.isnan(aggval): aggval = 'N/A' point['select'] = '' point['color'] = 'white' else: aggvals.append(aggval) point['n'] = aggval # TODO: allow scaling over just rows or cols instead of all values as currently try: maxval, minval = np.max(aggvals), np.min(aggvals) except ValueError: self.clear() return ptpval = maxval - minval color = GradientPaletteGenerator('#ffcccc', '#cc0000') selected_color = GradientPaletteGenerator('#cdd1ff', '#0715cd') for serie in series: for point in serie['data']: n = point['n'] if isinstance(n, Number): val = (n - minval) / ptpval if attr.is_discrete: point['n'] = attr.repr_val(n) elif isinstance(attr, TimeVariable): point['n'] = attr.repr_val(n) if attr.is_discrete: point['color'] = color = color_to_hex( attr.colors[int(n)]) sel_color = QColor(color).darker(150).name() else: point['color'] = color[val] sel_color = selected_color[val] point['states'] = dict( select=dict(borderColor="black", color=sel_color)) # TODO: make a white hole in the middle. Center w/o data. self.chart(series=series, xAxis_categories=[xname(i) for i in xvals], yAxis_categories=[yname(i) for i in reversed(yvals)], javascript_after=''' // Force zoomType which is by default disabled for polar charts chart.options.chart.zoomType = 'xy'; chart.pointer.init(chart, chart.options); ''')
def setSeries(self, timeseries, attr, xdim, ydim, fagg): if timeseries is None or not attr: self.clear() return if isinstance(xdim, str) and xdim.isdigit(): xdim = [str(i) for i in range(1, int(xdim) + 1)] if isinstance(ydim, str) and ydim.isdigit(): ydim = [str(i) for i in range(1, int(ydim) + 1)] if isinstance(xdim, DiscreteVariable): xcol = timeseries.get_column_view(xdim)[0] xvals, xfunc = xdim.values, lambda i, _: xdim.repr_val(xcol[i]) else: xvals, xfunc = xdim.value if isinstance(ydim, DiscreteVariable): ycol = timeseries.get_column_view(ydim)[0] yvals, yfunc = ydim.values, lambda i, _: ydim.repr_val(ycol[i]) else: yvals, yfunc = ydim.value attr = attr[0] values = timeseries.get_column_view(attr)[0] time_values = [fromtimestamp(i) for i in timeseries.time_values] if not yvals: yvals = sorted(set(yfunc(i, v) for i, v in enumerate(time_values) if v is not None)) if not xvals: xvals = sorted(set(xfunc(i, v) for i, v in enumerate(time_values) if v is not None)) indices = defaultdict(list) for i, tval in enumerate(time_values): if tval is not None: indices[(xfunc(i, tval), yfunc(i, tval))].append(i) if self._owwidget.invert_date_order: yvals = yvals[::-1] series = [] aggvals = [] self.indices = [] xname = self.AxesCategories.name_it(xdim) yname = self.AxesCategories.name_it(ydim) for yval in yvals: data = [] series.append(dict(name=yname(yval), data=data)) self.indices.append([]) for xval in xvals: inds = indices.get((xval, yval), ()) self.indices[-1].append(inds) point = dict(y=1) data.append(point) if inds: try: aggval = np.round(fagg(values[inds]), 4) except ValueError: aggval = np.nan else: aggval = np.nan if isinstance(aggval, Number) and np.isnan(aggval): aggval = 'N/A' point['select'] = '' point['color'] = 'white' else: aggvals.append(aggval) point['n'] = aggval # TODO: allow scaling over just rows or cols instead of all values as currently try: maxval, minval = np.max(aggvals), np.min(aggvals) except ValueError: self.clear() return ptpval = maxval - minval color = GradientPaletteGenerator('#ffcccc', '#cc0000') selected_color = GradientPaletteGenerator('#cdd1ff', '#0715cd') for serie in series: for point in serie['data']: n = point['n'] if isinstance(n, Number): val = (n - minval) / ptpval if attr.is_discrete: point['n'] = attr.repr_val(n) elif isinstance(attr, TimeVariable): point['n'] = attr.repr_val(n) if attr.is_discrete: point['color'] = color = color_to_hex(attr.colors[int(n)]) sel_color = QColor(color).darker(150).name() else: point['color'] = color[val] sel_color = selected_color[val] point['states'] = dict(select=dict(borderColor="black", color=sel_color)) # TODO: make a white hole in the middle. Center w/o data. self.chart(series=series, xAxis_categories=[xname(i) for i in xvals], yAxis_categories=[yname(i) for i in reversed(yvals)], javascript_after=''' // Force zoomType which is by default disabled for polar charts chart.options.chart.zoomType = 'xy'; chart.pointer.init(chart, chart.options); ''')
def update_plot(self): data = self.data if data is None or not len(data): self.clear() return self.optimize_button.setDisabled(not self.is_optimization_valid()) self.Warning.too_many_selected_dimensions( len(self.selected_attrs), self.MAX_N_DIMS, shown=len(self.selected_attrs) > self.MAX_N_DIMS) selected_attrs = self.selected_attrs[:self.MAX_N_DIMS] sample = self.sample dimensions = [] for attr in selected_attrs: attr = data.domain[attr] values = data.get_column_view(attr)[0][sample] dim = dict(label=attr.name, values=values, constraintrange=self.constraint_range.get(attr.name)) if attr.is_discrete: dim.update(tickvals=np.arange(len(attr.values)), ticktext=attr.values) elif isinstance(attr, TimeVariable): tickvals = [ np.nanmin(values), np.nanmedian(values), np.nanmax(values) ] ticktext = [attr.repr_val(i) for i in tickvals] dim.update(tickvals=tickvals, ticktext=ticktext) dimensions.append(dim) # Compute color legend line = dict() padding_right = 40 if self.color_attr: attr = data.domain[self.color_attr] values = data.get_column_view(attr)[0][sample] line.update(color=values, showscale=True) title = '<br>'.join( textwrap.wrap(attr.name.strip(), width=7, max_lines=4, placeholder='…')) if attr.is_discrete: padding_right = 90 colors = [color_to_hex(i) for i in attr.colors] values_short = [ textwrap.fill(value, width=9, max_lines=1, placeholder='…') for value in attr.values ] self.graph.exposeObject( 'discrete_colorbar', dict(colors=colors, title=title, values=attr.values, values_short=values_short)) line.update(showscale=False, colorscale=list( zip(np.linspace(0, 1, len(attr.values)), colors))) else: padding_right = 0 self.graph.exposeObject('discrete_colorbar', {}) line.update(colorscale=list( zip((0, 1), (color_to_hex(i) for i in attr.colors[:-1]))), colorbar=dict(title=title)) if isinstance(attr, TimeVariable): tickvals = [ np.nanmin(values), np.nanmedian(values), np.nanmax(values) ] ticktext = [attr.repr_val(i) for i in tickvals] line.update(colorbar=dict(title=title, tickangle=-90, tickvals=tickvals, ticktext=ticktext)) self.graph.plot([Parcoords(line=line, dimensions=dimensions)], padding_right=padding_right)