susielu / d3-legend

A reusable d3 legend component.
http://d3-legend.susielu.com/
Apache License 2.0
727 stars 104 forks source link

ShapeWidth for Symbols? #70

Closed alexlenail closed 6 years ago

alexlenail commented 7 years ago

This library is awesome! For my purposes, I just want to scale the symbols in my symbol legend so they're a little more visible. I think this is the "ShapeWidth" feature, which is available for color and size legends but not symbol legends?

susielu commented 7 years ago

Yes I don't provide a shapeWidth, the assumption is that you would pass in a larger shape as the path. Do you have an example you could share?

alexlenail commented 6 years ago

Hi @susielu !

You might have already given me the hint I needed. Right now, I have:

var display_type = d3.scaleOrdinal()
                     .domain(['protein', 'TF', 'metabolite'])
                     .range([d3.symbolCircle, d3.symbolTriangle, d3.symbolDiamond] );

svg.append("g")
   .attr("class", "legendSymbol")
   .attr("transform", "translate(20, 200)");

var legendPath = d3.legendSymbol()
                    .scale(display_type)
                    .orient("vertical")
                    .title("Node Types");

svg.select(".legendSymbol")
   .call(legendPath);

Should the scaleOrdinal have some size-related info added to it to fix this?

susielu commented 6 years ago

Yes you should be able to do instead of d3.symbolDiamond this d3.symbol().type(d3.symbolDiamond).size(100)() it defaults to 64, see docs: https://github.com/d3/d3-shape#symbols

alexlenail commented 6 years ago

Thanks @susielu !

I'm concerned this might cause an issue for me... further down I do

node.append("path")
        .attr("d", d3.symbol()
                     .type(function(d) { return display_type(d.type); })
                     .size(function(d) { return ... }) )

(recall)

var display_type = d3.scaleOrdinal()
                     .domain(['a', 'b', 'c'])
                     .range([d3.symbolCircle, d3.symbolTriangle, d3.symbolDiamond] );

I'm concerned that

var triangle = d3.symbol().type(d3.symbolTriangle)(),
     circle = d3.symbol().type(d3.symbolCircle)(),
     diamond = d3.symbol().type(d3.symbolDiamond)();

var display_type = d3.scaleOrdinal()
                     .domain(['a', 'b', 'c'])
                     .range([circle, triange, diamond] );

will cause issues for .type(function(d) { return display_type(d.type); }).size(...).

Is there some way I could change some part of this to avoid that issue?

If not, it might make sense to have d3.legendSymbol().scale(...) to be able to take a scale like my original "display_type" with .range([d3.symbolCircle, etc..

susielu commented 6 years ago

Hmm not sure I follow, I assume you could just do this and it would work as a legend, maybe I"m missing something in your example?:


var triangle = d3.symbol().type(d3.symbolTriangle).size(10)(),
     circle = d3.symbol().type(d3.symbolCircle).size(50)(),
     diamond = d3.symbol().type(d3.symbolDiamond).size(100)();

var display_type = d3.scaleOrdinal()
                     .domain(['protein', 'TF', 'metabolite'])
                     .range([triangle, circle, diamond] );

svg.append("g")
   .attr("class", "legendSymbol")
   .attr("transform", "translate(20, 200)");

var legendPath = d3.legendSymbol()
                    .scale(display_type)
                    .orient("vertical")
                    .title("Node Types");

svg.select(".legendSymbol")
   .call(legendPath);
alexlenail commented 6 years ago

@susielu sorry I was unclear. If I do what you suggest, the following code breaks:

node.append("path")
        .attr("d", d3.symbol()
                     .type(function(d) { return display_type(d.type); })
                     .size(function(d) { return ... }) )

Note that above, display_type is my scaleOrdinal. If my scaleOrdinal is

.range([d3.symbolCircle, d3.symbolTriangle, d3.symbolDiamond] );

Then

        .attr("d", d3.symbol()
                     .type(function(d) { return display_type(d.type); })

evaluates to a symbol type, which is good. But if I change display_type to have range

.range([triangle, circle, diamond] );

where

var triangle = d3.symbol().type(d3.symbolTriangle).size(10)(),

Then

.type(function(d) { return display_type(d.type); })

throws an error. There might be a solution whereby I can build my scaleOrdinal the way you do above, and then do something like

        .attr("d", d3.symbol()
                     .type(function(d) { return display_type(d.type).type; })

(notice the last .type). But I'm not sure.

I want to repeat that it might make sense to have d3.legendSymbol().scale(...) able to take a scale like my original "display_type" with .range([d3.symbolCircle, etc... What do you think?

susielu commented 6 years ago

So you should be able to use the scale ordinal and change the var with the size like you said:

var triangle = d3.symbol().type(d3.symbolTriangle).size(10)()

And later on when you use the scale instead of

.attr("d", d3.symbol().type(function(d) { return display_type(d.type)})

You should do:

.attr("d",d => display_type(d.type))

since you already evaluated the symbol with a size in the your scale, the range of your scale is already in the string path you need for the "d" property. Does that make sense?

I don't think I'll be able to handle a scale property because by the time I have the symbols in my legend function they're already path strings. Unless you using something like transform to scale it, but then the strokes etc. all get off on size.

alexlenail commented 6 years ago

This worked! I just need to figure out how to size the nodes now. The issue was that in

var circle = node.append("path")
        .attr("d",d => display_type(d.type))
        .size(function(d) { return Math.PI * Math.pow(size(d.size) || nominal_base_node_size, 2); })
        .style(...

.size returns an int, breaking with what seems like convention to me. =/