天天看點

highstock K線圖 深入研究

好久沒寫部落格了,這次來個品質點的。

K線圖,相信每個股民都不陌生,如何用SVG畫好一個K線圖是一個難題。

我選擇用highstock做為畫圖元件,适當的修改了一下源碼,參考了數個财經網站的案例,完成了一個不太成熟的K線圖,歡迎大家批評指正。

highstock K線圖 深入研究
highstock K線圖 深入研究

上圖就是整個K線圖的樣子,圖的上半部分是K線圖和5日均線,10日均線,30日均線,下半部分是成交量,用柱狀圖顯示,tooltips顯示了使用者選擇點的股票名額,所有顔色符合紅漲綠跌的原則。

實作的功能主要有:

1.根據使用者選擇的時間區間,顯示最高價和最低價。

2.點選最高價或最低價的flags會顯示出相應的時間。

3.動态改變X軸時間顯示格式(%Y       %Y-%m        %m-%d),防止樣式重疊在一起。

4. 動态改變Y軸的最大值最小值,防止K線圖畫出去。

5.根據目前點的開盤價和收盤價改變柱狀圖的顔色。

6.本地化一些常量,本地化日期格式。

7.根據滑鼠指向的目前點的位置。動态改變tooltip的位置

下面附上源碼

//highstock K線圖
var highStockChart = function(divID,result,crrentData){
	var $reporting = $("#report");
	var firstTouch = true;
	//開盤價^最高價^最低價^收盤價^成交量^成交額^漲跌幅^換手率^五日均線^十日均線^20日均線^30日均線^昨日收盤價 ^目前點離左邊的相對距離
	var  open,high,low,close,y,zde,zdf,hsl,MA5,MA10,MA20,MA30,zs,relativeWidth; 
	//定義數組
	var ohlcArray = [],volumeArray = [],MA5Array = [],MA10Array=[],MA20Array=[],MA30Array=[],zdfArray=[],zdeArray=[],hslArray=[],data=[],dailyData = [],data =[];	
	/*
	 * 這個方法用來控制K線上的flags的顯示情況,當afterSetExtremes時觸發該方法,通過flags顯示目前時間區間最高價和最低價
	 * minTime  目前k線圖上最小的時間點
	 * maxTime  目前k線圖上最大的時間點
	 * chart  目前的highstock對象
	 */
	var showTips = 	function (minTime,maxTime,chart){
	//	console.log( Highcharts.dateFormat('%Y-%m-%d %H:%M',minTime));
	//	console.log( Highcharts.dateFormat('%Y-%m-%d %H:%M',maxTime));
		chart.showLoading();
		//定義目前時間區間中最低價的最小值,最高價的最大值 以及對應的時間
		var lowestPrice,highestPrice,array=[],highestArray=[],lowestArray=[],highestTime,lowestTime,flagsMaxData_1=[],flagsMaxData_2=[],flagsMinData_1,flagsMinData_2; 
//		var chartData = chart.series[0].data;
//		for(var i=0;i<chartData.length-1;i++){
//			if(chartData[i].x>minTime && chartData[i].x<=maxTime){
//				array.push([
//				            chartData[i].x,
//				            chartData[i].high, //最高價
//				            chartData[i].low //最低價
//				            ])
//			}
//		}
		for(var i=0;i<ohlcArray.length-1;i++){
			if(ohlcArray[i][0]>=minTime && ohlcArray[i][0]<=maxTime){
				array.push([
				            ohlcArray[i][0],
				            ohlcArray[i][2], //最高價
				            ohlcArray[i][3] //最低價
				            ])
			}
		}
		if(!array.length>0){
			return;
		}
		highestArray = array.sort(function(x, y){  return y[1] - x[1];})[0];// 根據最高價降序排列
		highestTime =highestArray[0];  
		highestPrice =highestArray[1].toFixed(2);  
		lowestArray = array.sort(function(x, y){  return x[2] - y[2];})[0]; //根據最低價升序排列
		lowestTime =lowestArray[0];  
		lowestPrice =lowestArray[2].toFixed(2); 
		var formatDate1 = Highcharts.dateFormat('%Y-%m-%d',highestTime)
		var formatDate2 = Highcharts.dateFormat('%Y-%m-%d',lowestTime)
		flagsMaxData_1 = [
		               			{
		               			 x : highestTime,
		               			title : highestPrice+"("+formatDate1+")"
		               			}
		               		];
		
		flagsMaxData_2 = [
					               {
					                x : highestTime,
					                title : highestPrice
					               }
		               ];
		flagsMinData_1 = [
		                  {
		                	  x : lowestTime,
		                	  title : lowestPrice+"("+formatDate2+")"
		                  }
		                  ];
		
		flagsMinData_2 = [
		               {
		            	   x : lowestTime,
		            	   title : lowestPrice
		               }
		               ];
		var min =  parseFloat(flagsMinData_2[0].title) - parseFloat(flagsMinData_2[0].title)*0.05;
		var max =  parseFloat(flagsMaxData_2[0].title)+parseFloat(flagsMaxData_2[0].title)*0.05;
		var tickInterval = (( max-min)/5).toFixed(1)*1;
		var oneMonth = 1000*3600*24*30;
		var oneYear = 1000*3600*24*365;
		var tickIntervalTime,dataFormat='%Y-%m';
		if(maxTime-minTime>oneYear*2){
			tickIntervalTime = oneYear*2
			dataFormat = '%Y';
		}else if(maxTime-minTime>oneYear){
			tickIntervalTime = oneMonth*6
		}else if(maxTime-minTime>oneMonth*6){
			tickIntervalTime = oneMonth*3
		}else{
			tickIntervalTime = oneMonth
			dataFormat = '%m-%d'
		}
			
		//Y軸坐标自适應
		 chart.yAxis[0].update({
	    	   	min : min,
	    	   	max : max,
	    	   	tickInterval: tickInterval
	       });
		//X軸坐标自适應
		 chart.xAxis[0].update({
			 min : minTime,
			 max : maxTime,
			 tickInterval: tickIntervalTime,
			 labels: {
				   	y:-78,//調節y偏移
	             formatter: function(e) {
	             		 return Highcharts.dateFormat(dataFormat, this.value);
	             }
	         }
		 });
	 //動态update flags(最高價)
       chart.series[5].update({
    	   data : flagsMaxData_2,
            point:{
         	   events:{
         		  click:function(){
	         			 chart.series[5].update({
	         					data : flagsMaxData_1,
	         					width : 100
	         			 });
	         			 chart.series[6].update({
	         					data : flagsMinData_1,
	         					width : 100
	         			 });
                    }
                }
         },
         events:{
             mouseOut:function(){
		             	 chart.series[5].update({
		   					data :flagsMaxData_2,
		   					width : 25
		             	 });
		             	 chart.series[6].update({
			   					data :flagsMinData_2,
			   					width : 25
			   			 });
             	}
         	}
		});
       
       //動态update flags(最低價)
       chart.series[6].update({
    		data : flagsMinData_2,
    		   point:{
             	   events:{
             		  click:function(){
             			 chart.series[6].update({
	         					data : flagsMinData_1,
	         					width : 100
	         			 });
    	         			 chart.series[5].update({
    	         					data : flagsMaxData_1,
    	         					width : 100
    	         			 });
                        }
                    }
             },
             events:{
                 mouseOut:function(){
		                	 chart.series[6].update({
				   					data :flagsMinData_2,
				   					width : 25
				   			 });
    		             	 chart.series[5].update({
    		   					data :flagsMaxData_2,
    		   					width : 25
    		             	 });
                 	}
             	}
		});
   	chart.hideLoading();
	}
	
	  //修改colum條的顔色(重寫了源碼方法)
	 var originalDrawPoints = Highcharts.seriesTypes.column.prototype.drawPoints;
	    Highcharts.seriesTypes.column.prototype.drawPoints = function () {
	        var merge  = Highcharts.merge,
	            series = this,
	            chart  = this.chart,
	            points = series.points,
	            i      = points.length;
	        
	        while (i--) {
	            var candlePoint = chart.series[0].points[i];
	            if(candlePoint.open != undefined && candlePoint.close !=  undefined){  //如果是K線圖 改變矩形條顔色,否則不變
		            var color = (candlePoint.open < candlePoint.close) ? '#DD2200' : '#33AA11';
		            var seriesPointAttr = merge(series.pointAttr);
		            seriesPointAttr[''].fill = color;
		            seriesPointAttr.hover.fill = Highcharts.Color(color).brighten(0.3).get();
		            seriesPointAttr.select.fill = color;
	            }else{
	            	var seriesPointAttr = merge(series.pointAttr);
	            }
	            
	            points[i].pointAttr = seriesPointAttr;
	        }
	
	        originalDrawPoints.call(this);
	    }

	//常量本地化
	Highcharts.setOptions({
		global : {
			useUTC : false
		},
		lang: {
			rangeSelectorFrom:"日期:",
			rangeSelectorTo:"至",
			rangeSelectorZoom:"範圍",
			loading:'加載中...',
			/*decimalPoint:'.',
         downloadPNG:'下載下傳PNG圖檔',
         downloadJPEG:'下載下傳JPG圖檔',
         downloadPDF:'下載下傳PDF檔案',
         exportButtonTitle:'導出...',
         printButtonTitle:'列印圖表',
         resetZoom:'還原圖表',
         resetZoomTitle:'還原圖表為1:1大小',
         thousandsSep:',',*/
		shortMonths:['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
         weekdays:['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
		},
	});
	//格式化資料,準備繪圖 
	dailyData = result.vl.split("~");	
	for(i=0;i<dailyData.length-1;i++){
		data[i] = dailyData[i].split("^");
	}
	//把目前最新K線資料加載進來
	var length = data.length-1;
	var time = parseFloat(data[length][0]);
	var crrentTime = crrentData[0];
//	if(!(isNaN(crrentData[1]) || isNaN(crrentData[2]) || isNaN(crrentData[3]) || isNaN(crrentData[4]))){
//		if(crrentData[1]!=0 || crrentData[2]!=0 || crrentData[3]!=0 || crrentData[4]!=0){
//			if(time < crrentTime){
//				data.push(crrentData);
//			}else if(time == crrentTime){
//				data[length] = crrentData;	
//			}
//		}
//	}
		
	for (i = 0; i < data.length; i++) {
	//	console.log( Highcharts.dateFormat('%A ,%Y-%m-%d %H:%M',parseInt(data[i][0])));
		ohlcArray.push([
			parseInt(data[i][0]), // the date
			parseFloat(data[i][1]), // open
			parseFloat(data[i][3]), // high
			parseFloat(data[i][4]), // low
			parseFloat(data[i][2]) // close
		]);

		MA5Array.push([
	         parseInt(data[i][0]), // the date
	         parseFloat(data[i][11])
         ]);

		MA10Array.push([
	    	parseInt(data[i][0]),
	    	parseFloat(data[i][12]),
	     ]);
		MA20Array.push([
            parseInt(data[i][0]),
            parseFloat(data[i][13]),             
		                ])
		MA30Array.push([
	             	parseInt(data[i][0]),
	                parseFloat(data[i][14])
	        ]);
		  volumeArray.push([
				parseInt(data[i][0]), // the date
				parseInt(data[i][5]) // 成交量
			]);
	}

	//開始繪圖
	return new Highcharts.StockChart( {
		chart:{
			renderTo : divID,
			margin: [30, 30,30, 30],
			plotBorderColor: '#3C94C4',
			plotBorderWidth: 0.3,
			events:{
				load:function(){
					var length = ohlcArray.length-1;
					showTips(ohlcArray[0][0],ohlcArray[length][0],this);	
				}
			}
		},
		loading: {
	    	labelStyle: {
                position: 'relative',
	            top: '10em',
	            zindex:1000
	    	}
	    },
		 credits:{
	            enabled:false
	        },
	    rangeSelector: {
//	        selected: 1,
//	        buttons: [{
//				type: 'month',
//				count: 1,
//				text: '1月'
//			}, {
//				type: 'month',
//				count: 2,
//				text: '2月'
//			},{
//				type: 'all',
//				text: 'All'
//			}], 
			enabled:false,
	        inputDateFormat: '%Y-%m-%d'  //設定右上角的日期格式
	    },
	    plotOptions: {
	    	//修改蠟燭顔色
	    	candlestick: {
	    		color: '#33AA11',
	    		upColor: '#DD2200',
	    		lineColor: '#33AA11',	    		
	    		upLineColor: '#DD2200', 
	    		maker:{
	    			states:{
	    				hover:{
	    					enabled:false,
	    				}
	    			}
	    		}
	    	},
	    	//去掉曲線和蠟燭上的hover事件
            series: {
            	states: {
                    hover: {
                        enabled: false
                    }
                },
            line: {
                marker: {
                    enabled: false
                }
            }
            }
	    },
	    //格式化懸浮框
	    tooltip: {
		   formatter: function() {
			   if(this.y == undefined){
				   return;
			   }
			   for(var i =0;i<data.length;i++){
				   if(this.x == data[i][0]){
					   zdf = parseFloat(data[i][7]).toFixed(2);
					   zde = parseFloat(data[i][8]).toFixed(2);
				//	   hsl = parseFloat(data[i][9]).toFixed(2);
					   zs = parseFloat(data[i][10]).toFixed(2);
				   }
			   }
			   open = this.points[0].point.open.toFixed(2);
			   high = this.points[0].point.high.toFixed(2);
			   low = this.points[0].point.low.toFixed(2);
			   close = this.points[0].point.close.toFixed(2);
			   y = (this.points[1].point.y*0.0001).toFixed(2);
			   MA5 =this.points[2].y.toFixed(2);
			   MA10 =this.points[3].y.toFixed(2);
			   MA30 =this.points[4].y.toFixed(2);
			   relativeWidth = this.points[0].point.shapeArgs.x;
			   var stockName = this.points[0].series.name;
		      var tip= '<b>'+ Highcharts.dateFormat('%Y-%m-%d  %A', this.x) +'</b><br/>';
		      tip +=stockName+"<br/>";
		      if(open>zs){
    			  tip += '開盤價:<span style="color: #DD2200;">'+open+' </span><br/>';
    		  }else{
    			  tip += '開盤價:<span style="color: #33AA11;">'+open+' </span><br/>';
    		  } 
    		  if(high>zs){
    			  tip += '最高價:<span style="color: #DD2200;">'+high+' </span><br/>';
    		  }else{
    			  tip += '最高價:<span style="color: #33AA11;">'+high+' </span><br/>';
    		  } 
    		  if(low>zs){
    			  tip += '最低價:<span style="color: #DD2200;">'+low+' </span><br/>';
    		  }else{
    			  tip += '最低價:<span style="color: #33AA11;">'+low+' </span><br/>';
    		  }
    		  if(close>zs){
    			  tip += '收盤價:<span style="color: #DD2200;">'+close+' </span><br/>';
    		  }else{
    			  tip += '收盤價:<span style="color: #33AA11;">'+close+' </span><br/>';
    		  }
    		  if(zde>0){
    			  tip += '漲跌額:<span style="color: #DD2200;">'+zde+' </span><br/>';
    		  }else{
    			  tip += '漲跌額:<span style="color: #33AA11;">'+zde+' </span><br/>';
    		  }
    		  if(zdf>0){
    			  tip += '漲跌幅:<span style="color: #DD2200;">'+zdf+' </span><br/>';
    		  }else{
    			  tip += '漲跌幅:<span style="color: #33AA11;">'+zdf+' </span><br/>';
    		  }
    		  if(y>10000){
    			  tip += "成交量:"+(y*0.0001).toFixed(2)+"(億股)<br/>";
    		  }else{
    			  tip += "成交量:"+y+"(萬股)<br/>";
    		  }
    		 /* tip += "換手率:"+hsl+"<br/>";*/
    		  $reporting.html(
    				  '  <span style="font-weight:bold">'+stockName+'</span>'
    				+ '  <span>開盤:</span>'+ open
    				+'  <span>收盤:</span>'+close
              		+'  <span>最高:</span>'+ high
              		+'  <span>最低:</span>'+ low
              		+'  <span style="padding-left:25px;"> </span>'+	Highcharts.dateFormat('%Y-%m-%d',this.x)
              		+'	<br/><b style="color:#1aadce;padding-left:25px">MA5</b> '+ MA5
              		+'  <b style="color: #8bbc21;padding-left:150px">MA10 </b> '+ MA10
              		+'  <b style="color:#910000;padding-left:150px">MA30</b> '+ MA30
              		);
    		  return tip;
		   },
		 //crosshairs:	[true, true]//雙線
		   crosshairs: {
   				dashStyle: 'dash'
		   },
   			borderColor:	'white',
	    	positioner: function () { //設定tips顯示的相對位置
	    		var halfWidth = this.chart.chartWidth/2;//chart寬度
	    		var width = this.chart.chartWidth-155;
	    		var height = this.chart.chartHeight/5-8;//chart高度
	    		if(relativeWidth<halfWidth){
	    			return { x: width, y:height };
	    		}else{
	    			return { x: 30, y: height };
	    		}
	    	},
	    	shadow: false
		},
	    title: {
	        enabled:false
	    },
	    exporting: { 
            enabled: false  //設定導出按鈕不可用 
        }, 
		scrollbar: {
			barBackgroundColor: 'gray',
			barBorderRadius: 7,
			barBorderWidth: 0,
			buttonBackgroundColor: 'gray',
			buttonBorderWidth: 0,
			buttonArrowColor: 'yellow',
			buttonBorderRadius: 7,
			rifleColor: 'yellow',
			trackBackgroundColor: 'white',
			trackBorderWidth: 1,
			trackBorderColor: 'silver',
			trackBorderRadius: 7,
			//enabled: false,
			liveRedraw: false //設定scrollbar在移動過程中,chart不會重繪
		},
		 navigator: {
			 adaptToUpdatedData: false,
			 xAxis: {
				 labels: {
		             formatter: function(e) {
		             		 return Highcharts.dateFormat('%m-%d', this.value);
		             }
		         } 
			 },
			 handles: {
		    		backgroundColor: '#808080',
		    	//	borderColor: '#268FC9'
		    	},
		     margin:-10
		 },
	    xAxis: {
        	type: 'datetime',
        	 tickLength: 0,//X軸下标長度
        	// minRange: 3600 * 1000*24*30, // one month
        	 events: {
        		 afterSetExtremes: function(e) {
 	    			var minTime = Highcharts.dateFormat("%Y-%m-%d", e.min);
 	    			var maxTime = Highcharts.dateFormat("%Y-%m-%d", e.max);
 	    			var chart = this.chart;
 	    			showTips(e.min,e.max,chart);
 	    		}
        	 }
    	},
	    yAxis: [{
	        title: {	
	           enable:false
	        },
	        height: '70%',
	        lineWidth:1,//Y軸邊緣線條粗細
            gridLineColor: '#346691',
            gridLineWidth:0.1,
          // gridLineDashStyle: 'longdash',
            opposite:true
	    },{
	        title: {
	           enable:false
	        },
	        top: '75%',
	        height: '25%',
	        labels:{
	        	x:-15
	        },
	        gridLineColor: '#346691',
            gridLineWidth:0.1,
	        lineWidth: 1,
	    }],
	    series: [
	    {
	    	type: 'candlestick',
	    	id:"candlestick",
	        name: result.cname,
	        data: ohlcArray,
	        dataGrouping: {
				enabled: false
			}
	    }
	    ,{
	        type: 'column',//2
	        name: '成交量',
	        data: volumeArray,
	        yAxis: 1,
	        dataGrouping: {
				enabled: false
			}
	    } ,{
	        type: 'spline',
	        name: 'MA5',
	        color:'#1aadce',
	        data: MA5Array,
	        lineWidth:1,
	        dataGrouping: {
				enabled: false
			}
	    },{
	        type: 'spline',
	        name: 'MA10',
	        data: MA10Array,
	        color:'#8bbc21',
	        threshold: null, 
	        lineWidth:1,
	        dataGrouping: {
				enabled: false
			}
	    },{
	        type: 'spline',
	        name: 'MA30',
	        data: MA30Array,
	        color:'#910000',
	        threshold: null, 
	        lineWidth:1,
	        dataGrouping: {
				enabled: false
			}
	    },{
	    	 type : 'flags',
	           cursor:'pointer',
	           style:{
	        	   fontSize: '11px',
	               fontWeight: 'normal',
	               textAlign: 'center'
	           },
	           lineWidth:0.5,
	           onSeries : 'candlestick',
	           width : 25,
	           shape: 'squarepin'
	    },{
	    	 type : 'flags',
	         cursor:'pointer',
	         y: 33,
	         style:{
	        	   fontSize: '11px',
	               fontWeight: 'normal',
	               textAlign: 'center'
	           },
	           lineWidth:0.5,
	           onSeries : 'candlestick',
	           width : 25,
	           shape: 'squarepin'
	    }
	    ]
	});
}
           

html頁面調用的話需要這樣寫

<script>
new highStockChart('container',retTrade,crrentData);
</script>
<div id="container" style="height: 400px;width: 545px">
           

其中container是highstock的renderID , retTrade是需要組裝的數組,crrentData是當天需要實時更新的數組(也就是圖上的最後一個點,需要過一段時間更新一遍,因為當天的K線名額一直在變)

retTrade的資料格式如下:

{日期^1開盤價^2收盤價^3最高價^4最低價^5成交量^6成交額^7漲跌幅^8漲跌額^9換手率^10昨日收盤價^11MA5^12MA10^13MA20^14MA30^15MA60}

crrentData的資料格式如下:

{日期^1開盤價^2收盤價^3最高價^4最低價^5成交量^MA5^MA10^MA20^MA30}

當然您也可以自定義,隻需要把js中的相應數組下标調整好就可以了。

最後,别忘了引入highstock.js和較高版本的JQUERY,good luck!

附上demo,點開html就能看到效果

繼續閱讀