title: D3.js v7 - How to make Y axis labels always show on screen for a scrollable chart
body: <p>I have a chart that uses set widths for each x axis item, and because of this the chart is scrollable when lots of dates get added.</p>
  <p>What I want to do is have both Y axis labels always be on screen (I can't do <code>position: fixed</code> with CSS because the axis's are <code>&lt;g&gt;</code> elements.)</p>
  <p>How can I have both Y axis labels always be on screen? (this is the blue and green text labels). Here's an example of what I mean: <a href="https://observablehq.com/@d3/pannable-chart" rel="nofollow noreferrer">https://observablehq.com/@d3/pannable-chart</a>) - unfortunately the code on that site codes over my head.</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>let test = (async () =&gt; {
  
    // data source
    const data = JSON.parse(`{
        "2021-11-17":{
           "rawWeight":220,
           "rca":1821
        },
        "2021-05-17":{
           "rawWeight":230,
           "rca":1600
        },
        "2021-03-09":{
           "rawWeight":224,
           "rca":1800
        },
        "2020-10-30":{
           "rawWeight":234.36,
           "rca":2851
        },
        "2020-10-13":{
           "rawWeight":225.54,
           "rca":2541
        },
        "2020-09-25":{
           "rawWeight":225.4,
           "rca":2588
        },
        "2020-1-10":{
           "rawWeight":244,
           "rca":1800
        }
    }`)
  
    // parse the date / time
    var parseTime = d3.timeParse("%Y-%m-%d");
  
    //structure dataset
    let dataset = []
    for (let day in data) {
      dataset.push({
        day: day,
        date: parseTime(day),
        weight: Number(data[day].rawWeight),
        calories: data[day].rca
      })
    }
  
    let margin = {
      top: 10,
      right: 20,
      bottom: 0,
      left: 20
    }
    //let width = document.querySelector('.pane[data-area="weight"] .chart').clientWidth - margin.left - margin.right
    let width = (dataset.length * 230) - margin.left - margin.right
    let height = 150 - margin.top - margin.bottom
  
    // set the ranges
    let x = d3.scaleTime().range([0, width])
    let y0 = d3.scaleLinear().range([height, 0])
    let y1 = d3.scaleLinear().range([height, 0])
  
    let linecalories = d3.line()
      .curve(d3.curveCatmullRom)
      .x(d =&gt; x(d.date))
      .y(d =&gt; y0(d.calories))
  
    let areacalories = d3.area()
      .curve(d3.curveCatmullRom)
      .x(d =&gt; x(d.date))
      .y0(height)
      .y1(d =&gt; y0(d.calories))
  
    let lineweight = d3.line()
      .curve(d3.curveCatmullRom)
      .x(d =&gt; x(d.date))
      .y(d =&gt; y1(d.weight))
  
    let areaweight = d3.area()
      .curve(d3.curveCatmullRom)
      .x(d =&gt; x(d.date))
      .y0(height)
      .y1(d =&gt; y1(d.weight))
  
    let svg = d3
      .select('.pane[data-area="weight"] .chart')
      .append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
  
    // Scale the range of the data
    x.domain(d3.extent(dataset, d =&gt; d.date))
    y0.domain([0, d3.max(dataset, (d) =&gt; {
      // let rounded = Math.floor( Math.max(d.calories) / 500) * 500
      // return rounded + 1000
      return Math.max(d.calories) + 500
    })])
    y1.domain([
      // replace this with "0" to show scale from 0
      d3.min(dataset, d =&gt; Math.min(d.weight) - 25),
      d3.max(dataset, d =&gt; Math.max(d.weight) + 25)
    ])
  
  
  
  
  
  
  
    // gridlines in y axis function
    function make_y_gridlines() {
      return d3.axisLeft(y1)
        .ticks(8)
    }
    svg.append("g")
      .attr("class", "grid-y")
      .call(make_y_gridlines()
        .tickSize(-width)
        .ticks(5)
      )
      .call(g =&gt; g.select(".domain").remove())
      .call(g =&gt; g.selectAll("text").remove())
  
    // gridlines in x axis function
    function make_x_gridlines() {
      return d3.axisBottom(x)
        .ticks(20)
    }
    svg.append("g")
      .attr("class", "grid-x")
      .attr("transform", "translate(0," + height + ")")
      .call(make_x_gridlines()
        .tickSize(-height)
      )
      .call(g =&gt; g.select(".domain").remove())
      .call(g =&gt; g.selectAll("text").remove())
  
  
  
  
  
  
  
  
  
  
  
  
  
    // apply weight area
    svg.append("path")
      .data([dataset])
      .attr("class", "area-weight")
      .attr("d", areaweight)
  
    // apply calories area
    svg.append("path")
      .data([dataset])
      .attr("class", "area-calories")
      .attr("d", areacalories)
  
    // apply weight line
    svg.append("path")
      .data([dataset])
      .attr("class", "line-weight")
      .attr("d", lineweight)
  
    // apply calories line
    svg.append("path")
      .data([dataset])
      .attr("class", "line-calories")
      .attr("d", linecalories)
  
  
    svg.append("g")
      .attr("class", "axis-dates")
      .attr("transform", "translate(0," + (height + 8) + ")")
      .call(
        d3
        .axisBottom(x)
        .tickSize(0)
        .ticks(d3.utcMonth.every(1))
        .tickSizeOuter(0)
        .tickFormat(d3.timeFormat("%b %Y"))
        .tickPadding(-30)
      )
      .call(g =&gt; g.select(".domain").remove())
  
  
    // Add the Y0 Axis
    svg.append("g")
      .attr("class", "axis-calories")
      .attr("transform", "translate( " + width + ", 0 )")
      .call(
        d3
        .axisRight()
        .scale(y0)
        .tickSize(0)
        .ticks(height / 30)
        .tickFormat(d =&gt; {
          if ((d / 1000) &gt;= 1)
            d = d / 1000 + "K";
          return d
        })
  
      )
      .call(g =&gt; g.select(".domain").remove())
  
    // Add the Y1 Axis
    svg.append("g")
      .attr("class", "axis-weight")
      // .attr("transform", "translate( " + width + ", 0 )")
      .call(
        d3
        .axisLeft()
        .scale(y1)
        .tickSize(0)
        .ticks(height / 30)
      )
      .call(g =&gt; g.select(".domain").remove())
  
  
  
    // dates
    // svg.append("g")
    //    .attr("class", "axis-dates")
    //    .attr("transform", "translate(0," + (height + 8) + ")")
    //    .call(
    //        d3
    //            .axisBottom(x)
    //            .ticks(0)
    //            .tickValues(x.domain())
    //            .tickFormat(d3.timeFormat("%b %Y"))
    //            .tickPadding(-30)
    //    )
    //   .call(g =&gt; g.select(".domain").remove())
    //   .call(g =&gt; g.selectAll("line").remove())
    //   .call(g =&gt; g.select(".tick:first-of-type text").attr('transform', 'translate(32,0)'))
    //   .call(g =&gt; g.select(".tick:last-of-type text").attr('transform', 'translate(-32,0)'))
  
    // remove 0 label
    svg.selectAll(".tick text")
      .filter(function(d) {
        return d === 0
      })
      .remove()
  
  
  
  
  
  
    let colors = ['#56ab2f', '#a8e063']
    let grad = svg.append('defs')
      .append('linearGradient')
      .attr('id', 'calorieline')
      .attr('x1', '0%')
      .attr('x2', '100%')
      .attr('y1', '0%')
      .attr('y2', '100%');
  
    grad.selectAll('stop')
      .data(colors)
      .enter()
      .append('stop')
      .style('stop-color', function(d) {
        return d;
      })
      .attr('offset', function(d, i) {
        return 100 * (i / (colors.length - 1)) + '%';
      })
  
    colors = ['rgba(86, 171, 47, .15)', 'transparent']
    grad = svg.append('defs')
      .append('linearGradient')
      .attr('id', 'greenfade')
      .attr('x1', '0%')
      .attr('x2', '0%')
      .attr('y1', '0%')
      .attr('y2', '100%');
  
    grad.selectAll('stop')
      .data(colors)
      .enter()
      .append('stop')
      .style('stop-color', function(d) {
        return d;
      })
      .attr('offset', function(d, i) {
        return 100 * (i / (colors.length - 1)) + '%';
      })
  
    colors = ['rgba(32, 120, 227, .15)', 'transparent']
    grad = svg.append('defs')
      .append('linearGradient')
      .attr('id', 'bluefade')
      .attr('x1', '0%')
      .attr('x2', '0%')
      .attr('y1', '0%')
      .attr('y2', '100%');
  
    grad.selectAll('stop')
      .data(colors)
      .enter()
      .append('stop')
      .style('stop-color', function(d) {
        return d;
      })
      .attr('offset', function(d, i) {
        return 100 * (i / (colors.length - 1)) + '%';
      })
  
    colors = ['#2894f2', '#1d6cdc']
    grad = svg.append('defs')
      .append('linearGradient')
      .attr('id', 'goodbar')
      .attr('x1', '0%')
      .attr('x2', '25%')
      .attr('y1', '0%')
      .attr('y2', '100%');
  
    grad.selectAll('stop')
      .data(colors)
      .enter()
      .append('stop')
      .style('stop-color', function(d) {
        return d;
      })
      .attr('offset', function(d, i) {
        return 100 * (i / (colors.length - 1)) + '%';
      })
  
  
  });
  
  test();</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>body {
    background: #1e2546;
    width: 500px;
    display: block;
  }
  
  .chart .axis-dates text {
    font-size: .7rem;
    fill: #fff;
  
  }
  
  [data-area=weight] .chart .axis-dates .tick {
    margin-left: -100px;
    left: 10rem;
    position: relative
  }
  
  [data-area=weight] .chart .line-calories {
    stroke-linecap: round;
    stroke-width: .2rem;
    stroke: url(#calorieline);
    fill: none
  }
  
  [data-area=weight] .chart .area-calories {
    fill: url(#greenfade);
  }
  
  [data-area=weight] .chart .area-weight {
    fill: url(#bluefade)
  }
  [data-area=weight] .chart .axis-calories text {
    fill: url(#calorieline);
    font-size: .6rem;
    font-weight: 500
  }
  
  [data-area=weight] .chart .line-weight {
    stroke-linecap: round;
    stroke-width: .2rem;
    stroke: url(#goodbar);
    fill: transparent
  }
  
  [data-area=weight] .chart .axis-weight text {
    font-size: .6rem;
    font-weight: 500;
    fill: url(#goodbar);
  
  }
  
  [data-area=weight] .chart .grid-x line,
  [data-area=weight] .chart .grid-y line {
    stroke: rgba(0, 0, 0, .1);
    stroke-width: .1rem
  }
  
  [data-area=weight] .chart .label-calorie {
    font-size: .7rem
  }
  
  [data-area=weight] .chart .legend {
    -webkit-column-count: 2;
    -moz-column-count: 2;
    column-count: 2;
    -webkit-column-gap: .5rem;
    -moz-column-gap: .5rem;
    column-gap: .5rem;
    margin: .3rem auto 0 auto;
    width: 9rem;
    font-size: .9rem;
    text-align: center;
    position: relative
  }
  
  [data-area=weight] .chart .legend .i {
    display: block;
    vertical-align: top;
    width: 100%
  }
  
  [data-area=weight] .chart .legend .i:before {
    content: '';
    display: inline-block;
    width: .6rem;
    height: .3rem;
    border-radius: 5rem;
    margin-right: .3rem;
    vertical-align: .1rem
  }
  
  [data-area=weight] .chart .legend .calorie {
    color: #a7e063
  }
  
  [data-area=weight] .chart .legend .calorie:before {
    background: #a7e063
  }
  
  [data-area=weight] .chart .legend .weight {
    color: #2482e9
  }
  
  [data-area=weight] .chart .legend .weight:before {
    background: #2482e9
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;!DOCTYPE html&gt;
  &lt;html&gt;
  
    &lt;head&gt;
      &lt;meta charset="utf-8"&gt;
      &lt;meta name="viewport" content="width=device-width"&gt;
      &lt;title&gt;JS Bin&lt;/title&gt;
      &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.4/d3.min.js" integrity="sha512-T+1zstV6Llwh/zH+uoc1rJ7Y8tf9N+DiC0T3aL0+0blupn5NkBT52Avsa0l+XBnftn/14EtxpsztAWsmiAaqfQ==" crossorigin="anonymous" referrerpolicy="no-referrer"&gt;&lt;/script&gt;
    &lt;/head&gt;
  
    &lt;body&gt;
      &lt;div class="pane" data-area="weight"&gt;
        &lt;div class="chart"&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/body&gt;
  
  &lt;/html&gt;</code></pre>
  </div>
  </div>
  </p>
  <p>Here's a JSFiddle: <a href="https://jsfiddle.net/8h5fxn46/" rel="nofollow noreferrer">https://jsfiddle.net/8h5fxn46/</a></p>