web-dev-qa-db-ja.com

カレンダーイベントの視覚化。最大幅でイベントをレイアウトするアルゴリズム

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

calendar events layout

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

これをレイアウトするアルゴリズムが必要です。

前もって感謝します

54
Alexey
  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:十分なスペースがある場合、イベントを右に展開します。

55
Markus Jarderot

受け入れられた回答は、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:これは本当に受け入れられた答えへのコメントであるべきです。残念ながら、コメントする権限がありません。

13
Gabe Sidler