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

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)