图表

时间:2016-02-24 14:09:22

标签: javascript c# linq d3.js

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

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

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()
                                 });

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

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

使用以下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();
}
}

显然,这是非常长且非常有限的,因为图表的每个系列都是硬编码的。因此,如果添加另一种通信方法,那将是相当多的工作。解决这个问题背后的想法是拥有一个动态数量的系列,并为每个系列创建一条线。因此,我想我的数据结构必须是:

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以解决此问题。

为冗长的问题道歉,并提前感谢任何帮助。

如果需要更多信息,请询问,我会尽我所能。

谢谢,

修改

以下是Mark尝试以下建议后的更新代码:

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();
}
}

1 个答案:

答案 0 :(得分:2)

对于StackOverflow,您的问题可能有点过于宽泛,但我会尝试提供帮助。我总是接近我的API输出数据的问题的方式是问我的数据将如何在前端消耗?在这种情况下,您尝试创建一个d3多线图表,d3将需要一个包含数据点数组的对象数组(此处为example很棒) 。在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接口,flat是最有用的)。那么,我们如何为易于使用的d3结构进行最后的跳转。鉴于你:

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

会生成一个JSON对象,如:

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

d3然后可以到我们的&#34;易于使用&#34;格式一样简单:

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
         }
      ]
   }
]

从这种结构中,您的多线图变得轻而易举......

评论的编辑

我真的不明白您尝试对某些代码执行的操作(特别是对于您的methods变量 - 数据已经是d3的一种很好的格式)。所以我重构了一下:

&#13;
&#13;
<!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>
&#13;
&#13;
&#13;

编辑评论2

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

// 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});
    }
  }
});

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