Search code examples
javascriptd3.jssvelte

how to create d3.js bar chart as a Svelte component and bind d3 to SVG properly?


I have a focus/context bar chart that I created in d3. I want to put it in Svelte so that I can use it as a component. But I'm getting stuck figuring out how to bind the elements created in d3 to the html in the svelte component.

Below is the code for the d3.js chart

const data = [{
    "date": "2010-08-06",
    "count": 32348
  },
  {
    "date": "2010-08-07",
    "count": 32454
  },
  {
    "date": "2010-08-08",
    "count": 32648
  },
  {
    "date": "2010-08-09",
    "count": 32812
  },
  {
    "date": "2010-08-10",
    "count": 32764
  },
  {
    "date": "2010-08-11",
    "count": 32668
  },
  {
    "date": "2010-08-12",
    "count": 32484
  },
  {
    "date": "2010-08-13",
    "count": 32167
  },
  {
    "date": "2010-08-14",
    "count": 32304
  },
  {
    "date": "2010-08-15",
    "count": 32446
  },
  {
    "date": "2010-08-16",
    "count": 32670
  },
  {
    "date": "2010-08-17",
    "count": 32778
  },
  {
    "date": "2010-08-18",
    "count": 32756
  },
  {
    "date": "2010-08-19",
    "count": 32580
  }
]

const formatDate4 = d3.timeFormat("%m%d%Y")
const formatDate5 = d3.timeFormat('%Y-%m-%d')
const formatDate6 = d3.timeFormat("%b %d, %Y");

const bisectDate = d3.bisector(function(d) {
  return d.date;
}).left;

var parseTime = d3.timeParse("%Y-%m-%d")

const pageWidth = window.innerWidth

let wrapWidth = 969,
  mapRatio = .51

let margin = {
  top: 0,
  right: 0,
  bottom: 10,
  left: 0
}

let timeW = 960,
  timeH = 450

let timeMargin = {
    top: 20,
    right: 250,
    bottom: 80,
    left: 60
  },
  timeMargin2 = {
    top: 410,
    right: 250,
    bottom: 30,
    left: 60
  },
  timeWidth = timeW - timeMargin.left - timeMargin.right,
  timeHeight = timeH - timeMargin.top - timeMargin.bottom,
  timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;

let timeseries = d3.select("#timeseries-container").append('svg')
  .attr('id', 'timeseries')
  .attr("width", timeWidth + timeMargin.left + timeMargin.right)
  .attr("height", timeHeight + timeMargin.top + timeMargin.bottom)

var graph = timeseries.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

var parseDate = d3.timeParse("%Y-%m-%d");

var x2 = d3.scaleTime().range([0, timeWidth]),
  x3 = d3.scaleTime().range([0, timeWidth]),
  y2 = d3.scaleLinear().range([timeHeight, 0]),
  y3 = d3.scaleLinear().range([timeHeight2, 0]);

var xAxis2 = d3.axisBottom(x2).ticks(5)
  .tickFormat(d3.timeFormat("%b %d %Y")),
  yAxis2 = d3.axisLeft(y2).ticks(3);

timeseries.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("width", timeWidth)
  .attr("height", timeHeight)

var chartfocus = timeseries.append("g")
  .attr("class", "chartfocus")
  .attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");

const natlData = data;

console.log('natlData', natlData)
updateChart(natlData)

function updateChart(data) {

  const dataOld = data.map(a => ({ ...a
  }));

  data.forEach(d => {
    d.date = parseTime(d.date);
  })

  x2.domain(d3.extent(data, function(d) {
    return d.date;
  }));

  const charttooltip = d3.select("#time").append("div")
    .attr("class", "charttooltip");

  const chartguideline = d3.select("body").append("div")
    .attr("class", "chartguideline");

  let topY = d3.max(data, d => d.count)

  timeseries.selectAll(".axis").remove();

  let bisectDate = d3.bisector(function(d) {
    return d.date;
  }).left;

  chartfocus
    .on("mouseover", mouseover)
    .on("mousemove", mousemove)
    .on("mouseout", mouseout)

  y2.domain([0, topY]).nice();
  d3.select(".axis--y")
    .transition(1000)
    .call(yAxis2)

  d3.selectAll('.bar').remove()

  let dailyBars = chartfocus.selectAll("bar")
    .data(data)
    .enter().append("rect")
    .attr("clip-path", "url(#clip)")
    .attr('class', d => 'bar d' + formatDate4(d.date))

  y2.domain([0, topY]).nice();

  d3.select(".axis--y")
    .transition(1000)
    .call(yAxis2)

  dailyBars.transition()
    .attr("width", d => x2(d3.timeDay.offset(d.date)) - x2(d.date))
    .attr('x', d => x2(d.date))
    .attr("y", d => y2(d.count))
    .attr("height", d => timeHeight - y2(d.count))

  chartfocus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + timeHeight + ")")
    .call(xAxis2);

  chartfocus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis2);

  chartfocus.append("rect")
    .attr("class", "chartzoom")
    .attr("width", timeWidth)
    .attr("height", timeHeight)

  d3.selectAll('.focus').remove()

  function mouseover(event) {}

  function mousemove(event, d) {

    let outerMargins = pageWidth - wrapWidth
    let outerLeftMargin = outerMargins / 2

    let ex = event.x - timeMargin.left - outerLeftMargin

    var x0 = x2.invert(ex),
      i = bisectDate(data, x0, 1),
      d0 = data[i - 1],
      d1 = data[i],
      d = x0 - d0.date > d1.date - x0 ? d1 : d0;

    let dateX = d.date

    let dateXSimple = formatDate5(dateX)

    indvData = dataOld.filter(d => d.date == dateXSimple)

    dailyCount = []
    dailyCounts = []

    let tooltip_str = formatDate5(dateX) + "</p>Count: <strong>" + indvData[0].count

    charttooltip
      .style('left', event.x - outerLeftMargin + 'px')
      .style("top", 0)
      .attr('x', 0)
      .html(d => tooltip_str)

    chartguideline
      .style('left', event.x - 1 + 'px')
      .style("top", timeMargin.top + 'px')
      .attr("width", d => x2(d3.timeDay.offset(dateX)) - x2(dateX))
      .style('height', timeHeight + timeMargin.top - timeMargin.bottom + 45 + 'px')
      .attr('x', 0)

    charttooltip
      .style("visibility", "visible")

    chartguideline
      .style("visibility", "visible")

  }

  function mouseout() {

    d3.selectAll('.bar').style('fill', '#aaa')

    d3.selectAll('.bar').transition()
      .attr("width", d => x2(d3.timeDay.offset(d.date)) - x2(d.date))

    charttooltip.transition()
      .duration(0)
      .style("visibility", "hidden")

    chartguideline.transition()
      .duration(0)
      .style("visibility", "hidden")
  }

};
.chartzoom {
  cursor: move;
  fill: none;
  pointer-events: all;
}

.charttooltip {
  position: absolute;
  pointer-events: none;
  padding: 10px;
  background: #fff;
  visibility: hidden;
  opacity: .9;
  -moz-box-shadow: 0 0 15px #aaa;
  -webkit-box-shadow: 0 0 15px #aaa;
  box-shadow: 0 0 15px #aaa;
  margin-left: 10px;
}

.charttooltip:before {
  right: 100%;
  top: 50%;
  border: solid transparent;
  content: " ";
  height: 0;
  width: 0;
  position: absolute;
  pointer-events: none;
  border-color: rgba(255, 255, 255, 0);
  border-right-color: #ffffff;
  border-width: 10px;
  margin-top: -10px;
}

.chartguideline {
  position: absolute;
  width: 1px;
  opacity: .5;
  pointer-events: none;
  background: #ef4136;
  visibility: hidden;
}

.bar {
  fill: #aaa;
  stroke-width: 1px;
  stroke: #000;
}
<script src="https://d3js.org/d3.v7.min.js"></script>

<div id="time">
  <div id="timeseries-container"></div>
</div>

The code I'm using for the Svelte component is below. I'm trying to put the main SVG in the body, and then bind my d3 code to it, but I'm not able to get that working. Any help would be greatly appreciated.

I also made a REPL, if that's easier.

    <script>
      import * as d3 from "d3";
      const data = [
        {
          date: "2010-08-06",
          count: 32348,
        },
        {
          date: "2010-08-07",
          count: 32454,
        },
        {
          date: "2010-08-08",
          count: 32648,
        },
        {
          date: "2010-08-09",
          count: 32812,
        },
        {
          date: "2010-08-10",
          count: 32764,
        },
        {
          date: "2010-08-11",
          count: 32668,
        },
        {
          date: "2010-08-12",
          count: 32484,
        },
        {
          date: "2010-08-13",
          count: 32167,
        },
        {
          date: "2010-08-14",
          count: 32304,
        },
        {
          date: "2010-08-15",
          count: 32446,
        },
        {
          date: "2010-08-16",
          count: 32670,
        },
        {
          date: "2010-08-17",
          count: 32778,
        },
        {
          date: "2010-08-18",
          count: 32756,
        },
        {
          date: "2010-08-19",
          count: 32580,
        },
      ];

      let el;

      const formatDate4 = d3.timeFormat("%m%d%Y");
      const formatDate5 = d3.timeFormat("%Y-%m-%d");
      const formatDate6 = d3.timeFormat("%b %d, %Y");

      const bisectDate = d3.bisector(function (d) {
        return d.date;
      }).left;

      var parseTime = d3.timeParse("%Y-%m-%d");

      const pageWidth = window.innerWidth;

      let wrapWidth = 969,
        mapRatio = 0.51;

      let margin = { top: 0, right: 0, bottom: 10, left: 0 };

      let timeW = 960,
        timeH = 450;

      let timeMargin = { top: 20, right: 250, bottom: 80, left: 60 },
        timeMargin2 = { top: 410, right: 250, bottom: 30, left: 60 },
        timeWidth = timeW - timeMargin.left - timeMargin.right,
        timeHeight = timeH - timeMargin.top - timeMargin.bottom,
        timeHeight2 = timeH - timeMargin2.top - timeMargin2.bottom;

      let timeseries = d3
        .select(el)
        .attr("id", "timeseries")
        .attr("width", timeWidth + timeMargin.left + timeMargin.right)
        .attr("height", timeHeight + timeMargin.top + timeMargin.bottom);

      var graph = timeseries
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      var parseDate = d3.timeParse("%Y-%m-%d");

      var x2 = d3.scaleTime().range([0, timeWidth]),
        x3 = d3.scaleTime().range([0, timeWidth]),
        y2 = d3.scaleLinear().range([timeHeight, 0]),
        y3 = d3.scaleLinear().range([timeHeight2, 0]);

      var xAxis2 = d3.axisBottom(x2).ticks(5).tickFormat(d3.timeFormat("%b %d %Y")),
        yAxis2 = d3.axisLeft(y2).ticks(3);

      timeseries
        .append("defs")
        .append("clipPath")
        .attr("id", "clip")
        .append("rect")
        .attr("width", timeWidth)
        .attr("height", timeHeight);

      var chartfocus = timeseries
        .append("g")
        .attr("class", "chartfocus")
        .attr(
          "transform",
          "translate(" + timeMargin.left + "," + timeMargin.top + ")"
        );

      const natlData = data;

      console.log("natlData", natlData);
      updateChart(natlData);

      function updateChart(data) {
        const dataOld = data.map((a) => ({ ...a }));

        data.forEach((d) => {
          d.date = parseTime(d.date);
        });

        x2.domain(
          d3.extent(data, function (d) {
            return d.date;
          })
        );

        const charttooltip = d3
          .select("#time")
          .append("div")
          .attr("class", "charttooltip");

        const chartguideline = d3
          .select("body")
          .append("div")
          .attr("class", "chartguideline");

        let topY = d3.max(data, (d) => d.count);

        timeseries.selectAll(".axis").remove();

        let bisectDate = d3.bisector(function (d) {
          return d.date;
        }).left;

        chartfocus
          .on("mouseover", mouseover)
          .on("mousemove", mousemove)
          .on("mouseout", mouseout);

        y2.domain([0, topY]).nice();
        d3.select(".axis--y").transition(1000).call(yAxis2);

        d3.selectAll(".bar").remove();

        let dailyBars = chartfocus
          .selectAll("bar")
          .data(data)
          .enter()
          .append("rect")
          .attr("clip-path", "url(#clip)")
          .attr("class", (d) => "bar d" + formatDate4(d.date));

        y2.domain([0, topY]).nice();

        d3.select(".axis--y").transition(1000).call(yAxis2);

        dailyBars
          .transition()
          .attr("width", (d) => x2(d3.timeDay.offset(d.date)) - x2(d.date))
          .attr("x", (d) => x2(d.date))
          .attr("y", (d) => y2(d.count))
          .attr("height", (d) => timeHeight - y2(d.count));

        chartfocus
          .append("g")
          .attr("class", "axis axis--x")
          .attr("transform", "translate(0," + timeHeight + ")")
          .call(xAxis2);

        chartfocus.append("g").attr("class", "axis axis--y").call(yAxis2);

        chartfocus
          .append("rect")
          .attr("class", "chartzoom")
          .attr("width", timeWidth)
          .attr("height", timeHeight);

        d3.selectAll(".focus").remove();

        function mouseover(event) {}

        function mousemove(event, d) {
          let outerMargins = pageWidth - wrapWidth;
          let outerLeftMargin = outerMargins / 2;

          let ex = event.x - timeMargin.left - outerLeftMargin;

          var x0 = x2.invert(ex),
            i = bisectDate(data, x0, 1),
            d0 = data[i - 1],
            d1 = data[i],
            d = x0 - d0.date > d1.date - x0 ? d1 : d0;

          let dateX = d.date;

          let dateXSimple = formatDate5(dateX);

          indvData = dataOld.filter((d) => d.date == dateXSimple);

          dailyCount = [];
          dailyCounts = [];

          let tooltip_str =
            formatDate5(dateX) + "</p>Count: <strong>" + indvData[0].count;

          charttooltip
            .style("left", event.x - outerLeftMargin + "px")
            .style("top", 0)
            .attr("x", 0)
            .html((d) => tooltip_str);

          chartguideline
            .style("left", event.x - 1 + "px")
            .style("top", timeMargin.top + "px")
            .attr("width", (d) => x2(d3.timeDay.offset(dateX)) - x2(dateX))
            .style(
              "height",
              timeHeight + timeMargin.top - timeMargin.bottom + 45 + "px"
            )
            .attr("x", 0);

          charttooltip.style("visibility", "visible");

          chartguideline.style("visibility", "visible");
        }

        function mouseout() {
          d3.selectAll(".bar").style("fill", "#aaa");

          d3.selectAll(".bar")
            .transition()
            .attr("width", (d) => x2(d3.timeDay.offset(d.date)) - x2(d.date));

          charttooltip.transition().duration(0).style("visibility", "hidden");

          chartguideline.transition().duration(0).style("visibility", "hidden");
        }
      }
    </script>

    <div id="time">
      <div id="timeseries-container">
        <svg width="100%" viewBox="0 0 {timeW} {timeH}" id="chart" bind:this={el} />
      </div>
    </div>

    <style>
      .chartzoom {
        cursor: move;
        fill: none;
        pointer-events: all;
      }

      .charttooltip {
        position: absolute;
        pointer-events: none;
        padding: 10px;
        background: #fff;
        visibility: hidden;
        opacity: 0.9;
        -moz-box-shadow: 0 0 15px #aaa;
        -webkit-box-shadow: 0 0 15px #aaa;
        box-shadow: 0 0 15px #aaa;
        margin-left: 10px;
      }

      .charttooltip:before {
        right: 100%;
        top: 50%;
        border: solid transparent;
        content: " ";
        height: 0;
        width: 0;
        position: absolute;
        pointer-events: none;
        border-color: rgba(255, 255, 255, 0);
        border-right-color: #ffffff;
        border-width: 10px;
        margin-top: -10px;
      }

      .chartguideline {
        position: absolute;
        width: 1px;
        opacity: 0.5;
        pointer-events: none;
        background: #ef4136;
        visibility: hidden;
      }

      .bar {
        fill: #aaa;
        stroke-width: 1px;
        stroke: #000;
      }

Solution

  • You also just use an action. This does not require any imports, state variable declarations or this bindings:

    <script>
      // ...
      function initialize(svg) {
        d3.select(svg)...
      }
    </script>
    
    <svg use:initialize ...>
    

    (Also, by using d3 like this you are losing most of the advantages of Svelte. Unless any of its advanced algorithms are used, I would recommend just building the SVG directly in Svelte markup. That way it stays declarative and more readable.)