Defining custom D3 symbols

我的梦境 提交于 2021-01-27 21:10:08

问题


I'm trying to develop a custom shape in D3, but I'm a little green Javascript wise and am not sure what the D3-Shapes library is doing under the hood.

In particular, I'm not sure what the default function in symbol.js is doing or how it is used. From the little bit of JS that I do know it appears to be describing a prototype or possibly a class. Supposedly one may register additional shapes using the symbol.type class/function but I'm not sure how I should be calling this e.g. var symbol = symbol("SYMBOL").type(SYMBOL) or possibly var SYMBOL = symbol().type(SYMBOL) or some other combination. Issue 23 discusses how the shapes library has changed from the original implementation which I think negates this SO: answer. I also scrounged the D3-Symbol-Extra for clues but no dice.

So far I have the following shape definition for a superellipse that matches up to the symbol definitions in the D3 Shapes library, e.g. circle, diamond, etc.

var superellipse = {
 draw : function (context, size) {
  var w = size;
  var h = size;
  var n = 2/4;
  context.moveTo(w/2*Math.sign(Math.cos( 0/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 0/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 0/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 0/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 1/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 1/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 1/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 1/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 2/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 2/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 2/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 2/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 3/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 3/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 3/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 3/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 4/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 4/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 4/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 4/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 5/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 5/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 5/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 5/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 6/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 6/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 6/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 6/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 7/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 7/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 7/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 7/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 8/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 8/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 8/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 8/9*2*Math.PI)), n));
  context.lineTo(w/2*Math.sign(Math.cos( 9/9*2*Math.PI))*Math.pow(Math.abs(Math.cos( 9/9*2*Math.PI)), n), h/2*Math.sign(Math.sin( 9/9*2*Math.PI))*Math.pow(Math.abs(Math.sin( 9/9*2*Math.PI)), n));
  context.closePath();
  }
};

I can get the shape to be drawn according to the default size, but I can't tweak the dimensions as one might for a circle by setting the r attribute; in this case by setting the n, h and w attributes.

var svg = d3.select("svg")
    .attr("width", 500)
    .attr("height", 500);

function visualize(json) {
    var nodes = svg.selectAll("g nodetext")
        .data(json.nodes);

    var entryPoint = nodes.enter()
        .append("g")
        .attr("class", "node")
        .attr("transform", function(d) {
            return "translate(" + d.x + ",80)"
        });

    // var ellipse = entryPoint.append("circle")
    //               .attr("r", 50);

    var nodeShape = entryPoint.append("path")
        .attr("d", d3.symbol().type(superellipse))
        .style("stroke", "gray")
        .attr("fill", "pink")
        // .n(function(d) {return d.n;})
        .attr("w", function(d) {
            return d.w;
        })
        .attr("h", function(d) {
            return d.h;
        });

    var nodeText = entryPoint.append("text")
        .text(function(d) {
            return d.text;
        })
        .attr({
            "text-anchor": "middle"
        });
};

To be explicit, I'm invoking this as follows :

var superellipse = {
  draw: function(context, size) {
    var w = size;
    var h = size;
    var n = 2 / 4;
    context.moveTo(w / 2 * Math.sign(Math.cos(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(0 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(0 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(1 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(1 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(2 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(2 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(3 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(3 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(4 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(4 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(5 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(5 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(6 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(6 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(7 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(7 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(8 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(8 / 9 * 2 * Math.PI)), n));
    context.lineTo(w / 2 * Math.sign(Math.cos(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(9 / 9 * 2 * Math.PI)), n), h / 2 * Math.sign(Math.sin(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(9 / 9 * 2 * Math.PI)), n));
    context.closePath();
  }
};

var svg = d3.select("svg")
  .attr("width", 500)
  .attr("height", 500);

function visualize(json) {
  var nodes = svg.selectAll("g nodetext")
    .data(json.nodes);

  var entryPoint = nodes.enter()
    .append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + d.x + ",80)"
    });

  // var ellipse = entryPoint.append("circle")
  //               .attr("r", 50);

  var nodeShape = entryPoint.append("path")
    .attr("d", d3.symbol().type(superellipse))
    .style("stroke", "gray")
    .attr("fill", "pink")
    // .n(function(d) {return d.n;})
    .attr("w", function(d) {
      return d.w;
    })
    .attr("h", function(d) {
      return d.h;
    });

  var nodeText = entryPoint.append("text")
    .text(function(d) {
      return d.text;
    })
    .attr({
      "text-anchor": "middle"
    });
}

var json = {
  "nodes": [{
      "text": "test A",
      "x": 100,
      "y": 100,
      "w": 200,
      "n": 0.5,
      "h": 200,
      "color": "red"
    },
    {
      "text": "test B",
      "x": 200,
      "y": 200,
      "w": 200,
      "n": 3,
      "h": 100,
      "color": "red"
    },
  ]
};
visualize(json);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

回答1:


You claimed that you...

can't tweak the dimensions as one might for a circle by setting the r attribute; in this case by setting the n, h and w attributes.

Well, in your case you can pass one attribute, using symbol.size(). For instance, 100:

.attr("d", d3.symbol().type(superellipse).size(100))

However, the problem is that symbol.size() will pass just a single value: you cannot try to pass several arguments, or an array, or an object, or anything like these. So, in the example above, w, h and n will be 100.

If you inspect the source code of symbol.size() you'll see:

symbol.size = function(_) {
    return arguments.length ? 
        (size = typeof _ === "function" ? _ : constant(+_), symbol) : size;
};

That unary plus show us that, if you try to pass an array or an object, you'll get a NaN, and if you pass more than one value they will be simply ignored.

So, how can we fix this?

The correct tool for the task

In the end, it seems to me that your problem is simply that you're using the wrong toll for this task. You don't need a symbol generator, you can use a common function to generate the d attribute of the <path> element.

For instance:

function superellipse (w, h, n){
    return "M" + (w / 2 * Math.sign(Math.cos(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(0 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(0 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(1 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(1 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(2 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(2 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(3 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(3 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(4 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(4 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(5 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(5 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(6 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(6 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(7 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(7 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(8 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(8 / 9 * 2 * Math.PI)), n)))
        + " L" + (w / 2 * Math.sign(Math.cos(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(9 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(9 / 9 * 2 * Math.PI)), n)));
};

That way you can get the w, h and n properties defined in the datum object and pass them to the function without any problem.

Here is a demo with your data:

function superellipse (w, h, n){
	return "M" + (w / 2 * Math.sign(Math.cos(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(0 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(0 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(0 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(1 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(1 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(1 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(2 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(2 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(2 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(3 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(3 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(3 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(4 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(4 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(4 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(5 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(5 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(5 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(6 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(6 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(6 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(7 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(7 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(7 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(8 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(8 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(8 / 9 * 2 * Math.PI)), n)))
		+ " L" + (w / 2 * Math.sign(Math.cos(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.cos(9 / 9 * 2 * Math.PI)), n) + "," + (h / 2 * Math.sign(Math.sin(9 / 9 * 2 * Math.PI)) * Math.pow(Math.abs(Math.sin(9 / 9 * 2 * Math.PI)), n)));
};

var svg = d3.select("svg")
  .attr("width", 500)
  .attr("height", 500);

function visualize(json) {
  var nodes = svg.selectAll("g nodetext")
    .data(json.nodes);

  var entryPoint = nodes.enter()
    .append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      return "translate(" + d.x + "," + d.y + ")"
    });

  var nodeShape = entryPoint.append("path")
    .attr("d", function(d){
    	return superellipse(d.w, d.h, d.n)
    })
    .style("stroke", "gray")
    .attr("fill", "pink");

  var nodeText = entryPoint.append("text")
    .text(function(d) {
      return d.text;
    })
    .attr({
      "text-anchor": "middle"
    });
}

var json = {
  "nodes": [{
      "text": "test A",
      "x": 100,
      "y": 100,
      "w": 200,
      "n": 0.5,
      "h": 200,
      "color": "red"
    },
    {
      "text": "test B",
      "x": 300,
      "y": 100,
      "w": 200,
      "n": 3,
      "h": 100,
      "color": "red"
    },
  ]
};
visualize(json);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>


来源:https://stackoverflow.com/questions/51983142/defining-custom-d3-symbols

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!