从耗时和不耗时的项目生成线性时间轴表示,但仍需在上面绘制空间

13 投票
5 回答
18830 浏览
提问于 2025-04-17 08:01

这是一个关于如何为一组平行数据生成图像或其他表示形式的问题。这个问题不是关于绘图或图形用户界面编程,而是关于计算位置。

首先,我会简单介绍一下我目前的情况,接着第二张图片和示例会展示我的问题。

当前状态

示例一-简单 http://www.wargsang.de/text3935.png

我有一些一维的对象,它们通过放置在平行的“线上”来对齐。我们把这些一维对象称为“事件”,它们有一个“持续时间”,作为时间的单位。

这些事件有一种变体,就是没有任何活动的对象,它们没有数据,但有一个持续时间;我们称之为“间隙”对象。

因此,我们得到了一个包含事件和间隙的同时发生的对象的时间表,这样处理起来相对简单,可以看作是三个对象列表。

可视化也很简单:遍历这些列表,根据每个对象的持续时间来绘制它们。

class Event():
    def __init__(self, duration, displacement = 0):  #displacement is explained in the second example and the core problem of this question
        self.duration = duration
        self.displacement = displacement
        #additional data

    def self.draw(self, start_coordinate):
        """draw duration * 10 pixels in black"""
        #drawing code using start_coordinate to place the drawn object. see example graphic
        return duration * 10


class Gap():
    def __init__(self, duration, displacement = 0):
        self.duration = duration
        self.displacement = displacement
        #no data

    def self.draw(self, start_coordinate):
        """draw duration * 10 pixels in transparent"""
        #drawing code using start_coordinate to place the drawn object. see example graphic
        return duration * 10

row_one = [Event(1), Gap(1), Event(1), Gap(1), Event(1), Gap(1), Event(2)]
row_two = [Event(1), Gap(2), Event(1), Event(1), Gap(1), Event(1), Gap(1), ]
row_thr = [Gap(1), Event(1), Gap(1), Event(1), Gap(1), Event(3),]

timetable = [row_one, row_two, row_thr]

for row in timetable:
    pixelcounter = 0 # the current position.
    for item in row:
        ret = item.draw(pixelcounter) #draw on the current position. Get how width the item was
        pixelcounter += ret #save width for the next iteration        
    #instructions to move the "drawing cursor" down a few pixels so the next row does not overlap.     

问题

现在说说问题。存在一些需要图形空间但持续时间为零的对象。我称之为“位移”。

示例二-有问题 http://www.wargsang.de/text4120.png

或者我们需要一些有持续时间但也有位移的对象。当我们只有一行时,这仍然不是问题,但同步多行就复杂多了,我还没有找到解决方案。

在上面的图片中,红色块的持续时间为零并且有位移。蓝色块也有持续时间,并且同样有位移。

示例: *想象一下一个会议的时间表,每小时有不同的演讲者(我们的持续时间槽)。每一行代表一个不同的会议室。

  • 黑色块是演讲,可能在里面写有简短的主题(图形上)。

  • 蓝色块也是演讲,但主题太长,无法写下,所以我们需要更多的空间。

  • 红色块是像房间号变更这样的备注。它们本身不占用时间,但与所有在它们之后的项目相关联。

任务是找到一种方法,从上面的函数计算像素计数,使得每一行都是正确的,同时一行的位移影响所有其他行,并在那儿创建额外的空间。

目标是每一行的持续时间是固定且对齐的。任何应该在,例如,单位计数4上开始的事件或间隙,应该在同一个绝对位置开始。

这意味着任何零持续时间/位移的对象在时间/持续时间上开始于一个真实的点,但本身不消耗任何时间/持续时间,因此所有后续项目在下一个真实的持续时间事件之前都在同一个持续时间上开始。从另一个角度来看,这也意味着零持续时间的项目总是在有持续时间的事件之前开始。

在图片中,我们可以看到第二列是一个相对简单的情况,这也意味着它开始了第二个持续时间槽。尽管在那一列中有三个真实事件,但由于有一个位移项目,它们都向右移动。

第四列有一个有持续时间的项目,也有位移。同样,所有在第五槽开始的项目都向右移动。

第六列是最有趣的,也是我真正的问题,我在这里找不到解决方案。再次强调,第六列中的所有真实事件都向右移动,但仍然在同一时间开始。但这里有a)两个行中的位移对象和b)两个对象紧挨着。

因此,真实事件需要知道完整的位移,但第三行中的第二个对象也需要知道在它之前还有一个位移项目。

警告:图形表示可能会暗示一种基于表格的方法,其中每一列都有单独的宽度。但这就是这个例子的结束。实际应用处理的事件持续时间通常在300到10,000之间,但持续时间为1的情况不太可能,但在技术上是可能的。因此,表格的列宽将是一个持续时间。考虑到我们进入了数十万的完整持续时间(乘以行数),这可能会拖慢性能。

这张图片的数据看起来是这样的。我该如何用这些数据绘制第二张图片?或者需要改变什么,我对所有建议持开放态度。

非常感谢你的时间和关注。如果你不知道解决方案,请随时问我问题或指出我概念中的缺陷,这将帮助我思考。

row_one = [ Event(1), #1
            Event(0,1), Event(1), #2
            Gap(1), #3
            Event(1), #4
            Gap(1), #5
            Event(0,1), Event(1), #6
            Event(1), #7
            ] 
row_two = [ Event(1), #1
            Event(1), #2
            Gap(1), #3
            Event(1, 0.5), #4, 0,5 is not important. we can also simply to just ints. The important bit is that it has both values.
            Event(1), #5
            Event(1), #6
            Event(1), #7
            ] 
row_thr = [ Event(1), #1
            Event(1), #2
            Event(1), #3            
            Event(1), #4
            Event(1), #5            
            Event(0,1), Event(0,1), Event(1), #6   #Please pay attention to this case.
            Event(1), #7
            ] 

5 个回答

0

如果我理解你的问题没错的话……

对于每一列,在处理每一行的时候,要记录下那些持续时间为零的框的最大宽度。这样,当你最后要画出真正的事件时,它就会从该列的最大宽度的末尾开始。(我假设你已经在计算这一列的起始位置了。)

在你上面的彩色图表中,处理第一行时,你会得到持续时间为零的框的最大宽度,比如说是 [0, 10, 0, 0, 0, 10, 0]。第二行不会改变这个结果。第三行会把它改成 [0, 10, 0, 0, 0, 20, 0]。

当你要画一个真正的事件时,它的起始位置会是这一列的起始位置加上该列的最大零持续时间宽度。

0

你需要一个函数来把时间映射到x坐标上。比较难的地方是,有时候你在某个时刻需要更多的空间。这个问题可以通过一个对象列表来解决,这个列表记录了什么时候需要额外的空间,以及需要多少。

function timeToLocation(t)
  location = t * scale
  for (o : extraSpaceList)
    if o.when < t
      location = location + o.space
  return location

每当你尝试放置一个对象,发现空间不够(因为元素重叠了),你只需在需要的时刻插入一些额外的空间,比如用 extraSpaceList.add({when=2s,space=4pixels})。然后逐行处理所有的对象,最后再处理一遍,找到它们的最终位置。

如果你把对象转换成有开始时间和图形大小的形式,这样就更简单了。这样“间隙”和“事件”就没有区别了。

1

我不太确定,但我觉得你想要的是:

  • 在不同的行上,开始时间相同的位移是同步的,也就是说,它们的水平位置是一样的(每行的第一个位移和其他行的第一个位移是同步的)
  • “真实”的事件和间隙,如果它们的开始时间相同也是同步的,并且是在相同开始时间的位移之后出现
  • 事件的宽度取决于它的持续时间(可能还包括它的位移),但依赖于其他行的位移,也就是说,结束时间是不同步的

如果你想让结束时间也同步,那你需要解释一下怎么做;不过我没有看到明显的办法。

然后你会得到以下解决方案来处理你最初的问题(大写字母 = 事件,小写字母 = 位移,点 = 间隙,空格 = “等待同步”,数字是事件的开始时间):

0 123 4  567
AbC.D .e FG
A B.CcD  EF
A BCD EfgHI

在下面的例子中,你可以看到结束时间并没有同步:

0  12
AAa
Aa B
AaaB

还有一个更大的随机例子:

                         11    11  1 1     1     1  1   1  22 22   2     2  22  22   33    333    3  3  3 3   3 4   44444  4   4   4  45
    01 2 34   5678    9  01    23  4 5     6     7  8   9  01 23   4     5  67  89   01    234    5  6  7 8   9 0   12345  6   7   8  90
    AAAA  BB   CCC  dd..  EEe   Fff..      GGGGGg           ....         ...    HHH   ....        IIii  JJJ     ...   KKK  LLLLl
abbbCCC  DDDDDdd ..      EEEEE       Fff   GGG          HHH   IIIii      JJJjj  KKKK       LLLl   Mno.  PPP    qR   SSSSSs TT   uuuVV
    ...  AAAAA   BBB      CC    DDDD             ...       EE FFFF          GHhhIIII       JJ.    K  Lll.m....       NNNO  ....
    ......     AAAA      ..    ....        BBB          CCCCCc     DDDDDd        Ee  FFFFff  G hhhIIIII         JJJ   KLLLLLll        M
    .. AAA    BBBCcc  DD  EE    ..   FFF           gH   IIIIIi     J     KKk LL  MMMMM       NNNNNn           OOo   PPQQQQ  rrr...
    AAAAa .   BBBBbb  CCCCC        DDDDDd            eeeFFFFF      GG       HH  .....       IIIII         JJ    K   LM.NNNNN          .
    AAAaaBBB   CCCcc  DDDDDdd      EeFF          ...       GGgHHHH          III  JJJJ       KKK    llMMMm  nnnOOOO    PPPp ...        Q
    AAAAA     BBBBB      CCCC      .....                DDD   EEEEE          FFFff   ....    GGGG         HHHHhh     II....     j  .  .
    AAAaa..   BBBBbb  CccDDDDD       ....               EEE   .F   GgghhhII  Jj KKKK       ...    ...     LLll  ...   MMMM     N   OooP
    ....  Aa  ..BCCC      .....            DDD          EEEe  FFf  .....         GGGG       HIIIIIii          . JJ   ....  KKk     LL
    AAAAAa  bbC.....      DDDDD            ....          eeFFFFff  GGGGG         ...    hh IIJJJ        KKK     L   MMMMMmmNNNN
    ..aBBB    CCCCc   .....        .....                ...   D.   E     FFFFFff   ggHHhiiiJKKKk     LLLLL       mmmNNNOP  Q   RRR
    AA BbCCCC   DD    Ee FFFFFff     GGGGG                 HH IIIi       JjjK..  LLLll     MMMMmm    ....       .   NNNOOOOOoo        P
    AB CCCCC    .....        ddEEEE     fffGgg   HHHHHhh      II jjKKKK         LLLL       MMMM    nn..   OO    PPPPPpp QQQQQqq
    AAA  BBB   CCCC      DDdd  EE  FFF        gggHh IIIii   JJJJ         K  LLLLl    MMm   NNOOOO         .   PP    .QQQRRRRR

现在是代码(抱歉这么长,看看Timetable.__init__部分,那里是有趣的,其余的主要是格式化输出)。

from heapq import merge
from itertools import groupby, cycle, chain
from collections import defaultdict
from operator import attrgetter
from string import ascii_uppercase

# events are processed in this order:
# increasing start time, displacements (duration=0) first, and grouped by row_id
ev_sort_attrs = attrgetter("time", "duration", "row_id")


class Event:

    def __init__(self, duration, displacement=0, visible=True, time=None, row_id=None):
        self.duration = duration
        self.displacement = displacement
        self.visible = visible
        self.time = time
        self.row_id = row_id
        self.pos = None

    def draw(self, char):
        return char * self.duration + char.lower() * self.displacement

    def __lt__(self, other):
        return ev_sort_attrs(self) < ev_sort_attrs(other)


def Gap(duration):
    return Event(duration, visible=False)


class Timetable(list):
    def __init__(self, *args):
        """ compute positions for a list of rows of events """
        list.__init__(self, *args)

        # compute times for the events, and give them row_ids
        for i, row in enumerate(self):
            t = 0
            for ev in row:
                ev.time = t
                t += ev.duration
                ev.row_id = i

        # map times to position for displacements and event
        t2pos_disp = defaultdict(int) # maps times to position of synchronized start of displacements
        t2pos_ev = defaultdict(int) # maps times to position of synchronized start of events and gaps

        # the real work is done in the following loop
        t_prev = 0
        for t, g in groupby(merge(*self), key=attrgetter("time")):

            # different times should have a minimum distance corresponding to their difference
            t2pos_ev[t] = t2pos_disp[t] = max(t2pos_ev[t], t2pos_ev[t_prev] + t - t_prev)
            t_prev = t

            for (duration, row_id), g_row in groupby(g, key=attrgetter("duration", "row_id")): # process all displacements first, then the events
                pos_ev = t2pos_ev[t] if duration > 0 else t2pos_disp[t] # events and displacements start at different
                for ev in g_row:
                    ev.pos = pos_ev
                    pos_ev += ev.duration + ev.displacement
                t2pos_ev[t + ev.duration] = max(t2pos_ev[t + ev.duration], pos_ev)

        # keep our results...
        self.t2pos_ev = t2pos_ev
        self.t2pos_disp = t2pos_disp


    @staticmethod
    def str_row(row):
        """ draw row, uppercase letters for real events, lower case letters for
        displacements, dots for gaps"""

        ev_chars = cycle(ascii_uppercase)
        out = []
        l = 0
        for ev in row:
            if ev.pos > l:
                out.append(" " * (ev.pos - l))
            out.append(ev.draw(next(ev_chars) if ev.visible else "."))
            l = ev.pos + len(out[-1])
        return "".join(out)

    def __str__(self):
        max_t, max_p = max(self.t2pos_ev.items())
        w = len(str(max_t))
        header_temp = [" " * w] * (max_p + 1)
        for t, p in self.t2pos_ev.items():
            header_temp[p] = "%*d" % (w, t)
        headers = ("".join(header) for header in zip(*header_temp))

        rows = (self.str_row(row) for row in self)

        return "\n".join(chain(headers, rows))


if __name__ == "__main__":
    # original example
    row_one = [Event(1), Event(0,1), Event(1), Gap(1), Event(1), Gap(1), Event(0,1), Event(1), Event(1)]
    row_two = [Event(1), Event(1), Gap(1), Event(1, 1), Event(1), Event(1), Event(1)]
    row_thr = [Event(1), Event(1), Event(1), Event(1), Event(1), Event(0,1), Event(0,1), Event(1), Event(1)]

    timetable = Timetable([row_one, row_two, row_thr])
    print(timetable)

    print("-" * 80)

    # short example, shows ending times are not synchronized
    print(Timetable([[Event(2, 1)], [Event(1, 1), Event(1)], [Event(1, 2), Event(1)]]))

    print("-" * 80)

    # larger random example
    def random_row(l):
        import random
        res = []
        t = 0
        while t < l:
            x = random.random()
            if x < 0.1: res.append(Event(0, random.randint(1, 3)))
            elif x < 0.8: res.append(Event(min(random.randint(1, 5), l - t), random.randint(0, 1) * random.randint(0, 2)))
            else: res.append(Gap(min(random.randint(1, 5), l - t)))
            t += res[-1].duration
        return res

    print(Timetable([random_row(50) for _ in range(15)]))

撰写回答