Search code examples
javascriptobservable-plot

How to create standalone scale that is aware of Observable plot's width, margins, etc


How can I create standalone scale that is aware of Observable plot's width, margins, etc?

  • I need to create a standalone scale in order to use its scale.apply() method.
  • However, the resulting scale's apply() function returns values in the range [0,1], not in terms of the plot's dimensions (width, margin, etc).
  • Compare to plot.scale, which returns values in terms of the plot's dimensions.
  • I cannot use plot.scale because the scale.apply() function is needed to define the marks that are part of the options when creating the plot. (Sort of a chicken-and-egg problem.)
  • I tried adding plot dimension options when creating a scale, but these are ignored.
  • Ideally, the scale could be accessed from the callback where it is used, but this does not seem possible.
  • Currently, my work-around is to create the plot twice: first to create plot.scale, then again to define the plot that uses plot.scale
  • Interestingly, if the (Svelte) component is used multiple times, only the first component needs the work-around above. So a "throw-away" component could be hidden.
  • Another work-around may be to first generate a simple plot for the scale, then use that for the "real" plot.

I have created a simple sample demonstrating the problem below:

const data = [{
    text: 'Weekend',
    utc: new Date('2024-06-16'), end: new Date('2024-06-17'),
  },{
    text: 'Weekdays',
    utc: new Date('2024-06-17'), end: new Date('2024-06-22'),
  }, {
    text: 'Weekend',
    utc: new Date('2024-06-22'), end: new Date('2024-06-23'),
  }]
  
  // Construct scale:
  const scale = Plot.scale({x: {domain: [new Date('2024-06-16'), new Date('2024-06-23')]}})
  
  // Simple plot:
  const plot = Plot.plot({
    height: 150,
    width: 800,
    marginRight: 12,
    marginLeft: 12,
    marks: [
      Plot.rectY(data, {
        x1: (d) => d.utc,
        x2: (d) => d.end,
        y: 1,
        fill: (d) => d.text == 'Weekend' ? 'lightgray': 'beige'
      }),
      Plot.text(data, {
        x: (d) => (Number(d.utc) + Number(d.end))/2,
        text: 'text'
      }),
    ],
  });
  
  function compareScales(value) {
    console.log({
      value,
      '     scale': scale.apply(value),
      'plot.scale': plot.scale('x').apply(value)
    })
  }
  
  compareScales(new Date('2024-06-16')) 
  compareScales(new Date('2024-06-17')) 
  compareScales(new Date('2024-06-23')) 
  

  const div = document.querySelector('#myplot');
  div.append(plot);
#myplot {
  height: 200px;
}
<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>

<div id="myplot"></div>


Solution

  • When you create your "standalone" scale just give it the same range as your plot.

    const width = 800,
          marginLeft = 12,
          marginRight = 12;
    
    // construct scale
    const scale = Plot.scale({
      x: { 
        domain: [ new Date('2024-06-16'), new Date('2024-06-23') ],
        range: [ marginLeft, width - marginRight ] //<-- range matching plot
      },
    });
    

    Running example:

    <!DOCTYPE html>
    
    <html>
    
    <head>
      <style>
        #myplot {
          height: 200px;
        }
      </style>
    </head>
    
    <body>
      <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
      <script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
    
      <div id="myplot"></div>
    
      <script>
        const data = [{
            text: 'Weekend',
            utc: new Date('2024-06-16'),
            end: new Date('2024-06-17'),
          },
          {
            text: 'Weekdays',
            utc: new Date('2024-06-17'),
            end: new Date('2024-06-22'),
          },
          {
            text: 'Weekend',
            utc: new Date('2024-06-22'),
            end: new Date('2024-06-23'),
          },
        ];
    
        const width = 800,
          marginLeft = 12,
          marginRight = 12;
    
        // Construct scale:
        const scale = Plot.scale({
          x: {
            domain: [new Date('2024-06-16'), new Date('2024-06-23')],
            range: [marginLeft, width - marginRight]
          },
        });
    
        // Simple plot:
        const plot = Plot.plot({
          height: 150,
          width: width,
          marginRight: marginRight,
          marginLeft: marginLeft,
          marks: [
            Plot.rectY(data, {
              x1: (d) => d.utc,
              x2: (d) => d.end,
              y: 1,
              fill: (d) => (d.text == 'Weekend' ? 'lightgray' : 'beige'),
            }),
            Plot.text(data, {
              x: (d) => (Number(d.utc) + Number(d.end)) / 2,
              text: 'text',
            }),
          ],
        });
    
        function compareScales(value) {
          console.log({
            value,
            '     scale': scale.apply(value),
            'plot.scale': plot.scale('x').apply(value)
          })
        }
    
        compareScales(new Date('2024-06-16'))
        compareScales(new Date('2024-06-17'))
        compareScales(new Date('2024-06-23'))
    
        const div = document.querySelector('#myplot');
        div.append(plot);
      </script>
    </body>
    
    </html>