Felix
Felix

Reputation: 201

d3.js: grid lines displayed out of the charts

I added some gridlines to my charts. Now my problem is that the grid lines are not aligned to the chart.

Also there is a last grid line added on the end of the chart on the right side is there a possibility to prevent that and only add a line to each label on the x axis?

I tried to add the gird lines to the clip path somehow but it didn't work.

My grid lines are getting created this way:

var signalData = {
  signal1: {
    name: "signal1",
    data: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 1, 2, 3, 5, 4, 1, 4, 2, 9, 7, 5, 7, 4, 6],
  },
  signal2: {
    name: "signal2",
    data: [6, 4, 8, 5, 4, 8, 4, 3, 5, 4, 5, 8, 7, 2, 9, 5, 4, 1, 2, 6, 0, 5, 7, 1],
  },
  signal3: {
    name: "signal3",
    data: [9, 5, 12, 3, 8, 4, 8, 6, 3, 4, 7, 8, 5, 2, 1, 8, 6, 8, 5, 8, 4, 8, 5, 1],
  },
}


var margin = {
  top: 10,
  right: 50,
  bottom: 40,
  left: 50,
};
var width = window.innerWidth - margin.left - margin.right;
var height = 230 - margin.top - margin.bottom;
var minimapHeight = 150 - margin.top - margin.bottom;

var xScale = d3
  .scaleLinear()
  .domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]) // input
  .range([0, width]); // output

var brushXScale = d3
  .scaleLinear()
  .domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]) // input
  .range([0, width]); // output

var zoomBrush = d3
  .brushX()
  .extent([
    [0, 0],
    [width, minimapHeight],
  ])
  .on("brush", zoomBrushed)
  .on("end", function(event) {
    if (event.selection == null) {
      resetZoom();
    }
  });

var selectBrush = d3
  .brushX()
  .extent([
    [0, 0],
    [width, height],
  ])
  .on("brush", selectBrushed)
  .on("end", function(event) {
    if (event.selection == null) {
      resetSelection();
    }
  });

function resetZoom() {
  brushXScale.domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]); // input

  for (var signal in signalData) {
    updateChart(signalData[signal].data, signalData[signal].name);
  }
}

function zoomBrushed() {
  var selectionPx = d3.brushSelection(this); // === [lower, upper] in pixels

  // transform from pixels to x-values
  var selectionX = [
    xScale.invert(selectionPx[0]),
    xScale.invert(selectionPx[1]),
  ];

  // set x scale domain, then redraw the lines
  brushXScale.domain(selectionX);

  for (var signal in signalData) {
    updateChart(signalData[signal].data, signalData[signal].name);
  }
}

function resetSelection() {
  selectBrush.on("end", null);
  d3.selectAll("div[id^=signal] svg .brushcontainer").call(
    selectBrush.clear
  );
  selectBrush.on("end", function(event) {
    if (event.selection == null) {
      resetSelection();
    }
  });
}

function selectBrushed() {
  var selectionPx = d3.brushSelection(this); // === [lower, upper] in pixels

  selectBrush.on("brush", null);
  d3.selectAll("div[id^=signal] svg .brushcontainer").call(
    selectBrush.move,
    selectionPx
  );
  selectBrush.on("brush", selectBrushed);
}

//Generate the brush focus chart
generateMinimap(signalData.signal1.data);
//Generate charts dynamically as often as i have signals
for (var signal in this.signalData) {
  generateChart(signalData[signal].data, signalData[signal].name);
}

// This function is for the one time preparations
function generateChart(data, name) {
  var svg = d3
    .select("#" + name)
    .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 + ")"
    );

  //Gridlines
  function Xgridlines() {
    return d3.axisBottom(xScale).ticks(10);
  }

  svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", "translate(0," + height + ")")
    .style("stroke-dasharray", "3,3")
    .call(Xgridlines().tickSize(-height).tickFormat(""));

  //clipPath to prevent path from overflow
  svg
    .append("defs")
    .append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

  svg.append("g").attr("class", "brushcontainer").call(selectBrush);

  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")");

  svg.append("g").attr("class", "y axis");

  svg
    .append("path")
    .attr("clip-path", "url(#clip)")
    .attr("class", "line") // Assign a class for styling
    .attr("fill", "none")
    .attr("stroke", "blue");

  updateChart(data, name);
}

// This function needs to be called to update the already prepared chart
function updateChart(data, name) {
  var svg = d3.select("#" + name + " svg");

  var yScale = d3
    .scaleLinear()
    .domain([0, d3.max(data)]) // input
    .range([height, 0]); // output

  function Ygridlines() {
    return d3.axisLeft(yScale).ticks(5);
  }

  svg
    .append("g")
    .attr("class", "grid")
    .attr("transform", "translate(0," - height - ")")
    .style("stroke-dasharray", "3,3")
    .call(
      Ygridlines()
      .tickSize(-width - margin.left)
      .tickFormat("")
    );

  var line = d3
    .line()
    .x((d, i) => brushXScale(i))
    .y((d) => yScale(d));

  svg.select(".x.axis").call(d3.axisBottom(brushXScale));

  svg.select(".y.axis").call(d3.axisLeft(yScale));

  svg
    .select(".line")
    .datum(data) // 10. Binds data to the line
    .attr("d", line); // 11. Calls the line generator
}

function generateMinimap(data) {
  var yScale = d3
    .scaleLinear()
    .domain([0, d3.max(data)]) // input
    .range([minimapHeight, 0]); // output

  var line = d3
    .line()
    .x((d, i) => xScale(i))
    .y((d) => yScale(d));

  var svg = d3
    .select("#minimap")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", minimapHeight + margin.top + margin.bottom)
    .append("g")
    .attr(
      "transform",
      "translate(" + margin.left + "," + margin.top + ")"
    );

  svg.append("g").call(zoomBrush);

  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + minimapHeight + ")")
    .call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom

  svg
    .append("path")
    .datum(data) // 10. Binds data to the line
    .attr("class", "line") // Assign a class for styling
    .attr("d", line) // 11. Calls the line generator
    .attr("fill-opacity", "0.17")
    .attr("fill", "blue")
    .attr("stroke", "blue");
}
<div id="minimap"></div>
<!-- In the original project these divs are not 
    static and get generated with v-for as many times as 
    i have a signal in signalData -->
<div id="signal1"></div>
<div id="signal2"></div>
<div id="signal3"></div>


<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>

Upvotes: 0

Views: 213

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

  1. Don't use .append() in updateChart. You never remove the axis you draw, and just draw new ones over them...
  2. Just turn your existing axes into ones with gridlines, using tickSizeInner():

var signalData = {
  signal1: {
    name: "signal1",
    data: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 1, 2, 3, 5, 4, 1, 4, 2, 9, 7, 5, 7, 4, 6],
  },
  signal2: {
    name: "signal2",
    data: [6, 4, 8, 5, 4, 8, 4, 3, 5, 4, 5, 8, 7, 2, 9, 5, 4, 1, 2, 6, 0, 5, 7, 1],
  },
  signal3: {
    name: "signal3",
    data: [9, 5, 12, 3, 8, 4, 8, 6, 3, 4, 7, 8, 5, 2, 1, 8, 6, 8, 5, 8, 4, 8, 5, 1],
  },
}


var margin = {
  top: 10,
  right: 50,
  bottom: 40,
  left: 50,
};
var width = window.innerWidth - margin.left - margin.right;
var height = 230 - margin.top - margin.bottom;
var minimapHeight = 150 - margin.top - margin.bottom;

var xScale = d3
  .scaleLinear()
  .domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]) // input
  .range([0, width]); // output

var brushXScale = d3
  .scaleLinear()
  .domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]) // input
  .range([0, width]); // output

var zoomBrush = d3
  .brushX()
  .extent([
    [0, 0],
    [width, minimapHeight],
  ])
  .on("brush", zoomBrushed)
  .on("end", function(event) {
    if (event.selection == null) {
      resetZoom();
    }
  });

var selectBrush = d3
  .brushX()
  .extent([
    [0, 0],
    [width, height],
  ])
  .on("brush", selectBrushed)
  .on("end", function(event) {
    if (event.selection == null) {
      resetSelection();
    }
  });

function resetZoom() {
  brushXScale.domain([
    0,
    d3.max(Object.keys(signalData), (d) => signalData[d].data.length),
  ]); // input

  for (var signal in signalData) {
    updateChart(signalData[signal].data, signalData[signal].name);
  }
}

function zoomBrushed() {
  var selectionPx = d3.brushSelection(this); // === [lower, upper] in pixels

  // transform from pixels to x-values
  var selectionX = [
    xScale.invert(selectionPx[0]),
    xScale.invert(selectionPx[1]),
  ];

  // set x scale domain, then redraw the lines
  brushXScale.domain(selectionX);

  for (var signal in signalData) {
    updateChart(signalData[signal].data, signalData[signal].name);
  }
}

function resetSelection() {
  selectBrush.on("end", null);
  d3.selectAll("div[id^=signal] svg .brushcontainer").call(
    selectBrush.clear
  );
  selectBrush.on("end", function(event) {
    if (event.selection == null) {
      resetSelection();
    }
  });
}

function selectBrushed() {
  var selectionPx = d3.brushSelection(this); // === [lower, upper] in pixels

  selectBrush.on("brush", null);
  d3.selectAll("div[id^=signal] svg .brushcontainer").call(
    selectBrush.move,
    selectionPx
  );
  selectBrush.on("brush", selectBrushed);
}

//Generate the brush focus chart
generateMinimap(signalData.signal1.data);
//Generate charts dynamically as often as i have signals
for (var signal in this.signalData) {
  generateChart(signalData[signal].data, signalData[signal].name);
}

// This function is for the one time preparations
function generateChart(data, name) {
  var svg = d3
    .select("#" + name)
    .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 + ")"
    );

  //clipPath to prevent path from overflow
  svg
    .append("defs")
    .append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

  svg.append("g").attr("class", "brushcontainer").call(selectBrush);

  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")");

  svg.append("g").attr("class", "y axis");

  svg
    .append("path")
    .attr("clip-path", "url(#clip)")
    .attr("class", "line") // Assign a class for styling
    .attr("fill", "none")
    .attr("stroke", "blue");

  updateChart(data, name);
}

// This function needs to be called to update the already prepared chart
function updateChart(data, name) {
  var svg = d3.select("#" + name + " svg");

  var yScale = d3
    .scaleLinear()
    .domain([0, d3.max(data)]) // input
    .range([height, 0]); // output

  var line = d3
    .line()
    .x((d, i) => brushXScale(i))
    .y((d) => yScale(d));

  svg.select(".x.axis").call(
    d3.axisBottom(brushXScale)
      .tickSizeOuter(0)
      .tickSizeInner(-height)
  );

  svg.select(".y.axis").call(
    d3.axisLeft(yScale)
      .tickSizeInner(-width)
      .tickSizeOuter(0)
      .ticks(5)
  );

  svg
    .select(".line")
    .datum(data) // 10. Binds data to the line
    .attr("d", line); // 11. Calls the line generator
}

function generateMinimap(data) {
  var yScale = d3
    .scaleLinear()
    .domain([0, d3.max(data)]) // input
    .range([minimapHeight, 0]); // output

  var line = d3
    .line()
    .x((d, i) => xScale(i))
    .y((d) => yScale(d));

  var svg = d3
    .select("#minimap")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", minimapHeight + margin.top + margin.bottom)
    .append("g")
    .attr(
      "transform",
      "translate(" + margin.left + "," + margin.top + ")"
    );

  svg.append("g").call(zoomBrush);

  svg
    .append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + minimapHeight + ")")
    .call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom

  svg
    .append("path")
    .datum(data) // 10. Binds data to the line
    .attr("class", "line") // Assign a class for styling
    .attr("d", line) // 11. Calls the line generator
    .attr("fill-opacity", "0.17")
    .attr("fill", "blue")
    .attr("stroke", "blue");
}
.tick line {
  stroke-dasharray: 3 3;
}
<div id="minimap"></div>
<!-- In the original project these divs are not 
    static and get generated with v-for as many times as 
    i have a signal in signalData -->
<div id="signal1"></div>
<div id="signal2"></div>
<div id="signal3"></div>


<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>

Upvotes: 2

Related Questions