如何破解此 Bokeh HexTile 图以修复坐标、标签位置和轴?

How to hack this Bokeh HexTile plot to fix the coords, label placement and axes?

下面是 Bokeh 1.4.0 代码,它试图 绘制输入数据框的 HexTile 地图,带有轴,并试图在每个十六进制上放置标签 。 我已经坚持了两天,阅读散景文档、示例和 github 已知问题,SO,Bokeh Discourse and Red Blob Games's superb tutorial on Hexagonal Grids,并尝试编写代码。 (我对提出未来的散景问题不太感兴趣,而对已知限制的实用变通方法更感兴趣,以便让我的地图代码在今天工作。)情节在下面,代码在底部。

以下是问题,按重要性大致降序排列(由于 Bokeh 处理字形的方式,无法区分根本原因并分辨出哪些原因。如果我应用一个比例因子或坐标变换,它会修复一组问题,但打破了另一组,'whack-a-mole'效果):

  1. 标签放置显然是错误的,但我似乎无法破解任何 (x,y) 坐标或 (q,r) 坐标的变体来工作. (我尝试了 figure(..., match_aspect=True)) 的组合,我尝试 1/sqrt(2) 缩放 (x,y)-coords,我尝试 Hextile(... size, scale) 按照 redblobgames 的参数,例如 size = 1/sqrt(3) ~ 0.57735)。
  2. Bokeh 强制 原点在左上角,y 坐标随着您向下而增加,但是默认轴标签显示 y 或 r 为负值。我发现我仍然必须使用 p.text(q, -r, ...。我想我必须手动修补自动提供的 yaxis 标签或 TickFormatter 为正数。
  3. 我使用 np.mgrid 生成坐标网格,但我仍然似乎必须从右到左分配 q 坐标np.mgrid[0:8, (4+1):0:-1] .仍然不管我做什么,六边形从左到右翻转
    • (注意:空的“”县是获得所需形状的占位符,因此网格坐标上的布尔掩码 [counties!='']。这很好用,我想保持原样)
  4. 六边形的源 (q,r) 坐标是整数,我使用 'odd-r' 偏移坐标(不是轴坐标或六角坐标)。 无论我使用什么 HexTile(..., size, scale) 参数,图中的一个或两个维度都是错误的或被压扁了。或者我是否在坐标变换中包含 1/sqrt(2) 因子。
    • 我的 +q 轴是东,我的 +r 轴应该是 120° SSE
  5. 理想情况下,我希望原点位于左下角(数学绘图样式,而不是计算机图形)。但是 Bokeh 显然不支持那个,我可以没有那个。然而,将 y 轴标签默认为负数,同时需要正负坐标的混合,这会让人感到困惑。无论如何,如何以最小的悲伤破解自动修复? (手动p.yrange = Range1d(?, ?)?)
  6. Bokeh 将(十六进制)字形附加到绘图的方法很难使用。理想情况下,我只是 想在六角形、标签、轴的任何地方引用 (q,r)-坐标。我从不想看到 (x,y)-coords 出现在坐标轴、标签坐标、刻度线等上,但似乎 Bokeh 不允许。我想您必须稍后手动破解轴和刻度。此外,plot<->glyph 接口不允许您公开 (q,r) <-> (x,y) 坐标变换函数,当然不是双向函数。
  7. 默认轴似乎没有任何访问器来自动找到它们当前的 extent/limits;除非您指定,否则 p.yaxis.start/end 为空。 p.yaxis.major_tick_inp.yaxis.major_tick_out 的结果也是错误的,对于此图,它为 x 和 y 给出 (2,6),似乎将它们剪裁为 2(?) 的内部倍数。 如何自动获取轴的范围?

我目前的剧情:

我的代码:

import pandas as pd
import numpy as np
from math import sqrt    
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.glyphs import HexTile
from bokeh.io import show

# Data source is a list of county abbreviations, in (q,r) coords...
counties = np.array([
    ['TE','DY','AM','DN', ''],
    ['DL','FM','MN','AH', ''],
    ['SO','LM','CN','LH', ''],
    ['MO','RN','LD','WH','MH'],
    ['GA','OY','KE','D',  ''],
    ['',  'CE','LS','WW', ''],
    ['LC','TA','KK','CW', ''],
    ['KY','CR','WF','WX', ''],
    ])
#counties = counties[::-1] # UNUSED: flip so origin is at bottom-left

# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, (4+1):0:-1]
q = q[counties!='']
r = r[counties!='']

sqrt3 = sqrt(3)
# Try to transform odd-r (q,r) offset coords -> (x,y). Per Red Blob Games' tutorial.
x = q - (r//2) # this may be slightly dubious
y = r

counties_df = pd.DataFrame({'q': q, 'r': r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})

p = figure(tools='save,crosshair') # match_aspect=True?

glyph = HexTile(orientation='pointytop', q='x', r='y', size=0.76, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!? size=0.76 is an empirical hack.
p.add_glyph(counties_ds, glyph)

p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None

print(f'Axes: x={p.xaxis.major_tick_in}:{p.xaxis.major_tick_out} y={p.yaxis.major_tick_in}:{p.yaxis.major_tick_out}')

# Now can't manage to get the right coords for text labels
p.text(q, -r, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)], text_baseline="middle", text_align="center")
# Ideally I ultimately want to fix this and plot `abbrev` column as the text label

show(p)
  1. 有一个 axial_to_cartesian 函数可以为您计算十六进制中心。然后,您可以将标签贴在不同的方向并从这些方向固定。

  2. Bokeh 不强制原点在任何地方。 Bokeh 使用了一种轴到笛卡尔的映射,正是 axial_to_cartesian 给出的。六边形图块的位置(以及轴显示的笛卡尔坐标)由此而来。如果您想要不同的刻度,Bokeh 提供了很多关于刻度位置和刻度标签的控制点。

  3. 轴坐标有不止一种约定。 Bokeh 选择了具有 r 轴瓦片 "up an to the left" 的瓦片,即此处明确显示的瓦片:

    https://docs.bokeh.org/en/latest/docs/user_guide/plotting.html#hex-tiles

  4. Bokeh 需要左上角的轴坐标。您需要将您拥有的任何坐标系转换为该坐标系。对于 "squishing",您需要设置 match_aspect=True 以确保 "data space" 宽高比与 "pixel space" 宽高比 1-1 匹配。

    或者,如果您不使用或不能使用自动调整范围,则需要仔细设置绘图大小并使用 min_border_left 等控制边框大小,以确保边框始终较大足以容纳您拥有的任何刻度标签(这样内部区域就不会调整大小)

  5. 我不太理解这个问题,但是您可以绝对控制视觉上出现的报价,而不管底层报价数据如何。除了内置的格式化程序,还有 FuncTickFormatter 可以让你用一段 JS 代码以任何你想要的方式格式化刻度。 [1](如果您愿意,您还可以控制刻度的位置。)

    [1] 请注意,CoffeeScript 和 from_py_func 选项都已弃用并在下一个 2.0 版本中被删除。

  6. 同样,您需要使用 axial_to_cartesian 来放置六边形瓷砖以外的任何东西。 Bokeh 中没有其他字形理解轴坐标(这就是我们提供转换功能的原因)。

  7. 您误解了 major_tick_inmajor_tick_out 的用途。它们实际上是刻度 视觉上 在图框内外延伸的距离, 像素

    自动调整范围(使用 DataRange1d)仅在 浏览器中计算,在 JavaScript 中,这就是为什么 start/end "Python" 端不可用。如果您需要知道 start/end,您需要自己明确设置 start/end。但是请注意,match_aspect=True 仅适用于 DataRange1d。如果您手动明确设置 start/end,Bokeh 会假定您知道自己想要什么,并且会尊重您的要求,无论它对 aspect 做了什么。

以下是我的解决方案和情节。主要是根据@bigreddot 的建议,但仍然需要一些协调黑客:

  1. 期望用户将输入坐标作为轴向坐标而不是偏移坐标传递是一个主要限制。我解决这个问题。创建 offset_to_cartesian() 没有意义,因为我们需要在三个位置中的两个位置取反 r:
  2. 我的输入是 even-r 偏移坐标。我仍然需要手动应用偏移量:q = q + (r+1)//2
  3. 我需要在 axial_to_cartesian() 调用和字形的数据源创建中手动否定 r。 (但不在 text() 调用中)。
  4. 调用需要是:axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')
  5. 需要 p = figure(match_aspect=True ...) 来防止挤压
  6. 我需要手动创建 x,y 轴以获得正确的范围

解决方案:

import pandas as pd
import numpy as np
from math import sqrt
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Range1d
from bokeh.models.glyphs import HexTile
from bokeh.io import curdoc, show
from bokeh.util.hex import cartesian_to_axial, axial_to_cartesian

counties = np.array([
    ['DL','DY','AM','',    ''],
    ['FM','TE','AH','DN',  ''],
    ['SO','LM','CN','MN',  ''],
    ['MO','RN','LD','MH','LH'],
    ['GA','OY','WH','D' ,''  ],
    [''  ,'CE','LS','KE','WW'],
    ['LC','TA','KK','CW',''  ],
    ['KY','CR','WF','WX',''  ]
    ])

counties = np.flip(counties, (0)) # Flip UD for bokeh

# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, 0:(4+1)]
q = q[counties!='']
r = r[counties!='']

# Transform for odd-r offset coords; +r-axis goes up
q = q + (r+1)//2
#r = -r # cannot globally negate 'r', see comments

# Transform odd-r offset coords (q,r) -> (x,y)
x, y = axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')

counties_df = pd.DataFrame({'q': q, 'r': -r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})

p = figure(match_aspect=True, tools='save,crosshair')

glyph = HexTile(orientation='pointytop', q='q', r='r', size=2/3, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!?
p.add_glyph(counties_ds, glyph)

p.x_range = Range1d(-2,6)
p.y_range = Range1d(-1,8)
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None

p.text(x, y, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)],
    text_baseline="middle", text_align="center")

show(p)