图表上的动态线数 [英] Dynamic number of lines on chart

查看:76
本文介绍了图表上的动态线数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前有一个d3多系列折线图,它显示了已收到多少电子邮件和电话.

I currently have a d3 multiseries line chart which displays how many emails and phone calls have been received.

我的数据检索和数据结构如下:

My data retrieval and data structure is as follows:

var allCommunications = _uow.CommunicationRepository.Get()
                                .Where(c => c.DateOpened.Year == year)
                                .GroupBy(c => new { c.Method, c.DateOpened.Month })
                                .Select(g => new
                                 {
                                     Type = g.Key.Method,
                                     xVal = g.Key.Month,
                                     Value = g.Count()
                                 });

然后将其转换为以下结构:

This is then converted to the following structure:

public class LineChartData
{
    public int xValue { get; set; }
    public int EmailValue { get; set; }
    public int PhoneValue { get; set; }
}

该图是使用以下javascript创建的:

The graph is created using the following javascript:

function buildCommunicationLineChart(data, placeholder, callback, type) {
var margin = { top: 20, right: 30, bottom: 40, left: 50 },
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
    emailLineColour = "#779ECB", phoneLineColour = "#FF6961", tooltipTextColour = "white";

var x;

if (type == "month") {
    var x = d3.scale.linear()
                    .domain([1, 31])
                    .range([0, width]);
} else if (type == "year")
{
    var x = d3.scale.linear()
                    .domain([1, 12])
                    .range([0, width]);
}


var minPhone = Math.min.apply(Math, data.map(function (o) { return o.PhoneValue }));
var maxPhone = Math.max.apply(Math, data.map(function (o) { return o.PhoneValue }));
var minEmail = Math.min.apply(Math, data.map(function (o) { return o.EmailValue }));
var maxEmail = Math.max.apply(Math, data.map(function (o) { return o.EmailValue }));

var minY = Math.min(minPhone, minEmail);
var maxY = Math.max(maxPhone, maxEmail);

var y = d3.scale.linear()
    .domain([minY, maxY + 5])
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .tickSize(-height)
    .tickPadding(10)
    .tickSubdivide(true)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .tickPadding(10)
    .tickSize(-width)
    .tickSubdivide(true)
    .orient("left");

if (type == "month") {
    var emailTip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function (d) {
          return "<strong>Emails:</strong> <span style='color:"+tooltipTextColour+"'>" + d.EmailValue + "</span><br /><strong>Day of Month:</strong><span style='color:white'>" + d.xValue + "</span>";
      });

    var phoneTip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function (d) {
          return "<strong>Calls:</strong> <span style='color:" + tooltipTextColour + "'>" + d.PhoneValue + "</span><br /><strong>Day of Month:</strong><span style='color:white'>" + d.xValue + "</span>";
      });
}
else if (type == "year") {
    var emailTip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function (d) {
          return "<strong>Emails:</strong> <span style='color:" + tooltipTextColour + "'>" + d.EmailValue + "</span><br /><strong>Month of Year:</strong><span style='color:white'>" + d.xValue + "</span>";
      });

    var phoneTip = d3.tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html(function (d) {
          return "<strong>Calls:</strong> <span style='color:" + tooltipTextColour + "'>" + d.PhoneValue + "</span><br /><strong>Month of Year:</strong><span style='color:white'>" + d.xValue + "</span>";
      });
}

var svg = placeholder.append("svg")
    .attr("width", width + margin.left + margin.right + 50)
    .attr("height", height + margin.top + margin.bottom)
    .attr("class", "chart")
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

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

svg.call(emailTip);
svg.call(phoneTip);

if (type == "year") {
    svg.append("g")
        .attr("class", "x axis")
        .append("text")
        .attr("class", "axis-label")
        .attr("transform", "none")
        .attr("y", (-margin.left) + 530)
        .attr("x", -height + 860)
        .text('Month');
}
else if (type == "month") {
    svg.append("g")
        .attr("class", "x axis")
        .append("text")
        .attr("class", "axis-label")
        .attr("transform", "none")
        .attr("y", (-margin.left) + 525)
        .attr("x", -height + 860)
        .text('Day');
}        

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

svg.append("g")
    .attr("class", "y axis")
    .append("text")
    .attr("class", "axis-label")
    .attr("transform", "rotate(-90)")
    .attr("y", (-margin.left) + 15)
    .attr("x", -height / 2)
    .text('Communications');

svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

var emailLine = d3.svg.line()
    .interpolate("linear")
    .x(function (d) { return x(d.xValue); })
    .y(function (d) { return y(d.EmailValue); });

var phoneLine = d3.svg.line()
    .interpolate("linear")
    .x(function (d) { return x(d.xValue); })
    .y(function (d) { return y(d.PhoneValue); });

svg.selectAll('.emailLine')
        .data(data)
        .enter()
        .append("path")
        .attr("class", "line")
        .attr('stroke', emailLineColour)
        .attr("d", emailLine(data));

svg.selectAll("circle.emailLine")
        .data(data)
        .enter().append("svg:circle")
        .attr("class", "emailLine")
        .style("fill", emailLineColour)
        .attr("cx", emailLine.x())
        .attr("cy", emailLine.y())
        .attr("r", 5)
        .on('mouseover', emailTip.show)
        .on('mouseout', emailTip.hide);

svg.selectAll('.phoneLine')
    .data(data)
    .enter()
    .append("path")
    .attr("class", "line")
    .attr('stroke', phoneLineColour)
    .attr("d", phoneLine(data));

svg.selectAll("circle.phoneLine")
        .data(data)
        .enter().append("svg:circle")
        .attr("class", "phoneLine")
        .style("fill", phoneLineColour)
        .attr("cx", phoneLine.x())
        .attr("cy", phoneLine.y())
        .attr("r", 5)
        .on('mouseover', phoneTip.show)
        .on('mouseout', phoneTip.hide);

svg.append("text")
    .attr("transform", "translate(" + (x(data[data.length - 1].xValue) + 5) + "," + y(data[data.length - 1].EmailValue) + ")")
    .attr("dy", ".35em")
    .style("fill", emailLineColour)
    .text("Email");

svg.append("text")
    .attr("transform", "translate(" + (x(data[data.length - 1].xValue) + 5) + "," + y(data[data.length - 1].PhoneValue) + ")")
    .attr("dy", ".35em")
    .style("fill", phoneLineColour)
    .text("Phone");

if (callback) {
    callback();
}
}

显然,由于图表的每个系列都进行了硬编码,因此这是很长且非常有限的.因此,如果添加另一种通信方法,则将需要大量工作.解决这个问题的我的想法是要有一个动态的系列数,并为每个系列创建一条线.因此,我想我的数据结构必须像这样:

Obviously this is very long and very limited due to each series for the chart being hardcoded. Therefore, it would be quite a bit of work if another method of communication is added. My idea behind resolving this is to have a dynamic number of series and create a line for each series. Therefore i guess my data structure would have to be something like:

public class LineChartData
{
    public string Type {get;set;} //for the label
    public Data Data{get;set;}
}

public class Data
{
    public int xValue { get; set; }
    public int Value { get; set; }
}

还是类似的东西?

所以我想我的问题是,这是构造数据的正确方法吗,是否有任何建议可以更改查询以实现此目的,以及我将如何编辑javascript以解决此问题?

So i guess my question is, would this be the correct approach to structuring my data, any suggestions to change my query to do this, and how would i edit my javascript in order to account for this.

对于长期以来提出的问题深表歉意,并在此先感谢您的帮助.

Apologies for the long winded question and thanks in advance for any help.

如果需要更多信息,请询问,我会提供一切.

If any more info is required, please ask and i will provide anything i can.

谢谢

在尝试以下Mark的建议后,这是我的更新代码:

Here is my updated code after attempting the suggestion by Mark below:

function buildCommunicationLineChart(data, placeholder, callback, type) {
var margin = { top: 20, right: 30, bottom: 40, left: 50 },
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
    emailLineColour = "#779ECB", phoneLineColour = "#FF6961", tooltipTextColour = "white";

var color = d3.scale.category10();

var nest = d3.nest()
              .key(function (d) { return d.Type; })
              .entries(data);

var x;

if (type == "month") {
    var x = d3.scale.linear()
                    .domain([1, 31])
                    .range([0, width]);
} else if (type == "year")
{
    var x = d3.scale.linear()
                    .domain([1, 12])
                    .range([0, width]);
}

var y = d3.scale.linear()
    .domain([0, 100])
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .tickSize(-height)
    .tickPadding(10)
    .tickSubdivide(true)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .tickPadding(10)
    .tickSize(-width)
    .tickSubdivide(true)
    .orient("left");

var line = d3.svg.line()
            .interpolate("linear")
            .x(function (d) { return x(d.xValue); })
            .y(function (d) { return y(d.Value); });

var svg = placeholder.append("svg")
    .attr("width", width + margin.left + margin.right + 50)
    .attr("height", height + margin.top + margin.bottom)
    .attr("class", "chart")
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

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

if (type == "year") {
    svg.append("g")
        .attr("class", "x axis")
        .append("text")
        .attr("class", "axis-label")
        .attr("transform", "none")
        .attr("y", (-margin.left) + 530)
        .attr("x", -height + 860)
        .text('Month');
}
else if (type == "month") {
    svg.append("g")
        .attr("class", "x axis")
        .append("text")
        .attr("class", "axis-label")
        .attr("transform", "none")
        .attr("y", (-margin.left) + 525)
        .attr("x", -height + 860)
        .text('Day');
}        

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

svg.append("g")
    .attr("class", "y axis")
    .append("text")
    .attr("class", "axis-label")
    .attr("transform", "rotate(-90)")
    .attr("y", (-margin.left) + 15)
    .attr("x", -height / 2)
    .text('Communications');

svg.append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

color.domain(d3.keys(nest[0]).filter(function (key) { return key === nest[0].key; }));

var methods = color.domain().map(function (commType) {
    return {
        commType: commType,
        values: nest.map(function (d) {
            return { xValue: d.xVal, Value: d.Value };
        })
    };
});

x.domain(d3.extent(nest, function (d) { return d.xVal; }));

y.domain([
    d3.min(methods, function (m) { return d3.min(m.values, function (v) { return v.Value; }); }),
    d3.max(methods, function (m) { return d3.max(m.values, function (v) { return v.Value; }); })
]);

var method = svg.selectAll('.method')
                    .data(methods)
                    .enter().append('g')
                    .attr('class', 'method');

method.append('path')
        .attr('class', 'line')
        .attr('d', function (d) { return line(d.values); })
        .attr('stroke', function (d) { return color(d.commType); });

method.append('text')
        .datum(function (d) { return { commType: d.commType, value: d.values[d.values.length - 1] }; })
        .attr("transform", function (d) { return "translate(" + x(d.value.xVal) + "," + y(d.value.Value) + ")"; })
        .attr('x', 3)
        .attr('dy', '.35em')
        .text(function (d) { return d.commType; });

if (callback) {
    callback();
}
}

推荐答案

对于StackOverflow,您的问题可能太宽泛了,但我会尽力提供帮助.我一直处理API应该如何输出数据的问题的方式是,问我的数据将如何在前端使用?在这种情况下,您尝试创建d3多折线图,并且d3将需要一个包含数据点数组的对象数组(这是一个很棒的

Your question might be a little too broad for StackOverflow, but I'll try to help. The way I always approach the question of how should my API output data, is to ask how is my data going to be consumed on the front-end? In this case, you are trying to create a d3 multi-line chart and d3 will want an array of objects containing an array of data points (here's a great example). Something like this in JSON:

[
  {
    key: 'Email', //<-- identifies the line
    values: [ //<-- points for the line
      {
        xVal: '20160101',
        Value: 10
      }, {
        xVal: '20160102',
        Value: 20
      }, ...
    ]
  }, {
    key: 'Phone',
    values: [
      {
        xVal: 'Jan',
        Value: 30
      }, {
        xVal: '20160102',
        Value: 25
      }, ...
    ]
  },
  ...
]

现在的问题是如何将您的数据放入这样的结构中.给定许多小时,您可能会写一个会执行的linq语句,我有点像返回一个平面JSON对象(毕竟,如果我们正在编写一个可重用的Restful接口,则平面是最重要的)有用).因此,我们如何才能使易于使用d3结构的最后一步.鉴于您:

Now the question becomes how to get your data into a structure like that. Given many hours, you could probably write a linq statement that'll do but, I kinda like returning a flat JSON object (after all if we are writing a re-useable restful interface, flat is the most useful). So, how then would we make that final jump for our easy to use d3 structure. Given your:

.Select(g => new
{
   Type = g.Key.Method,
   xVal = g.Key.Month,
   Value = g.Count()
});

会产生一个像这样的JSON对象:

would produce a JSON object like:

[{"Type":"Phone","xVal":"Feb","Value":1},{"Type":"Email","xVal":"Jan","Value":3},{"Type":"Phone","xVal":"Jan","Value":1}]

然后

d3可以像下面这样简单地使用我们的易于使用"格式:

d3 could then get to our "easy to work with" format as easy as:

var nest = d3.nest()
  .key(function(d) { return d.Type; })
  .entries(data);

哪个会产生:

[  
   {  
      "key":"Phone",
      "values":[  
         {  
            "Type":"Phone",
            "xVal":"Feb",
            "Value":1
         },
         {  
            "Type":"Phone",
            "xVal":"Jan",
            "Value":1
         }
      ]
   },
   {  
      "key":"Email",
      "values":[  
         {  
            "Type":"Email",
            "xVal":"Jan",
            "Value":3
         }
      ]
   }
]

通过这种结构,您的多折线图变得轻而易举....

From this structure, your multi-line chart becomes a breeze....

评论编辑

我真的不明白您要使用某些代码(特别是使用methods变量-数据对于d3而言已经是一种很好的格式)了.所以我重构了一下:

I really didn't understand what you were attempting to do with some of your code (in particular with your methods variable - the data was already in a great format for d3). So I refactored a bit:

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>

  <style>
    body {
      font: 10px sans-serif;
    }
    
    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }
    
    .x.axis path {
      display: none;
    }
    
    .line {
      fill: none;
      stroke: steelblue;
      stroke-width: 1.5px;
    }
  </style>
</head>

<body>
  <script>
    // function buildCommunicationLineChart(data, placeholder, callback, type) {
    var margin = {
        top: 20,
        right: 30,
        bottom: 40,
        left: 50
      },
      width = 960 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;

    var colors = {
      "Phone": "#FF6961",
      "Email": "#779ECB"
    }
    
    var color = d3.scale.category10();

    var data = [{
      "Type": "Phone",
      "xValue": 1,
      "Value": 5
    }, {
      "Type": "Email",
      "xValue": 1,
      "Value": 7
    }, {
      "Type": "Email",
      "xValue": 2,
      "Value": 1
    }, {
      "Type": "Phone",
      "xValue": 2,
      "Value": 4
    }, {
      "Type": "Phone",
      "xValue": 4,
      "Value": 2
    }];

    var nest = d3.nest()
      .key(function(d) {
        return d.Type;
      })
      .entries(data);

    var x;
    var type = "month";
    if (type == "month") {
      var x = d3.scale.linear()
        .domain([1, 31])
        .range([0, width]);
    } else if (type == "year") {
      var x = d3.scale.linear()
        .domain([1, 12])
        .range([0, width]);
    }

    var y = d3.scale.linear()
      .domain([0, 100])
      .range([height, 0]);

    var xAxis = d3.svg.axis()
      .scale(x)
      .tickSize(-height)
      .tickPadding(10)
      .tickSubdivide(true)
      .orient("bottom");

    var yAxis = d3.svg.axis()
      .scale(y)
      .tickPadding(10)
      .tickSize(-width)
      .tickSubdivide(true)
      .orient("left");

    var line = d3.svg.line()
      .interpolate("linear")
      .x(function(d) {
        return x(d.xValue);
      })
      .y(function(d) {
        return y(d.Value);
      });

    var svg = d3.select('body').append("svg")
      .attr("width", width + margin.left + margin.right + 50)
      .attr("height", height + margin.top + margin.bottom)
      .attr("class", "chart")
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
      
    y.domain([
      0,
      d3.max(nest, function(t) { return d3.max(t.values, function(v) { return v.Value; }); })
    ]);
    
    x.domain([
      d3.min(nest, function(t) { return d3.min(t.values, function(v) { return v.xValue; }); }),
      d3.max(nest, function(t) { return d3.max(t.values, function(v) { return v.xValue; }); })
    ]);
    
    nest.forEach(function(d){
      for (var i = x.domain()[0]; i <= x.domain()[1]; i++){
        if (!d.values.some(function(v){ return (v.xValue === i) })){
          d.values.splice((i - 1), 0, {xValue: i, Value: 0});
        }
      }
    });
    
    var xAxis = svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

    if (type == "year") {
      xAxis
        .append("text")
        .attr("class", "axis-label")
        .attr("transform", "none")
        .attr("y", margin.top + 15)
        .attr("x", width / 2)
        .text('Month');
    } else if (type == "month") {
      xAxis
        .append("text")
        .attr("class", "axis-label")
        .attr("y", margin.top + 15)
        .attr("x", width / 2)
        .text('Day')
        .style('text-anchor', 'middle');
    }

    svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
      .append("text")
      .attr("class", "axis-label")
      .attr("transform", "rotate(-90)")
      .attr("y", (-margin.left) + 15)
      .attr("x", -height / 2)
      .text('Communications')
      .style('text-anchor', 'middle');

    svg.append("clipPath")
      .attr("id", "clip")
      .append("rect")
      .attr("width", width)
      .attr("height", height);

    /*
    color.domain(d3.keys(nest[0]).filter(function(key) {
      return key === nest[0].key;
    }));

    var methods = color.domain().map(function(commType) {
      return {
        commType: commType,
        values: nest.map(function(d) {
          return {
            xValue: d.xVal,
            Value: d.Value
          };
        })
      };
    });
    */

    var method = svg.selectAll('.method')
      .data(nest)
      .enter().append('g')
      .attr('class', 'method');

    method.append('path')
      .attr('class', 'line')
      .attr('d', function(d) {
        return line(d.values);
      })
      .style('stroke', function(d) {
        return color(d.key);
        // OR if you want to use you defined ones
        //return colors[d.key];
      });

    method.append('text')
      .attr("transform", function(d) {
        var len = d.values.length - 1;
        return "translate(" + x(d.values[len].xValue) + "," + y(d.values[len].Value) + ")";
      })
      .attr('x', 3)
      .attr('dy', '.35em')
      .text(function(d) {
        return d.key;
      });

    //if (callback) {
    //  callback();
    //}
    //  }
  </script>
</body>

</html>

编辑评论2

这实际上是一个棘手的问题.怎么样:

That's actually a tricky question. How about:

// for each dataset
nest.forEach(function(d){
  // loop our domain
  for (var i = x.domain()[0]; i <= x.domain()[1]; i++){
    // if there's no xValue at that location
    if (!d.values.some(function(v){ return (v.xValue === i) })){
      // add a zero in place
      d.values.splice((i - 1), 0, {xValue: i, Value: 0});
    }
  }
});

上面的代码示例也被编辑.

Code sample above is edited also.

这篇关于图表上的动态线数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆