63

各イベントボックスの幅が最大になるようにカレンダーイベントをレイアウトするアルゴリズム(javascriptを使用してクライアント側で開発されますが、実際には問題ではありません。ほとんどの場合、アルゴリズム自体に関心があります)についてサポートが必要です。次の写真をご覧ください。

カレンダーイベントのレイアウト

Y軸は時間です。したがって、「テストイベント」が正午に開始され(たとえば)、それ以上交差するものがない場合は、幅全体が100%になります。「ウィークリーレビュー」は「タンブリングYMCA」と「アンナ/アメリア」と交差していますが、後者の2つは交差していないため、すべて50%を占めています。Test3、Test4、Test5はすべて交差しているため、最大幅はそれぞれ33.3%です。ただし、Test3は33%固定されているため(上​​記を参照)、Test7は66%であり、使用可能なすべてのスペース(66%)を使用します。

これをどのようにレイアウトするかというアルゴリズムが必要です。

前もって感謝します

4

3 に答える 3

65
  1. 左端だけの無制限のグリッドを考えてみてください。
  2. 各イベントは1セル幅で、高さと垂直位置は開始時間と終了時間に基づいて固定されています。
  3. 各イベントを、その列の以前のイベントと交差しないように、できるだけ左の列に配置するようにしてください。
  4. 次に、接続された各イベントグループが配置されると、実際の幅は、グループで使用される最大列数の1/nになります。
  5. 左端と右端のイベントを展開して、残りのスペースを使い切ることもできます。
/// Pick the left and right positions of each event, such that there are no overlap.
/// Step 3 in the algorithm.
void LayoutEvents(IEnumerable<Event> events)
{
    var columns = new List<List<Event>>();
    DateTime? lastEventEnding = null;
    foreach (var ev in events.OrderBy(ev => ev.Start).ThenBy(ev => ev.End))
    {
        if (ev.Start >= lastEventEnding)
        {
            PackEvents(columns);
            columns.Clear();
            lastEventEnding = null;
        }
        bool placed = false;
        foreach (var col in columns)
        {
            if (!col.Last().CollidesWith(ev))
            {
                col.Add(ev);
                placed = true;
                break;
            }
        }
        if (!placed)
        {
            columns.Add(new List<Event> { ev });
        }
        if (lastEventEnding == null || ev.End > lastEventEnding.Value)
        {
            lastEventEnding = ev.End;
        }
    }
    if (columns.Count > 0)
    {
        PackEvents(columns);
    }
}

/// Set the left and right positions for each event in the connected group.
/// Step 4 in the algorithm.
void PackEvents(List<List<Event>> columns)
{
    float numColumns = columns.Count;
    int iColumn = 0;
    foreach (var col in columns)
    {
        foreach (var ev in col)
        {
            int colSpan = ExpandEvent(ev, iColumn, columns);
            ev.Left = iColumn / numColumns;
            ev.Right = (iColumn + colSpan) / numColumns;
        }
        iColumn++;
    }
}

/// Checks how many columns the event can expand into, without colliding with
/// other events.
/// Step 5 in the algorithm.
int ExpandEvent(Event ev, int iColumn, List<List<Event>> columns)
{
    int colSpan = 1;
    foreach (var col in columns.Skip(iColumn + 1))
    {
        foreach (var ev1 in col)
        {
            if (ev1.CollidesWith(ev))
            {
                return colSpan;
            }
        }
        colSpan++;
    }
    return colSpan;
}

編集:イベントがソートされていると想定するのではなく、イベントをソートするようになりました。

Edit2:十分なスペースがある場合、イベントを右側に展開します。

于 2012-07-04T06:57:09.513 に答える
16

受け入れられた答えは、5つのステップからなるアルゴリズムを説明しています。受け入れられた回答のコメントにリンクされている実装例は、ステップ1から4のみを実装します。ステップ5は、右端のイベントが使用可能なすべてのスペースを使用することを確認することです。OPによって提供された画像のイベント7を参照してください。

説明したアルゴリズムのステップ5を追加して、特定の実装を拡張しました。

$( document ).ready( function( ) {
  var column_index = 0;
  $( '#timesheet-events .daysheet-container' ).each( function() {

    var block_width = $(this).width();
    var columns = [];
    var lastEventEnding = null;

    // Create an array of all events
    var events = $('.bubble_selector', this).map(function(index, o) {
      o = $(o);
      var top = o.offset().top;
      return {
        'obj': o,
        'top': top,
        'bottom': top + o.height()
      };
    }).get();

    // Sort it by starting time, and then by ending time.
    events = events.sort(function(e1,e2) {
      if (e1.top < e2.top) return -1;
      if (e1.top > e2.top) return 1;
      if (e1.bottom < e2.bottom) return -1;
      if (e1.bottom > e2.bottom) return 1;
      return 0;
    });

    // Iterate over the sorted array
    $(events).each(function(index, e) {

      // Check if a new event group needs to be started
      if (lastEventEnding !== null && e.top >= lastEventEnding) {
        // The latest event is later than any of the event in the 
        // current group. There is no overlap. Output the current 
        // event group and start a new event group.
        PackEvents( columns, block_width );
        columns = [];  // This starts new event group.
        lastEventEnding = null;
      }

      // Try to place the event inside the existing columns
      var placed = false;
      for (var i = 0; i < columns.length; i++) {                   
        var col = columns[ i ];
        if (!collidesWith( col[col.length-1], e ) ) {
          col.push(e);
          placed = true;
          break;
        }
      }

      // It was not possible to place the event. Add a new column 
      // for the current event group.
      if (!placed) {
        columns.push([e]);
      }

      // Remember the latest event end time of the current group. 
      // This is later used to determine if a new groups starts.
      if (lastEventEnding === null || e.bottom > lastEventEnding) {
        lastEventEnding = e.bottom;
      }
    });

    if (columns.length > 0) {
      PackEvents( columns, block_width );
    }
  });
});


// Function does the layout for a group of events.
function PackEvents( columns, block_width )
{
  var n = columns.length;
  for (var i = 0; i < n; i++) {
    var col = columns[ i ];
    for (var j = 0; j < col.length; j++)
    {
      var bubble = col[j];
      var colSpan = ExpandEvent(bubble, i, columns);
      bubble.obj.css( 'left', (i / n)*100 + '%' );
      bubble.obj.css( 'width', block_width * colSpan / n - 1 );
    }
  }
}

// Check if two events collide.
function collidesWith( a, b )
{
  return a.bottom > b.top && a.top < b.bottom;
}

// Expand events at the far right to use up any remaining space. 
// Checks how many columns the event can expand into, without 
// colliding with other events. Step 5 in the algorithm.
function ExpandEvent(ev, iColumn, columns)
{
    var colSpan = 1;

    // To see the output without event expansion, uncomment 
    // the line below. Watch column 3 in the output.
    //return colSpan;

    for (var i = iColumn + 1; i < columns.length; i++) 
    {
      var col = columns[i];
      for (var j = 0; j < col.length; j++)
      {
        var ev1 = col[j];
        if (collidesWith(ev, ev1))
        {
           return colSpan;
        }
      }
      colSpan++;
    }
    return colSpan;
}

実用的なデモはhttp://jsbin.com/detefuveta/edit?html,js,outputで入手でき ます。右端のイベントを展開する例については、出力の列3を参照してください。

PS:これは本当に受け入れられた答えへのコメントでなければなりません。残念ながら、コメントする権限がありません。

于 2015-01-30T08:01:57.567 に答える
1

これは、Typescriptを使用してReactに実装されたものと同じアルゴリズムです。(もちろん)ニーズに合わせて微調整する必要がありますが、Reactで作業している人には役立つはずです。

// Place concurrent meetings side-by-side (like GCal).
// @see {@link https://share.clickup.com/t/h/hpxh7u/WQO1OW4DQN0SIZD}
// @see {@link https://stackoverflow.com/a/11323909/10023158}
// @see {@link https://jsbin.com/detefuveta/edit}

// Check if two events collide (i.e. overlap).
function collides(a: Timeslot, b: Timeslot): boolean {
  return a.to > b.from && a.from < b.to;
}

// Expands events at the far right to use up any remaining
// space. Returns the number of columns the event can
// expand into, without colliding with other events.
function expand(
  e: Meeting,
  colIdx: number,
  cols: Meeting[][]
): number {
  let colSpan = 1;
  cols.slice(colIdx + 1).some((col) => {
    if (col.some((evt) => collides(e.time, evt.time)))
      return true;
    colSpan += 1;
    return false;
  });
  return colSpan;
}

// Each group contains columns of events that overlap.
const groups: Meeting[][][] = [];
// Each column contains events that do not overlap.
let columns: Meeting[][] = [];
let lastEventEnding: Date | undefined;
// Place each event into a column within an event group.
meetings
  .filter((m) => m.time.from.getDay() === day)
  .sort(({ time: e1 }, { time: e2 }) => {
    if (e1.from < e2.from) return -1;
    if (e1.from > e2.from) return 1;
    if (e1.to < e2.to) return -1;
    if (e1.to > e2.to) return 1;
    return 0;
  })
  .forEach((e) => {
    // Check if a new event group needs to be started.
    if (
      lastEventEnding &&
      e.time.from >= lastEventEnding
    ) {
      // The event is later than any of the events in the
      // current group. There is no overlap. Output the
      // current event group and start a new one.
      groups.push(columns);
      columns = [];
      lastEventEnding = undefined;
    }

    // Try to place the event inside an existing column.
    let placed = false;
    columns.some((col) => {
      if (!collides(col[col.length - 1].time, e.time)) {
        col.push(e);
        placed = true;
      }
      return placed;
    });

    // It was not possible to place the event (it overlaps
    // with events in each existing column). Add a new column
    // to the current event group with the event in it.
    if (!placed) columns.push([e]);

    // Remember the last event end time of the current group.
    if (!lastEventEnding || e.time.to > lastEventEnding)
      lastEventEnding = e.time.to;
  });
groups.push(columns);

// Show current time indicator if today is current date.
const date = getDateWithDay(day, startingDate);
const today =
  now.getFullYear() === date.getFullYear() &&
  now.getMonth() === date.getMonth() &&
  now.getDate() === date.getDate();
const { y: top } = getPosition(now);

return (
  <div
    key={nanoid()}
    className={styles.cell}
    ref={cellRef}
  >
    {today && (
      <div style={{ top }} className={styles.indicator}>
        <div className={styles.dot} />
        <div className={styles.line} />
      </div>
    )}
    {groups.map((cols: Meeting[][]) =>
      cols.map((col: Meeting[], colIdx) =>
        col.map((e: Meeting) => (
          <MeetingItem
            now={now}
            meeting={e}
            viewing={viewing}
            setViewing={setViewing}
            editing={editing}
            setEditing={setEditing}
            setEditRndVisible={setEditRndVisible}
            widthPercent={
              expand(e, colIdx, cols) / cols.length
            }
            leftPercent={colIdx / cols.length}
            key={e.id}
          />
        ))
      )
    )}
  </div>
);

あなたはここで完全なソースコードを見ることができます。これは非常に意見の分かれる実装であることを認めますが、それは私を助けてくれたので、ここに投稿して、他の誰かに役立つかどうかを確認します!

于 2021-02-18T01:22:25.613 に答える