Search code examples
d3.jsrowscalecategorical-data

Assign distinct y coordinate to each categorical value


I have a clear requirement to calculate a y coordinate in a d3 context, but I am struggling to...

a) find the d3 (scaling?) component and usage pattern to deliver the solution

b) identify any navigable d3 API resources which would help me find a)

I have a list of events which have a start and finish time and which are each associated with a process in which they execute sequentially. It's natural to present this like a multi-track interface (a bit like an audio editor or a gantt chart) in which each process/track is allocated its own horizontal swim lane, and the events are laid out horizontally according to their start and end time. It might end up looking a bit like this view from a research publication

enter image description here

Event data items have a reference to their process id (in the diagram shown the process ids would be M1, M2 and the events would be the little boxes labelled with numbers). I know each event's start and end time (determining their x coordinate and width) but I need to put the rectangle for the action in the right process row (y coordinate).

The list of events will be updating continually as they start and finish, and possibly mediated by a filter/brush in the future.

The number of rows varies as the number of processes varies. For this reason, only after joining a list of events (and encountering all the process ids they refer to) do you know how many rows are needed, the row height, and therefore what each event's y scale position should be. When a new event item enters the selection which points to a new process id, the scale should rejig to add an extra row. When it exits the selection, it may have been the last one in that process row, so then the row should disappear.

Is there a pattern for assigning the y coordinate of an event's row on a dynamic basis by its (categorical) process id for a dynamic list of event data?

How do I find patterns and solutions for a case like this?


Solution

  • Here is a simple snippet using the following data model:

    [
      {
        label: string; // Row label
        color: string; // Row color
        items: [ { label: string; start: number; end: number; }, ...];
      },
      ...
    ]
    

    The snippet uses d3.scaleLinear and d3.axisBottom routines.

    NOTE: I use _top because (for some misterious reason) top is already used... I suppose it's a bug in D3 V5 and suggest to use V6.

    const data = [
      {
        label: 'M1',
        color: 'red',
        items: [
          {label: 'A', start: 10, end: 12},
          {label: 'B', start: 15, end: 20}
        ]
      },
      {
        label: 'M2',
        color: 'orange',
        items: [
          {label: 'C', start: 0, end: 4},
          {label: 'D', start: 4, end: 6},
          {label: 'E', start: 11, end: 17}
        ]
      },
      {
        label: 'M3',
        color: 'yellow',
        items: [
          {label: 'F', start: 0, end: 3},
          {label: 'G', start: 3, end: 9},
          {label: 'H', start: 9, end: 14}
        ]
      }
    ];
    
    
    const ticks = 25;
    const left = 50;
    const _top = 50;
    const rowHeight = 20;
    const labelWidth = 30;
    const bottom = _top + data.length * rowHeight;
    
    const svg = d3.select("svg");
    
    const xScale = d3.scaleLinear()
      .domain([0, ticks])
      .range([0, 300]);
      
    const xAxis = d3.axisBottom()
        .scale(xScale);
    
    svg.append("g")
        .attr('transform', `translate(${left}, ${bottom - 1})`)
      .call(xAxis);
          
    let i, j;      
    for (i = 0; i <= ticks; i++)
        svg.append('line')
        .attr('x1', xScale(i) + left)
        .attr('x2', xScale(i) + left)
        .attr('y1', _top)
        .attr('y2', bottom)
        .style('stroke', 'black');
        
    for (i = 0; i < data.length; i++) {
        svg.append('line')
        .attr('x1', left)
        .attr('x2', xScale(ticks) + left)
        .attr('y1', _top + i * rowHeight)
        .attr('y2', _top + i * rowHeight)
        .style('stroke', 'black');
      svg.append('rect')
        .attr('x', left - labelWidth)
        .attr('y', _top + i * rowHeight)
        .attr('width', labelWidth)
        .attr('height', rowHeight)
        .style('fill', data[i].color)
        .style('stroke', 'black')
      svg.append('text')
        .text(data[i].label)
        .attr('x', left - labelWidth / 2)
        .attr('y', _top + (i + 0.5) * rowHeight)
        .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .style('fill', 'black');
        
      const items = data[i].items;  
      for (j = 0; j < items.length; j++) {
        const x = left + xScale(items[j].start);
        const width = xScale(items[j].end) - xScale(items[j].start);
      svg.append('rect')
        .attr('x', x)
        .attr('y', _top + i * rowHeight)
        .attr('width', width)
        .attr('height', rowHeight)
        .style('fill', data[i].color)
        .style('stroke', 'black')
        
      svg.append('text')
        .text(items[j].label)
        .attr('x', x + width / 2)
        .attr('y', _top + (i + 0.5) * rowHeight)
        .attr('text-anchor', 'middle')
        .attr('alignment-baseline', 'middle')
        .style('fill', 'black');
        
      }  
    }    
      
    text {
      font-family: 'Ubuntu';
      font-size: 12px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <svg width="400" height="200">
    </svg>