You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
645 lines
19 KiB
645 lines
19 KiB
import numpy as np |
|
import skia |
|
from typing import NamedTuple, TypedDict |
|
from functools import cache |
|
from colourpal import * |
|
|
|
from collections import OrderedDict |
|
|
|
|
|
class Rect(NamedTuple): |
|
x: int |
|
y: int |
|
w: int |
|
h: int |
|
|
|
def area(self): |
|
return self.w * self.h |
|
|
|
def center(self): |
|
return self.x + self.w / 2, self.y + self.h / 2 |
|
|
|
@cache |
|
def skia(self): |
|
return skia.Rect(self.x, self.y, self.x + self.w, self.y + self.h) |
|
|
|
def contains_point(self, x: int, y: int): |
|
right = self.x + self.w |
|
bottom = self.y + self.h |
|
left = self.x |
|
top = self.y |
|
return (x > left and x < right) and (y > top and y < bottom) |
|
|
|
|
|
def rect_step_towards(self: Rect, other: Rect, amount=0.2): |
|
if amount >= 1: |
|
return other, True |
|
right = self.x + self.w |
|
bottom = self.y + self.h |
|
left = self.x |
|
top = self.y |
|
|
|
other_right = other.x + other.w |
|
other_bottom = other.y + other.h |
|
other_left = other.x |
|
other_top = other.y |
|
|
|
add_right = other_right - right |
|
add_left = other_left - left |
|
add_top = other_top - top |
|
add_bottom = other_bottom - bottom |
|
diff = abs(add_left) + abs(add_right) + abs(add_top) + abs(add_bottom) |
|
if diff < 5 * amount: |
|
return other, True |
|
|
|
left += amount * add_left |
|
right += amount * add_right |
|
top += amount * add_top |
|
bottom += amount * add_bottom |
|
|
|
return Rect(left, top, right - left, bottom - top), False |
|
|
|
|
|
def rect_subdivide_with(self: Rect, props: list["TreeFrame"], padding=0): |
|
# assert isinstance (props[0] , TreeFrame) |
|
if self.h > self.w: |
|
subs = Rect(self.y, self.x, self.h, self.w).subdivide_with( |
|
props, padding=padding |
|
) |
|
|
|
return [(df, Rect(s.y, s.x, s.h, s.w)) for df, s in subs] |
|
|
|
rect = Rect(self.x, self.y, self.w, self.h) |
|
total = sum(x.size for x in props) |
|
width_budget = self.w - padding * (len(props) + 1) |
|
rects = [] |
|
x = rect.x + padding |
|
y = rect.y + padding |
|
h = self.h - 2 * padding |
|
for df in props: |
|
normalised = float(df.size) / float(total) |
|
df.attribs["normalised"] = normalised |
|
|
|
w = normalised * width_budget |
|
rects.append((df, Rect(x, y, w, h))) |
|
x += w + padding |
|
|
|
return rects |
|
|
|
|
|
def rect_subdivide(self: Rect, proportions: list[int], padding=0): |
|
if self.h > self.w: |
|
subs = Rect(self.y, self.x, self.h, self.w).subdivide( |
|
proportions, padding=padding |
|
) |
|
return [Rect(s.y, s.x, s.h, s.w) for s in subs] |
|
|
|
rect = Rect(self.x, self.y, self.w, self.h) |
|
total = sum(proportions) |
|
width_budget = self.w - padding * (len(proportions) + 1) |
|
rects = [] |
|
x = rect.x + padding |
|
y = rect.y + padding |
|
h = self.h - 2 * padding |
|
for proportion in proportions: |
|
w = math.floor((float(proportion) / float(total)) * width_budget) |
|
rects.append(Rect(x, y, w, h)) |
|
x += w + padding |
|
|
|
return rects |
|
|
|
|
|
Rect.subdivide = rect_subdivide |
|
Rect.subdivide_with = rect_subdivide_with |
|
Rect.step_towards = rect_step_towards |
|
|
|
|
|
from collections import defaultdict |
|
from functools import lru_cache |
|
|
|
|
|
class DataFrame(NamedTuple): |
|
path: tuple[str] |
|
size: int |
|
weight: float |
|
|
|
|
|
class FrameAttribs(TypedDict): |
|
color: str |
|
label: str |
|
rect: Rect |
|
|
|
|
|
class TreeFrame: |
|
path: tuple[str] |
|
size: int |
|
weight: float |
|
children: OrderedDict |
|
attribs: dict |
|
|
|
def __init__(self, path, size, weight, children, attribs): |
|
self.path = path |
|
self.size = size |
|
self.weight = weight |
|
self.children = children |
|
self.attribs = attribs |
|
|
|
|
|
def is_leaf(self): |
|
return len(self.children) == 0 |
|
|
|
def trim(self, threshold=0.01): |
|
children = OrderedDict( |
|
dict( |
|
(k, v.trim()) |
|
for k, v in filter( |
|
lambda x: x[1].attribs["normalised"] > threshold, |
|
self.children.items(), |
|
) |
|
) |
|
) |
|
return TreeFrame(self.path, self.size, self.weight, children, self.attribs) |
|
|
|
def count_children(self): |
|
return 1 + sum(v.count_children() for v in self.children.values()) |
|
|
|
def cumsum_attribute(self, attribute, into_attribute=None, op=sum): |
|
for child in self.children.values(): |
|
child.cumsum_attribute(attribute, into_attribute=into_attribute, op=op) |
|
|
|
if into_attribute is None: |
|
into_attribute = attribute |
|
|
|
amount = op(child.attribs[into_attribute] for child in self.children.values() |
|
if into_attribute in child.attribs) |
|
if attribute in self.attribs: |
|
amount = op((amount, self.attribs[attribute])) |
|
|
|
self.attribs[into_attribute] = amount |
|
|
|
|
|
|
|
def locate_at_key(self, key: tuple, rem=tuple()): |
|
|
|
if len(key) == 0 and len(rem) == 0: |
|
return self |
|
|
|
rem = rem + (key[len(rem)],) |
|
|
|
if key == rem: |
|
if rem in self.children: |
|
return self.children[rem] |
|
|
|
if len(rem) < len(key): |
|
if rem in self.children: |
|
return self.children[rem].locate_at_key(key, rem) |
|
return None |
|
|
|
def pack( |
|
self, rect: Rect, level=0, max_level=-1, at=tuple(), padding=0, rattrib="rect" |
|
): |
|
child = self.locate_at_key(at) |
|
if len(at) > 0: |
|
if child is not None: |
|
return chlid.pack( |
|
rect, |
|
level=0, |
|
max_level=max_level, |
|
at=at[1:], |
|
padding=padding, |
|
rattrib=rattrib, |
|
) |
|
return None |
|
# Locate subtree |
|
# if len(at) > 0: |
|
# p = tuple(list(self.path) + [at[0]]) |
|
# for k, i in self.children.items(): |
|
# if i.path == p: |
|
# return i.pack( |
|
# rect, |
|
# level=0, |
|
# max_level=max_level, |
|
# at=at[1:], |
|
# padding=padding, |
|
# rattrib=rattrib, |
|
# ) |
|
# return None |
|
|
|
self.attribs["level"] = level |
|
self.attribs[rattrib] = rect |
|
if max_level != -1 and level > max_level: |
|
self.attribs.pop(rattrib) |
|
return self |
|
|
|
if len(self.children) < 1: |
|
return self |
|
|
|
for df, crect in rect.subdivide_with(self.children.values(), padding=padding): |
|
df.attribs[rattrib] = crect |
|
df.pack( |
|
crect, |
|
level=level + 1, |
|
max_level=max_level, |
|
padding=padding, |
|
rattrib=rattrib, |
|
) |
|
|
|
return self |
|
|
|
def leaves(self, max_level=-1, level=0): |
|
if self.is_leaf(): |
|
yield self |
|
|
|
if max_level == -1 or level < max_level: |
|
for k, i in self.children.items(): |
|
yield from i.leaves(max_level=max_level, level=level + 1) |
|
|
|
|
|
def items(self, max_level=-1, level=0): |
|
yield self |
|
if max_level == -1 or level < max_level: |
|
for k, i in self.children.items(): |
|
yield from i.items(max_level=max_level, level=level + 1) |
|
|
|
def level(self) -> int: |
|
if "level" in self.attribs: |
|
return self.attribs["level"] |
|
return len(self.path) - 1 |
|
|
|
def locate_at_position(self, x: int, y: int, levels=-1): |
|
containing = None |
|
for k, child in self.children.items(): |
|
if child.attribs["rect"].contains_point(x, y): |
|
containing = child |
|
break |
|
|
|
if containing is None: |
|
return self |
|
|
|
cr = containing.attribs["rect"] |
|
if levels < 0: |
|
next = containing.locate_at_position(x, y, levels) |
|
if next is not None: |
|
return next |
|
else: |
|
return self |
|
|
|
if levels > 0: |
|
return containing.locate_at_position(x, y, levels - 1) |
|
|
|
return containing |
|
|
|
|
|
class hashable_defaultdict(defaultdict): |
|
def __hash__(self): |
|
return id(self) |
|
|
|
|
|
class hashable_immutable_dict(dict): |
|
def __hash__(self): |
|
return id(self) |
|
|
|
def _immutable(self, *args, **kws): |
|
raise TypeError("object is immutable") |
|
|
|
__setitem__ = _immutable |
|
__delitem__ = _immutable |
|
clear = _immutable |
|
update = _immutable |
|
setdefault = _immutable |
|
pop = _immutable |
|
popitem = _immutable |
|
|
|
|
|
class TreeMap: |
|
levels: hashable_immutable_dict[tuple, DataFrame] |
|
|
|
def __init__(self, levels): |
|
self.levels = hashable_immutable_dict(levels) |
|
self.levels_cache = hashable_defaultdict(list) |
|
|
|
for k, v in self.levels.items(): |
|
for l in range(len(k) + 1): |
|
self.levels_cache[k[0:l]].append((k, v)) |
|
|
|
def locate_n(self, path: tuple): |
|
yield from self.levels_cache[path] |
|
|
|
# for k, v in self.levels.items(): |
|
# if k[0 : len(path)] == path: |
|
# yield (k, v) |
|
|
|
|
|
def to_groups(self): |
|
groups = defaultdict(dict) |
|
|
|
for k, v in self.levels.items(): |
|
for i in range(len(k)): |
|
groups[i][k[0:i]] = self.locate_n(k[0:i]) |
|
return groups |
|
|
|
|
|
def to_tree_r(self, path: tuple): |
|
groups = self.groups_under(path) |
|
total = sum(x.size for x in groups) |
|
if len(groups) == 0: |
|
return TreeFrame(path, 0, 0, {}, {}) |
|
|
|
if len(groups) == 1: |
|
g = groups[0] |
|
return TreeFrame(path, g.size, g.weight, {}, {}) |
|
|
|
return TreeFrame( |
|
path, |
|
sum(x.size for x in groups), |
|
sum(x.weight for x in groups) / len(groups), |
|
{g.path: self.to_tree_r(g.path) for g in groups if g.path != path}, |
|
{}, |
|
) |
|
|
|
def sort(self): |
|
return TreeMap( |
|
{ |
|
k: v |
|
for k, v in sorted(self.levels.items(), key=lambda item: item[1].size) |
|
} |
|
) |
|
|
|
def to_tree(self, base_name="base"): |
|
unique = {} |
|
for k in self.levels.keys(): |
|
unique[(k[0],)] = None |
|
res = {} |
|
for k in unique.keys(): |
|
res[k] = self.to_tree_r(k) |
|
if len(res) == 1: |
|
return list(res.values())[0] |
|
if len(res) != 1: |
|
return TreeFrame(("base",), 1, 1, res, {}) |
|
|
|
@cache |
|
def groups_under(self, key: tuple): |
|
total_size = defaultdict(int) |
|
avg_weight = defaultdict(float) |
|
num_items = defaultdict(int) |
|
|
|
for k, v in self.locate_n(key): |
|
total_size[k[0 : len(key) + 1]] += v.size |
|
avg_weight[k[0 : len(key) + 1]] += v.weight |
|
num_items[k[0 : len(key) + 1]] += 1 |
|
|
|
for k in avg_weight: |
|
avg_weight[k] /= num_items[k] |
|
|
|
return [DataFrame(k, total_size[k], avg_weight[k]) for k in total_size] |
|
|
|
|
|
import random |
|
import math |
|
|
|
def to_rgb(colour: CColour): |
|
return colour.get_r(), colour.get_g(), colour.get_b() |
|
#return int(255 * colour.get_red()), int(255 * colour.get_green()), int(255 * colour.get_blue()), |
|
|
|
def to_sk(colour: CColour): |
|
return skia.Color( |
|
colour.get_r(), |
|
colour.get_g(), |
|
colour.get_b() |
|
) |
|
|
|
#def to_sk(colour: Color): |
|
# return to_sk_ccol(colour) |
|
# return skia.Color( |
|
# int(255 * colour.get_red()), |
|
# int(255 * colour.get_green()), |
|
# int(255 * colour.get_blue()), |
|
# ) |
|
# |
|
|
|
def set_colours_gradient(y: TreeFrame, gradient: list[CColour]): |
|
print("Set colours") |
|
for tf in y.items(): |
|
index = int((len(gradient) - 1) * tf.attribs["gradient"]) |
|
tf.attribs["colour"] = gradient[index].hsv() |
|
|
|
def set_colours_bw_new(y: TreeFrame): |
|
print("Set colours") |
|
for tf in y.items(): |
|
tf.attribs["colour"] = hsv(0, 0, 1) |
|
|
|
|
|
#def set_colours_bw(y: TreeFrame): |
|
# print("Set colours") |
|
# for tf in y.items(): |
|
# tf.attribs["colour"] = Color("white") |
|
# tf.attribs["colour"].set_luminance(min(tf.weight, 1)) |
|
# |
|
|
|
def set_colours_new(y: TreeFrame, start: CColour, end: CColour): |
|
y.attribs["colour"] = start |
|
#y.attribs["colour"].set_v(1) |
|
|
|
colours = list(make_gradient(start, end, 101)) |
|
|
|
""" |
|
levels = [0] + list( |
|
np.cumsum( |
|
[ |
|
math.floor(100 * child.attribs["normalised"]) |
|
for child in y.children.values() |
|
] |
|
) |
|
) |
|
""" |
|
children = list(y.children.values()) |
|
cum = 0 |
|
next_cum = 0 |
|
for i in range(len(y.children)): |
|
child = children[i] |
|
next_cum += math.floor(100 * child.attribs["normalised"]) |
|
#col_index = math.floor(100 * child.attribs["normalised"]) |
|
set_colours_new(child, colours[cum], colours[next_cum]) |
|
cum = next_cum |
|
|
|
|
|
#def set_colours(y: TreeFrame, start: Color, end: Color): |
|
# y.attribs["colour"] = Color(start) |
|
# y.attribs["colour"].set_luminance(0.7) |
|
# |
|
# colours = list(start.range_to(end, 101)) |
|
# """ |
|
# levels = [0] + list( |
|
# np.cumsum( |
|
# [ |
|
# math.floor(100 * child.attribs["normalised"]) |
|
# for child in y.children.values() |
|
# ] |
|
# ) |
|
# ) |
|
# """ |
|
# children = list(y.children.values()) |
|
# cum = 0 |
|
# next_cum = 0 |
|
# for i in range(len(y.children)): |
|
# child = children[i] |
|
# next_cum += math.floor(100 * child.attribs["normalised"]) |
|
# #col_index = math.floor(100 * child.attribs["normalised"]) |
|
# set_colours(child, colours[cum], colours[next_cum]) |
|
# cum = next_cum |
|
# |
|
|
|
def draw_labels( |
|
canvas, y: TreeFrame, label_level=1, max_level=4, border_width=1, text_scale=1, |
|
autoscale=False |
|
): |
|
text_paint = skia.Paint(AntiAlias=True, Color=skia.ColorBLACK) |
|
text_stroke_paint = skia.Paint(AntiAlias=True, |
|
Color=skia.ColorWHITE, |
|
Style=skia.Paint.kStroke_Style, |
|
StrokeWidth=text_scale * 2) |
|
|
|
|
|
for tm in y.items(max_level=max_level): |
|
rect = tm.attribs["rect"] |
|
if tm.level() == label_level: |
|
font = skia.Font(None, 12) |
|
font.setSize(12 * text_scale) |
|
text = skia.TextBlob(tm.attribs["label"], font) |
|
textb = text.bounds() |
|
|
|
cx, cy = rect.center() |
|
tw, th = textb.width(), textb.height() |
|
|
|
if "sk_colour" not in tm.attribs: |
|
colour = to_sk(tm.attribs["colour"]) |
|
stroke_colour = tm.attribs["colour"].hsv() |
|
stroke_colour.set_v(stroke_colour.get_v() * 0.8) |
|
stroke_colour = to_sk(stroke_colour) |
|
tm.attribs["rgbcol"] = to_rgb(tm.attribs["colour"]) |
|
tm.attribs["sk_colour"] = colour |
|
tm.attribs["sk_stroke_colour"] = stroke_colour |
|
|
|
r, g, b = tm.attribs["rgbcol"] |
|
colour = skia.Color(int(min(tm.weight, 1) * r), |
|
int(min(tm.weight, 1) * g), |
|
int(min(tm.weight, 1) * b)) |
|
|
|
text_stroke_paint = skia.Paint(AntiAlias=True, |
|
Color=skia.ColorWHITE, |
|
Style=skia.Paint.kStroke_Style, |
|
StrokeWidth=text_scale * 2) |
|
|
|
if tw < rect.w and th < rect.h: |
|
canvas.drawTextBlob(text, cx - tw / 2, cy + th / 2, text_stroke_paint) |
|
canvas.drawTextBlob(text, cx - tw / 2, cy + th / 2, text_paint) |
|
elif autoscale: |
|
new_scale = min(rect.w / tw, rect.h / th) |
|
if new_scale > 0.1: |
|
sz = font.getSize() |
|
font.setSize(sz * new_scale) |
|
text = skia.TextBlob(tm.attribs["label"], font) |
|
textb = text.bounds() |
|
tw, th = textb.width(), textb.height() |
|
|
|
text_stroke_paint = skia.Paint(AntiAlias=True, |
|
Color=skia.ColorWHITE, |
|
Style=skia.Paint.kStroke_Style, |
|
StrokeWidth=text_scale * 2 * new_scale) |
|
|
|
canvas.drawTextBlob(text, cx - tw / 2, cy + th / 2, text_stroke_paint) |
|
canvas.drawTextBlob(text, cx - tw / 2, cy + th / 2, text_paint) |
|
|
|
|
|
def draw( |
|
canvas, y: TreeFrame, label_level=1, max_level=4, border_width=1, text_scale=1, leaves_only=False |
|
): |
|
# canvas.clear(skia.ColorWHITE) |
|
iter = y.leaves if leaves_only else y.items |
|
for tm in iter(max_level=max_level): |
|
if "rect" not in tm.attribs: |
|
max_level = tm.level() |
|
continue |
|
rect = tm.attribs["rect"] |
|
if rect.w < 0 or rect.h < 0: |
|
continue |
|
|
|
colour = None |
|
stroke_colour = None |
|
if "sk_colour" in tm.attribs: |
|
colour = tm.attribs["sk_colour"] |
|
stroke_colour = tm.attribs["sk_stroke_colour"] |
|
|
|
r,g,b = tm.attribs["rgbcol"] |
|
colour = skia.Color(int(min(tm.weight, 1) * r), |
|
int(min(tm.weight, 1) * g), |
|
int(min(tm.weight, 1) * b)) |
|
|
|
elif "colour" in tm.attribs: |
|
colour = to_sk(tm.attribs["colour"]) |
|
stroke_colour = tm.attribs["colour"].hsv() |
|
stroke_colour.set_v(stroke_colour.get_v() * 0.8) |
|
stroke_colour = to_sk(stroke_colour) |
|
tm.attribs["rgbcol"] = to_rgb(tm.attribs["colour"]) |
|
tm.attribs["sk_colour"] = colour |
|
tm.attribs["sk_stroke_colour"] = stroke_colour |
|
|
|
r,g,b = tm.attribs["rgbcol"] |
|
colour = skia.Color(int(min(tm.weight, 1) * r), |
|
int(min(tm.weight, 1) * g), |
|
int(min(tm.weight, 1) * b)) |
|
else: |
|
colour = skia.Color( |
|
random.randint(0, 255), |
|
random.randint(0, 255), |
|
random.randint(0, 255), |
|
) |
|
stroke_colour = colour |
|
|
|
|
|
|
|
t_border_width = border_width |
|
if "highlight" in tm.attribs: |
|
colour = to_sk(tm.attribs["colour"]) |
|
stroke_colour = tm.attribs["colour"] |
|
stroke_colour.set_v(0.1) |
|
stroke_colour = to_sk(stroke_colour) |
|
t_border_width = border_width * 2 |
|
|
|
paint = skia.Paint(AntiAlias=False, Color=colour) |
|
# if tm.level() < 1: |
|
# paint = skia.Paint(AntiAlias=False, Color=skia.ColorWHITE) |
|
|
|
stroke_style = skia.Paint( |
|
AntiAlias=True, |
|
Color=colour, |
|
Style=skia.Paint.kStroke_Style, |
|
StrokeWidth=t_border_width, |
|
) |
|
|
|
canvas.drawRect(rect.skia(), paint) |
|
canvas.drawRect(rect.skia(), stroke_style) |
|
|
|
|
|
|
|
# Text labels |
|
|
|
|
|
def draw_text( |
|
canvas, text: str, cx: float, cy: float, size=12 |
|
): |
|
font = skia.Font(None, size) |
|
text = skia.TextBlob(text, font) |
|
textb = text.bounds() |
|
text_paint = skia.Paint(AntiAlias=True, Color=skia.ColorWHITE, style=skia.Paint.kFill_Style) |
|
text_stroke_paint = skia.Paint(AntiAlias=True, |
|
Color=skia.ColorWHITE, |
|
Style=skia.Paint.kStroke_Style, |
|
StrokeWidth=2) |
|
|
|
|
|
#tw, th = textb.width(), textb.height() |
|
#canvas.drawTextBlob(text, cx, cy, text_stroke_paint) |
|
canvas.drawTextBlob(text, cx, cy, text_paint) |
|
# canvas.clear(skia.ColorWHITE) |
|
|
|
|