用D3.js進行醫療數據可視化 (一)折線圖 (Line Chart)

介紹

大概說說這個事情的前因後果吧。

 

去年下半年,我有機會參加了一個大型項目,在裏面參與了前端可視化組建的開發,用的library是D3.js [2]。雖然3個月後我就結束了在這個項目裏的工作,但後面斷斷續續也用D3做了一些別的實現。

而從幾年前開始,我一直在做和healthcare相關的項目,主要就是做醫療數據的信息化,從而優化醫療領域的信息管理、決策支持。從項目的角度出發,我們的着眼點一般都是某個具體的問題,比如如何優化病人診療的流程。但從宏觀上出發,如果能瞭解某個地區乃至全國的醫療服務情況是怎麼樣的,也是 非常有意義的 。

結合這兩者,我就想做一件事情,將一些宏觀的醫療服務數據通過可視化技術,更直觀地表達出來。

 

原則

對於做這個事情,個人認爲也有一些原則要遵守。首先,最基本的,醫療數據的來源要公開、權威。其次,最重要的,這樣的可視化要體現醫療服務的特點,使得其在醫療領域能體現價值。當然,這不是一蹴而就的,而需要不斷地在探索和實踐中摸索。實踐本身也是一個學習的過程。除了D3.js官網的API文檔,主要的參考書就是Nick Qi Zhu的《D3.js數據可視化實戰手冊》[3]。由於本人較懶,後面很多實踐也是在Nick的源碼基礎上直接修改所得。

 

數據來源

本着公開、權威的原則,找到了國家衛生和計劃生育委員會統計信息中心[1]的統計數據。由於在這個階段我的目標是驗證醫療數據可視化的一個實現的流程,因此並沒有花太多時間在數據的抓取上。通過簡單的調研,發現其中全國醫療服務情況的數據還比較全面。而由於精力所限,最終選擇了以下幾類醫療衛生機構醫療服務量中的診療人次數,輸出的是JSON格式文檔。

  • 機構合計
    • 醫院
      • 三級醫院
      • 二級醫院
      • 一級醫院
      • 未定級醫院
    • 基層醫療衛生機構

{
	"醫療衛生機構合計": [
		{
			"date": "2013-05",
			"value":	60005.2	
		},{
			"date": "2013-06",
			"value":	58861.9	
		},{
			"date": "2013-07",
			"value":	60264.2	
		},{
			"date": "2013-08",
			"value":	59519.3	
		},...
        ], ...
}

 

可視化選型

通過對數據的解讀並結合之前的原則,我一開始的想法是想用D3圖表表現全國不同醫療機構月均診療人次數隨着時間的變化。也就是說,圖表的橫軸是時間,縱軸是診療人次數,而不同的醫療機構的數據能夠出現在同一個圖表中以便做對比。因此,我自然而然將線圖(linechart)作爲了第一選擇。

 

D3實現

對於基本線圖的實現,《D3.js數據可視化實戰手冊》中有很好的例子,也提供了源碼,因此在整個coding過程中,比較多的時間花在了數據的處理上。本人使用的D3版本是3.5.5。源碼如下。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>全國醫療衛生機構醫療服務量</title>
    <link rel="stylesheet" type="text/css" href="css/styles.css"/>
    <script type="text/javascript" src="js/d3.min.js"></script>
</head>

<body>

<script type="text/javascript">
function lineChart() {
    var _chart = {};

    var _width = screen.width * 0.9, _height = 350,
            _margins = {top: 30, left: 50, right: 30, bottom: 30},
            _x, _y,
            _data = [],
            _colors = d3.scale.category10(),
            _svg,
            _bodyG,
            _line;

    _chart.render = function () {
        if (!_svg) {
            _svg = d3.select("body").append("div")
            		.style("text-align", "center")
            		.append("svg")
                    .attr("height", _height)
                    .attr("width", _width);

            renderAxes(_svg);

            defineBodyClip(_svg);
        }

        renderBody(_svg);
    };

    function renderAxes(svg) {
        var axesG = svg.append("g")
                .attr("class", "axes");

        renderXAxis(axesG);

        renderYAxis(axesG);
    }
    
    function renderXAxis(axesG){
        var xAxis = d3.svg.axis()
                .scale(_x.range([0, quadrantWidth()]))
                .orient("bottom");        

        axesG.append("g")
                .attr("class", "x axis")
                .attr("transform", function () {
                    return "translate(" + xStart() + "," + yStart() + ")";
                })
                .call(xAxis);
                
        d3.selectAll("g.x g.tick")
            .append("line")
                .classed("grid-line", true)
                .attr("x1", 0)
                .attr("y1", 0)
                .attr("x2", 0)
                .attr("y2", - quadrantHeight());
    }
    
    function renderYAxis(axesG){
        var yAxis = d3.svg.axis()
                .scale(_y.range([quadrantHeight(), 0]))
                .orient("left");
                
        axesG.append("g")
                .attr("class", "y axis")
                .attr("transform", function () {
                    return "translate(" + xStart() + "," + yEnd() + ")";
                })
                .call(yAxis);
                
         d3.selectAll("g.y g.tick")
            .append("line")
                .classed("grid-line", true)
                .attr("x1", 0)
                .attr("y1", 0)
                .attr("x2", quadrantWidth())
                .attr("y2", 0);
    }

    function defineBodyClip(svg) {
        var padding = 5;

        svg.append("defs")
                .append("clipPath")
                .attr("id", "body-clip")
                .append("rect")
                .attr("x", 0 - padding)
                .attr("y", 0)
                .attr("width", quadrantWidth() + 2 * padding)
                .attr("height", quadrantHeight());
    }

    function renderBody(svg) {
        if (!_bodyG)
            _bodyG = svg.append("g")
                    .attr("class", "body")
                    .attr("transform", "translate(" 
                        + xStart() + "," 
                        + yEnd() + ")")
                    .attr("clip-path", "url(#body-clip)");        

        renderLines();

        renderDots();
    }

    function renderLines() {
        _line = d3.svg.line()
                        .x(function (d) { return _x(d.date); })
                        .y(function (d) { return _y(d.value); });
                        
        _bodyG.selectAll("path.line")
                    .data(_data)
                .enter()
                .append("path")                
                .style("stroke", function (d, i) { 
                    return _colors(i);
                })
                .attr("class", "line");
        
        _bodyG.selectAll("path.line")
        			.data(_data)
        			.exit()
        			.remove();

        _bodyG.selectAll("path.line")
                .transition()
                .duration(1000)
                .attr("d", function (d) { return _line(d); });
    }

    function renderDots() {
        _data.forEach(function (list, i) {
            _bodyG.selectAll("circle._" + i)
                        .data(list)
                    .enter()
                    .append("circle")
                    .attr("class", "dot _" + i);
            
            _bodyG.selectAll("circle._" + i)
            		.data(list) 
            		.exit()
            		.remove();

            _bodyG.selectAll("circle._" + i)
                    .data(list)                    
                    .style("stroke", function (d) { 
                        return _colors(i);
                    })
                    .transition()
                    .duration(1000)
                    .attr("cx", function (d) { return _x(d.date); })
                    .attr("cy", function (d) { return _y(d.value); })
                    .attr("r", 3.5);
        });
    }

    function xStart() {
        return _margins.left;
    }

    function yStart() {
        return _height - _margins.bottom;
    }

    function xEnd() {
        return _width - _margins.right;
    }

    function yEnd() {
        return _margins.top;
    }

    function quadrantWidth() {
        return _width - _margins.left - _margins.right;
    }

    function quadrantHeight() {
        return _height - _margins.top - _margins.bottom;
    }

    _chart.width = function (w) {
        if (!arguments.length) return _width;
        _width = w;
        return _chart;
    };

    _chart.height = function (h) {
        if (!arguments.length) return _height;
        _height = h;
        return _chart;
    };

    _chart.margins = function (m) {
        if (!arguments.length) return _margins;
        _margins = m;
        return _chart;
    };

    _chart.colors = function (c) {
        if (!arguments.length) return _colors;
        _colors = c;
        return _chart;
    };

    _chart.x = function (x) {
        if (!arguments.length) return _x;
        _x = x;
        return _chart;
    };

    _chart.y = function (y) {
        if (!arguments.length) return _y;
        _y = y;
        return _chart;
    };

    _chart.addSeries = function (series) {
        _data.push(series);
        return _chart;
    };

    return _chart;
}

function randomData() {
    return Math.random() * 9;
}

function update() {
    for (var i = 0; i < data.length; ++i) {
        var series = data[i];
        series.length = 0;
        for (var j = 0; j < numberOfDataPoint; ++j)
            series.push({x: j, y: randomData()});
    }

    chart.render();
}
        
var names = [];
var data = [];
var minvalue = 0, maxvalue = 0;
var mindate = 0, maxdate = 0;

var timeformat = d3.time.format("%Y-%m");

var chart;

<pre name="code" class="javascript">//load data
d3.text("data/health-service-quantity.json", function(rawdatastr){
	var rawdata = JSON.parse(rawdatastr);
	names = d3.keys(rawdata);
	names.forEach(function(name){
		var list = rawdata[name].map(function(item){
			return {
				"date": timeformat.parse(item.date), 
				"value": item.value
			}
		});
		
		//get min and max value
		if(minvalue == 0){
			minvalue = d3.min(list, function(d){return d.value;});			
		}else{
			minvalue = d3.min([minvalue, d3.min(list, function(d){return d.value;})]);
		}
		maxvalue = d3.max([maxvalue, d3.max(list, function(d){return d.value;})]);
		
		//get min and max date
		if(mindate == 0){
			mindate = d3.min(list, function(d){return d.date;});
		}
		if(maxdate == 0){
			maxdate = d3.max(list, function(d){return d.date;});
		}
		
		data.push(list);
	});
	
	chart = lineChart()
			.x(d3.time.scale().domain([mindate, maxdate]))
			.y(d3.scale.linear().domain([minvalue * 0.5, maxvalue * 1.02]));
	
	data.forEach(function (series) {
	    chart.addSeries(series);
	});
	
	chart.render();
});

</script>

</body>

</html>



 

分析一下幾個技術點。

1. 從JSON文件加載數據

D3提供了d3.json,但很不幸地,此API只能加載list式的JSON(例如[1,2, 3])而不能加載key-value式的(例如{“id”:1})。而我之前構造的數據是以不同機構的名字作爲key,其下面的數據列表作爲value。因此我選擇了用d3.text加載JSON文件成String,再通過JSON.parse將String變換成JSON Object。

接下來,考慮到每條線對應的是list式的數據,因此還需要對原始JSONObject做一些變換,比如將每個value中的list放到list變量data中,並將原來以字符串形式存在的date通過d3.time.format變換成Date Object。

 

2. 得到數據分佈的最大最小值

要生成x軸和y軸,需要設置其合理的domain。這兒就要計算數據的最大最小值。對x軸而言,對應的是數據中的日期。而對不同的醫療機構而言,這兒我們假設其數據的日期範圍是一樣的,因此只要對一個機構的數據進行計算即可。用到的就是d3.min和d3.max。並且由於每條數據是一個JSON Object,設置d3.min和d3.max的accessor爲一個返回date的function。對於y軸而言,需要計算所有機構的數據的最大最小值,因此使用了嵌套的d3.min和d3.max。

 

可視化結果

 

 

討論

從呈現的結果來看,達到了初步的預期:每種機構的日均診療人次數各有大小;總體呈緩慢上升趨勢;過年期間會小降;等等。

但總體而言,這個可視化呈現還有很多待改進的地方。

1. 需要顯示圖例,說明每條曲線對應的機構

2. x軸最好能顯示每個月的中文名字,中間區域內的網格也是每個月一格。

3. 由於其中的機構種類有包含關係,是否對其改用堆積區圖 (stackedarea chart) 比較好?

4. 從美觀考慮,線的出現最好有動畫效果。

後面的系列會陸續探討這幾點。

 

參考文獻

1.    國家衛生和計劃生育委員會統計信息中心.http://www.moh.gov.cn/mohwsbwstjxxzx/index.shtml

2.    D3.jsAPI Reference. https://github.com/mbostock/d3/wiki/API-Reference

3.    Nick Qi Zhu. D3.js數據可視化實戰手冊. http://book.douban.com/subject/26256865/

 

發佈了81 篇原創文章 · 獲贊 29 · 訪問量 42萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章