[Bokeh 2.3.2]
Hi there,
I have created some custom models to allow having a logarithmic color_mapper
with negative {low or high}-values by simply doing an offset by abs(min(data))+1
if theres values <= 0 .
The color_mapper itself works great. The data is, even with negative values, mapped correctly.
This is the modified LogColorMapper
:
OffsetLogColorMapper
TSCODE = """
import {LogColorMapper} from "models/mappers/log_color_mapper"
import {Arrayable} from "core/types"
import {min, max} from "core/util/arrayable"
export type OffsetLogScanData = {
min: number
max: number
scale: number
offset: number
}
export class OffsetLogColorMapper extends LogColorMapper {
protected scan(data: Arrayable<number>, n: number): OffsetLogScanData {
let low = this.low != null ? this.low : min(data)
let high = this.high != null ? this.high : max(data)
let offset = 0
if (low <= 0){
// low and high cannot be offset here,
// as they are passed to the tickers get_ticks
// if we did offset, we would get ticks from 0 to y+x
// instead of -x to y
offset = Math.abs(low) + 1
}
const scale = n / (Math.log(high+offset) - Math.log(low+offset)) // subtract the low offset
return {max: high, min: low, scale, offset}
}
protected cmap<T>(d: number, palette: Arrayable<T>, low_color: T, high_color: T, scan_data: OffsetLogScanData): T {
const max_key = palette.length - 1
d += scan_data.offset
let _max = scan_data.max + scan_data.offset
let _min = scan_data.min + scan_data.offset
if (d > _max) {
return high_color
}
// This handles the edge case where d == high, since the code below maps
// values exactly equal to high to palette.length, which is greater than
// max_key
if (d == _max){
return palette[max_key]
}
else if (d < _min){
return low_color
}
// Get the key
const log = Math.log(d) - Math.log(_min) // subtract the low offset
let key = Math.floor(log * scan_data.scale)
// Deal with upper bound
if (key > max_key) {
key = max_key
}
return palette[key]
}
}
"""
from bokeh.models import LogColorMapper
class OffsetLogColorMapper(LogColorMapper):
__implementation__ = TSCODE
''' Works exactly the same as its base class, however
OffsetLogColorMapper also allows mapping datasets
with negative values. This does not work like mpl's
symlog but instead we use a standard log and offset
every value <= 0.
'''
I also had to subclass LogTickFormatter and LogTicker (Doesn’t really matter for this problem).
The problem:
The y_axis_type of the ColorBar is log_axis. I think this is why the ticks do not appear at the right location? If I use negative ticks they cause Error: invalid bbox
, if I use positive ticks they appear where they should be if you looked at a range of (0,20) instead of(-10,10).
Demo Code
from bokeh.models import HoverTool
from bokeh.models import FixedTicker
from bokeh.palettes import turbo
from bokeh.models import ColorBar
from bokeh.plotting import figure
from bokeh.io import show
from bokeh.settings import settings
settings.minified = False
palette = turbo(128)
data_arr = [[i for i in range(-10, 10)] for j in range(-10, 10)]
lg = OffsetLogColorMapper(palette, low=-9, high=9)
c = ColorBar(color_mapper=lg, ticker=FixedTicker(ticks=[-5, 2, 9]))#,
formatter=OffsetLogTickFormatter(), ticker=OffsetLogTicker())
fig = figure(width=500, height=500, tools=[])
fig.image(image=[data_arr], x=0, y=0, dw=500, dh=500, color_mapper=lg)
fig.add_tools(HoverTool(tooltips="@image"))
fig.add_layout(c, "right")
show(fig)
I’m not really sure what to do at this point. Do I also have to create a custom log_axis? If so, how would I make the colorbar use that.
I hope there’s a way to save this. (Simply offsetting my dataset into positives isn’t really an option)