1

I am trying to enable legend toggling for my line graph and I'm getting an d3 error when trying to toggle "General Motors Truck Company"

Error: "Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '#line_General Motors Truck Company (GMC)' is not a valid selector."

I think it is because of the "()" character in the legend, not sure though. Anyone know how I can fix this?

Here is a jsfiddle: https://jsfiddle.net/jw8dskvu/

Also code snippet:

var data = [{
  "Brand": "Toyota",
  "Count": 1800,
  "Time": "2017-04-02 16"
}, {
  "Brand": "Toyota",
  "Count": 1172,
  "Time": "2017-04-02 17"
}, {
  "Brand": "Toyota",
  "Count": 2000,
  "Time": "2017-04-02 18"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 8765,
  "Time": "2017-04-02 16"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 3445,
  "Time": "2017-04-02 17"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 1232,
  "Time": "2017-04-02 18"
}];

data.forEach(function(d) {
  d.Time = d3.timeParse("%Y-%m-%d %H")(d.Time)
});

var dataGroup = d3.nest() //d3 method that groups data by Brand  
  .key(function(d) {
    return d.Brand;
  })
  .entries(data);

//var color = d3.scale.category10();
var vis = d3.select("#visualisation"),
  WIDTH = 1000,
  HEIGHT = 500,
  MARGINS = {
    top: 50,
    right: 20,
    bottom: 50,
    left: 50
  },
  xScale = d3.scaleTime().range([MARGINS.left, WIDTH - MARGINS.right]).domain([d3.min(data, function(d) { //set up x-axis based on data
    return d.Time;
  }), d3.max(data, function(d) {
    return d.Time;
  })]),

  yScale = d3.scaleLinear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([d3.min(data, function(d) { //set up y-axis based on data
    return d.Count;
  }), d3.max(data, function(d) {
    return d.Count;
  })]),

  xAxis = d3.axisBottom()
  .scale(xScale),
  yAxis = d3.axisLeft()
  .scale(yScale)

vis.append("svg:g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
  .call(xAxis);

vis.append("svg:g")
  .attr("class", "y axis")
  .attr("transform", "translate(" + (MARGINS.left) + ",0)")
  .call(yAxis);

var lineGen = d3.line()
  .x(function(d) {
    return xScale(d.Time);
  })
  .y(function(d) {
    return yScale(d.Count);
  })
  .curve(d3.curveBasis);

dataGroup.forEach(function(d, i) { //iterate over the dataGroup and create line graph for each brand
  vis.append('svg:path')
    .attr('d', lineGen(d.values))
    .attr('stroke', function(d, j) {
      return "hsl(" + Math.random() * 360 + ",100%,50%)"; //random color for each brand line on graph
    })
    .attr('stroke-width', 2)
    .attr('id', 'line_' + d.key)
    .attr('fill', 'none');

  lSpace = WIDTH / dataGroup.length; //define the legend space based on number of brands
  vis.append("text")
    .attr("x", (lSpace / 2) + i * lSpace)
    .attr("y", HEIGHT)
    .style("fill", "black")
    .attr("class", "legend")
    .on('click', function() {
      var active = d.active ? false : true;
      var opacity = active ? 0 : 1;
      d3.select("#line_" + d.key).style("opacity", opacity);
      d.active = active;
    })
    .text(d.key);
});
   .axis path {
   fill: none;
   stroke: #777;
   shape-rendering: crispEdges;
 }
 
 .axis text {
   font-family: Lato;
   font-size: 13px;
 }
 
 .legend {
   font-size: 14px;
   font-weight: bold;
   cursor: pointer;
<title>D3 Test</title>
<script src="https://d3js.org/d3.v4.js"></script>

<body>
  <svg id="visualisation" width="1000" height="600"></svg>
  <script src="InitChart.js"></script>
</body>
Daniel Tesfaye
  • 81
  • 1
  • 13

1 Answers1

0

I think it is because of the "()" character in the legend, not sure though.

Yes, it is, but not only that: you should also not use spaces in a CSS selector.

Thus, an easy solution is removing them when setting the ID:

.attr('id', 'line_' + d.key.replace(/[ )(]/g,''))

And also, of course, when selecting:

d3.select("#line_" + d.key.replace(/[ )(]/g,''))

Here is your updated code:

var data = [{
  "Brand": "Toyota",
  "Count": 1800,
  "Time": "2017-04-02 16"
}, {
  "Brand": "Toyota",
  "Count": 1172,
  "Time": "2017-04-02 17"
}, {
  "Brand": "Toyota",
  "Count": 2000,
  "Time": "2017-04-02 18"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 8765,
  "Time": "2017-04-02 16"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 3445,
  "Time": "2017-04-02 17"
}, {
  "Brand": "General Motors Truck Company (GMC)",
  "Count": 1232,
  "Time": "2017-04-02 18"
}];

data.forEach(function(d) {
  d.Time = d3.timeParse("%Y-%m-%d %H")(d.Time)
});

var dataGroup = d3.nest() //d3 method that groups data by Brand  
  .key(function(d) {
    return d.Brand;
  })
  .entries(data);

//var color = d3.scale.category10();
var vis = d3.select("#visualisation"),
  WIDTH = 1000,
  HEIGHT = 500,
  MARGINS = {
    top: 50,
    right: 20,
    bottom: 50,
    left: 50
  },
  xScale = d3.scaleTime().range([MARGINS.left, WIDTH - MARGINS.right]).domain([d3.min(data, function(d) { //set up x-axis based on data
    return d.Time;
  }), d3.max(data, function(d) {
    return d.Time;
  })]),

  yScale = d3.scaleLinear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([d3.min(data, function(d) { //set up y-axis based on data
    return d.Count;
  }), d3.max(data, function(d) {
    return d.Count;
  })]),

  xAxis = d3.axisBottom()
  .scale(xScale),
  yAxis = d3.axisLeft()
  .scale(yScale)

vis.append("svg:g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
  .call(xAxis);

vis.append("svg:g")
  .attr("class", "y axis")
  .attr("transform", "translate(" + (MARGINS.left) + ",0)")
  .call(yAxis);

var lineGen = d3.line()
  .x(function(d) {
    return xScale(d.Time);
  })
  .y(function(d) {
    return yScale(d.Count);
  })
  .curve(d3.curveBasis);

dataGroup.forEach(function(d, i) { //iterate over the dataGroup and create line graph for each brand
  vis.append('svg:path')
    .attr('d', lineGen(d.values))
    .attr('stroke', function(d, j) {
      return "hsl(" + Math.random() * 360 + ",100%,50%)"; //random color for each brand line on graph
    })
    .attr('stroke-width', 2)
    .attr('id', 'line_' + d.key.replace(/[ )(]/g,''))
    .attr('fill', 'none');

  lSpace = WIDTH / dataGroup.length; //define the legend space based on number of brands
  vis.append("text")
    .attr("x", (lSpace / 2) + i * lSpace)
    .attr("y", HEIGHT)
    .style("fill", "black")
    .attr("class", "legend")
    .on('click', function() {
      var active = d.active ? false : true;
      var opacity = active ? 0 : 1;
      d3.select("#line_" + d.key.replace(/[ )(]/g,'')).style("opacity", opacity);
      d.active = active;
    })
    .text(d.key);
});
   .axis path {
   fill: none;
   stroke: #777;
   shape-rendering: crispEdges;
 }
 
 .axis text {
   font-family: Lato;
   font-size: 13px;
 }
 
 .legend {
   font-size: 14px;
   font-weight: bold;
   cursor: pointer;
<title>D3 Test</title>
<script src="https://d3js.org/d3.v4.js"></script>

<body>
  <svg id="visualisation" width="1000" height="600"></svg>
  <script src="InitChart.js"></script>
</body>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • PS: I'm not a regex expert, I'm just using the first one I found online. If you want, replace it for the regex of your choice. – Gerardo Furtado May 10 '17 at 01:57
  • hey so is regex used to remove special characters as you demonstrated? I myself am unfamiliar with regex but this worked perfectly. How would I be able to remove a period character from a css selector? example: removing "." from google.com? – Daniel Tesfaye May 10 '17 at 21:50
  • Here it is: http://stackoverflow.com/questions/2390789/how-to-replace-all-dots-in-a-string-using-javascript – Gerardo Furtado May 11 '17 at 05:34
  • Hey, I've really appreciated your help on d3 related requests. Wondering if you have any experience on d3 legend spacing? As per my question here, https://stackoverflow.com/q/44420867/5573429 – Daniel Tesfaye Jun 08 '17 at 16:57
  • @Daniel, It seems evenly spaced to me... Can you explain better what's the problem there? – Gerardo Furtado Jun 08 '17 at 22:22
  • Thanks for your response!! So, yes it is evenly spaced when you go on the jsfiddle link that I provided. However that is only because I've manually inserted fixed lengths/widths in the legend function that spaces it evenly for that specific data output. The problem I am having is that won't always be my data output, there may be cases with just 1 or 2 legends, which will be displayed all the way to the right. So rather than fixed numbers, I was hoping to find out a way create some dynamic configurations that'll automatically space legends based on # of legends. Hope that makes sense! thanks! – Daniel Tesfaye Jun 08 '17 at 22:47