最近還在忙着基於ABP的項目,但本篇博客和ABP無關,喜歡ABP框架的朋友請點擊傳送門。
這不,最近項目基本功能做的差不多了,現在在做一個數據統計的功能,需要繪制區域圖(或折線圖)和餅圖。一開始,樓主就去Google了一下最常用的繪圖插件都有哪些,最后直接去Github上搜索關鍵詞chart,搜索結果如下:

點了幾個進去看了之后,樓主考慮到項目要以后肯定要維護,萬一維護的開發者英文不咋地呢(其實樓主我是喜歡看英文文檔的)?所以,我起初選擇了某度出品的Echarts.js。但是選擇了它之后,查看文檔學習,雖然文檔是中文的,但我感覺這文檔比英文還難讀懂,因為有些術語解釋不詳細,最后好不容易做了個demo,但還出現了bug,開了一個Issue,維護人員簡單地敷衍之后反而直接給關了,樓主表示很受傷也很氣憤。心想,好吧,你某度牛逼,我不用你的Echarts行了吧,惹不起還躲不起嘛。
最后,經過幾個朋友的介紹,他們都選擇的Highcharts,去Highcharts官網看了下,感覺文檔就是比Echarts詳細,簡單易懂,所以我也就選擇了她。【這里建議新手朋友們先使用Highcharts,等對圖表熟悉了再使用Echarts,畢竟Echarts的圖表種類很豐富】而且,到現在,功能也都實現了,效果如下:


樓主在學習的時候,發現網上這方面的資料也不是很多,尤其是從服務端如何傳數據到客戶端,沒有詳細的解決方案,也是摸索了很久,這次,我直接將自己的解決方案拿出來和大家分享,供初學者參考,少走彎路,大神請繞道。
區域圖
<div class="row">
<div class="portlet light bordered">
<div class="portlet-title">
<div class="caption">
<i class="fa fa-area-chart font-purple"></i>
<span class="caption-subject bold uppercase">收入趨勢</span>
</div>
</div>
<div class="portlet-body">
<div id="incomeTrend" style="width:98%;height: 500px">
</div>
</div>
</div>
</div>
var dateSpan;
Highcharts.setOptions({
lang: {
printChart: '打印圖表',
downloadJPEG: '下載為JPEG圖片',
downloadPDF: '下載為PDF',
downloadPNG: '下載為PNG圖片',
downloadSVG: '下載為SVG矢量圖',
months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
weekdays: ["周日", "周一", "周二", "周三", "周四", "周五", "周六"],
shortMonths: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
}
});
var isByDay = true;//default by days
var option = {
chart: {
type: 'area'
},
title: {
text: '收入趨勢圖'
},
subtitle: {
text: '沒有選擇時間范圍的話,默認顯示最近7天的數據'
},
credits: {
enabled: false
},
xAxis: {
type: 'datetime',
tickmarkPlacement: 'on',
title: {
enabled: false
},
dateTimeLabelFormats: {
day: "%Y-%m-%d",
week: "%A",
month: "%Y-%m",
year: "%Y"
}
},
yAxis: {
title: {
text: '單位:元'
},
labels: {
formatter: function () {
return this.value;
}
}
},
tooltip: {
shared: true,
valueSuffix: ' 元',
dateTimeLabelFormats: {
day: "%Y-%m-%d,%A",
week: "%A開始, %Y-%m-%d",
month: "%Y-%m",
year: "%Y"
}
},
plotOptions: {
area: {
stacking: 'normal',
lineColor: '#666666',
lineWidth: 1,
marker: {
lineWidth: 1,
lineColor: '#666666'
}
},
series: {
//pointStart: Date.UTC(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate() - 7),
//pointInterval: 24 * 36e5 //一天
}
},
series: [{}]
}
var url = getTargetUrl('Dashboard', "GetJsonResult");//這里是url
var drp = $('#dateRange').data('daterangepicker');
if (!dateSpan) {
dateSpan = { start: drp.startDate.format('YYYY-MM-DD'), end: drp.endDate.format('YYYY-MM-DD') }
}
rawChart(isByDay);
$('#createChart').click(function (e) {
if ($('#byMonth').attr('checked')) {//按月
isByDay = false;
//alert('選擇了' + $('#byMonth').attr('checked'));
}
e.preventDefault();
drawChart(isByDay);
drawPieChart(isByDay);
});
$('#defaultChart').click(function (e) {
e.preventDefault();
drp.setStartDate(moment().subtract(7, "days"));
drp.setEndDate(moment().subtract(1, "days"));
dateSpan = { start: drp.startDate.format('YYYY-MM-DD'), end: drp.endDate.format('YYYY-MM-DD') };
$('#dateRange').val('');
isByDay = true;
drawChart(isByDay);
drawPieChart(isByDay);
});
function drawChart(isByDay) {
var year = moment(dateSpan.start).format('YYYY');
var month = moment(dateSpan.start).format('M') - 1;//js的date函數的月份是從0-11,所以這里減1
var day = moment(dateSpan.start).format('D');
//console.log(year,month,day);
if (isByDay) {
$.getJSON(url, dateSpan, function (datas) {
option.series = datas;
option.plotOptions.series.pointStart = Date.UTC(year, month, day);
option.plotOptions.series.pointInterval = 24 * 36e5;
$('#incomeTrend').highcharts(option);
});
} else {
var start = drp.startDate.format('YYYY-MM');
var end = drp.endDate.format('YYYY-MM');
if (start == end) {
start = drp.startDate.subtract(5, "month").format('YYYY-MM');
}
year = moment(start).format('YYYY');
month = moment(start).format('M')-1;
dateSpan = { start: start, end: end };
$.getJSON(url, dateSpan, function (datas) {
option.series = datas;
option.plotOptions.series.pointStart = Date.UTC(year, month, 1);
option.plotOptions.series.pointInterval = 1;
option.plotOptions.series.pointIntervalUnit = "month";
$('#incomeTrend').highcharts(option);
});
}
}
注意: 區域圖和餅圖公用同一個action,所以代碼一起放到最后。
餅圖
<div class="row">
<div class="portlet light bordered col-md-8">
<div class="portlet-title">
<div class="caption">
<i class="fa fa-adjust font-red"></i>
<span class="caption-subject bold uppercase">收入比例</span>
</div>
</div>
<div class="portlet-body">
<div id="incomeRatio" style="width:90%;height: 500px">
</div>
</div>
</div>
var pieChartOption = {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie'
},
title: {
text: ''
},
credits: {
enabled: false
},
tooltip: {
pointFormat: '{series.name}: <b>{point.percentage:.1f}%</b>'
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
format: '<b>{point.name}</b>: {point.percentage:.1f}%<br/> {y}元 ',
style: {
color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black'
}
}
}
},
series: [
{
name: '占比',
colorByPoint: true,
data: []
}
]
};
function drawPieChart() {
var year = moment(dateSpan.start).format('YYYY');
var month = moment(dateSpan.start).format('M') - 1;//js的date函數的月份是從0-11,所以這里減1
var day = moment(dateSpan.start).format('D');
//console.log(year,month,day);
$.getJSON(url + "?chartType=pie", dateSpan, function (datas) {
pieChartOption.series[0].data = datas;
var sum=0;
for (var i = 0; i < datas.length; i++) {
sum += datas[i].y;
}
pieChartOption.title.text = "收入比例情況:(總收入"+sum+")元";
$('#incomeRatio').highcharts(pieChartOption);
});
}
drawPieChart();
服務端Web層的C#代碼如下:
public async Task<ContentResult> GetJsonResult(string start, string end)
{
string dataJsonStr;
var defaultStart = DateTime.Parse(start);
var defaultEnd = DateTime.Parse(end);
var timeSpan = new DateTimeSpan { Start = defaultStart, End = defaultEnd };
var totalIncomeList = await _orderAppService.GetDateIncome(new GetDateIncomeDto
{
Start = defaultStart,
End = defaultEnd
});//總收入
var scanCodeChargeIncomeList = await _orderAppService.GetDateIncome(new GetDateIncomeDto
{
Start = defaultStart,
End = defaultEnd,
IsScanCodeChargingIncome = true
});//掃碼充電收入
var lineSoldIncomeList = await _orderAppService.GetDateIncome(new GetDateIncomeDto
{
Start = defaultStart,
End = defaultEnd,
IsLineSoldIncome = true
});//售線收入
var castCoinsIncomeList = await _castCoinsAppService.GetDateCoinsIncome(new GetDateCoinsIncomeDto
{
Start = defaultStart,
End = defaultEnd
});//投幣收入
var allKindsOfIncomeList = new List<DateIncomeListWithName>
{
new DateIncomeListWithName
{
DateIncomeDtos = castCoinsIncomeList,
Name = "投幣"
},
new DateIncomeListWithName
{
DateIncomeDtos = lineSoldIncomeList,
Name = "售線"
},
new DateIncomeListWithName
{
DateIncomeDtos = scanCodeChargeIncomeList,
Name = "掃碼充電"
}
};
if (Request.QueryString.Get("chartType") == "pie")//餅圖
{
var pieDataList = new List<PieChartDataFormat>();
GetPieChartData(pieDataList, allKindsOfIncomeList);
dataJsonStr = JsonConvert.SerializeObject(pieDataList, new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() });
}
else
{
var dataList = new List<ChartDataFormat>();
allKindsOfIncomeList.Add(new DateIncomeListWithName{DateIncomeDtos = totalIncomeList,Name = "總收入"});
GetData(dataList,allKindsOfIncomeList,timeSpan);
dataJsonStr = JsonConvert.SerializeObject(dataList, new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() });
}
return Content(dataJsonStr);
}
private void GetData(List<ChartDataFormat> dataList, List<DateIncomeListWithName> incomeList, DateTimeSpan span)
{
var dateList = ConvertTimeSpanToList(span);
foreach (DateIncomeListWithName dateIncomeListWithName in incomeList)
{
var newList = CheckoutIncomeList(dateIncomeListWithName.DateIncomeDtos, dateList);
var list = newList.Select(dateIncomeDto => dateIncomeDto.Income).ToList();
dataList.Add(new ChartDataFormat
{
Name = dateIncomeListWithName.Name,
Data = list
});
}
}
private void GetPieChartData(List<PieChartDataFormat> dataList, List<DateIncomeListWithName> incomeLists)
{
foreach (DateIncomeListWithName dateIncomeListWithName in incomeLists)
{
var total = dateIncomeListWithName.DateIncomeDtos.Sum(i => i.Income);
var item = new PieChartDataFormat
{
Name = dateIncomeListWithName.Name,
Y = total
};
dataList.Add(item);
}
}
List<DateIncomeDto> CheckoutIncomeList(List<DateIncomeDto> incomeList, List<DateTime> dateList)
{
var newIncomeList = new List<DateIncomeDto>();
newIncomeList = (from date in dateList
join incomeDto in incomeList on date.Date equals incomeDto.Date into result
from r in result.DefaultIfEmpty()
select new DateIncomeDto
{
Date = date.Date,
Income = r == null ? 0 : r.Income
}).ToList();
return newIncomeList;
}
private List<DateTime> ConvertTimeSpanToList(DateTimeSpan span)
{
var list = new List<DateTime>();
for (DateTime i = span.Start; i <= span.End; i = i.AddDays(1))
{
list.Add(i);
}
return list;
}
上面這段代碼,樓主自認為封裝的還不錯,很簡潔(這已經成為樓主編程追求的目標),平均每個方法10行左右(除了第一個),僅供大家參考。
下面兩個類定義了兩種圖表從Server端到Client端的數據格式 :####
class ChartDataFormat
{
public string Name { get; set; }
public List<decimal> Data { get; set; }
}
class PieChartDataFormat
{
public string Name { get; set; }
public decimal Y { get; set; }
}
應用服務層也貼一個方法的代碼,僅供參考
public async Task<List<DateIncomeDto>> GetDateIncome(GetDateIncomeDto input)
{
var query= _orderRepository.GetAll()
.Where(o => o.Status == OrderStatus.Freezen || o.Status == OrderStatus.Settled || o.Status == OrderStatus.HasInformedDevice)
.Where(o => o.OrderDate >= input.Start && o.OrderDate <= input.End)
.WhereIf(input.IsLineSoldIncome,o=>o.OrderType==OrderType.LineSold)
.WhereIf(input.IsScanCodeChargingIncome,o=>o.OrderType==OrderType.Charge)
.OrderBy(o => DbFunctions.TruncateTime(o.OrderDate))
.GroupBy(o => DbFunctions.TruncateTime(o.OrderDate))
.Select(group => new DateIncomeDto{Date=group.Key.Value,Income=group.Sum(item=>item.PayFee??0)});
var list = await query.ToListAsync();
return list;
}
這些就是整個圖表的實現方案,切記僅供參考,不可生搬硬套,如因程序bug導致您公司的重大損失,本人一概不負責任。此話莫當真,純屬娛樂一下。
