ラインチャートで推定値を表現するサンプル。
Created
February 16, 2018 08:12
-
-
Save shimizu/e4d9e315e6db367d26fdb1ee3188b3a4 to your computer and use it in GitHub Desktop.
Estimation Line
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="jp"> | |
<head> | |
<style> | |
html, body { | |
margin: 0px; | |
padding: 0px; | |
width:100%; | |
height:100%; | |
} | |
#chart { | |
width: 80%; | |
height: 80%; | |
border: 8px dashed gray; | |
border-left: none; | |
border-top:none; | |
cursor:all-scroll; | |
} | |
#chart svg{ | |
width: 100%; | |
height: 100%; | |
cursor: default; | |
} | |
.grid .tick line { | |
stroke:#ccc; | |
stroke-dasharray:1; | |
} | |
.plot .real{ | |
stroke:skyblue; | |
stroke-width:4; | |
fill:none; | |
} | |
.plot .estimate { | |
stroke:white; | |
stroke-width:4; | |
stroke-dasharray:4; | |
fill:none; | |
} | |
#ui { | |
padding: 0px 20px; | |
} | |
#threshold { | |
width: 80%; | |
} | |
</style> | |
<script src="//unpkg.com/babel-standalone@6.26.0/babel.min.js"></script> | |
<script src="//unpkg.com/d3@4.12.2/build/d3.min.js"></script> | |
</head> | |
<body> | |
<div id="chart"> | |
<svg></svg> | |
</div> | |
<div id="ui"> | |
<p id="thresholdLabel"></p> | |
<input id="threshold" type="range" min="0" max="11" list="month" /> | |
<datalist id="month"> | |
<option value="0"> | |
<option value="1"> | |
<option value="2"> | |
<option value="3"> | |
<option value="4"> | |
<option value="5"> | |
<option value="6"> | |
<option value="7"> | |
<option value="8"> | |
<option value="9"> | |
<option value="10"> | |
<option value="11"> | |
</datalist> | |
</div> | |
<script ="text/babel"> | |
const data = d3.range(0, 12).map( (d, i) => { console.log(d,new Date(2017,d,1)) ;return {date:new Date(2017,d,1), value:~~((Math.random() * 6) + (i* 8) )} }); | |
const svg = d3.select("#chart").select("svg"); | |
const defs = svg.append("defs"); | |
const clipPaht = defs.append("clipPath").attr("id", "lineClip").append("rect"); | |
const grid = svg.append("g").classed("grid", true); | |
const plot = svg.append("g").classed("plot", true); | |
const axis = svg.append("g").classed("axis", true); | |
const yScale = d3.scaleLinear().domain([100, 0]); | |
const xScale = d3.scaleTime().domain([new Date(2016,11,1), new Date(2017,12,1)]); | |
const lineGen = d3.line() | |
.x(d => xScale(d.date)) | |
.y(d => yScale(d.value)) | |
const yearFormat = d3.timeFormat("%Y年"); | |
const monthFormat = d3.timeFormat("%m月"); | |
const thresholdLabel = d3.select("#thresholdLabel"); | |
let estimateThreshold; | |
//スライドーバーの値を推定値の閾値としてチャートを再描画する。 | |
d3.select("#threshold").on("input", function() { | |
estimateThreshold = new Date(2017, this.value, 1); | |
thresholdLabel.text(`${(+this.value)+1}月`) | |
render(); | |
}); | |
//読み込み時に一回強制的にinputイベントを発火させる。 | |
document.querySelector("#threshold").dispatchEvent(new Event('input')); | |
//render(); | |
function render(){ | |
const m = {top:30, left:40, right:20, bottom:30}; | |
const w = svg.node().clientWidth || svg.node().parentNode.clientWidth; | |
const h = svg.node().clientHeight || svg.node().parentNode.clientHeight; | |
const pw = w - (m.left + m.right); | |
const ph = h - (m.top + m.bottom); | |
yScale.range([0, ph]); | |
xScale.range([0, pw]); | |
//axis layer | |
axis.attr("transform", `translate(${m.left}, ${m.top})`); | |
//y axis | |
const yAxisUpdate = axis.selectAll(".yAxis").data([null]); | |
const yAxisEnter = yAxisUpdate.enter().append("g").classed("yAxis", true); | |
const yAxisRender = d3.axisLeft().scale(yScale); | |
const yAxis = yAxisUpdate.merge(yAxisEnter).call(yAxisRender); | |
yAxis.selectAll(".tick line").remove(); | |
//x axis | |
const xAxisUpdate = axis.selectAll(".xAxis").data([null]); | |
const xAxisEnter = xAxisUpdate.enter().append("g").classed("xAxis", true); | |
const renderAxis = d3.axisBottom().scale(xScale) | |
.tickFormat(d => { | |
return monthFormat(d).replace(/^0/, ""); | |
}) | |
.tickValues(data.map(d => d.date)) //データが存在する範囲のtickだけを表示するように制限する | |
const xAxis = xAxisUpdate.merge(xAxisEnter).call(renderAxis) | |
.attr("transform", `translate(0, ${ph})`); | |
xAxis.selectAll(".tick line").remove(); | |
//grid layer | |
grid.attr("transform", `translate(${m.left}, ${m.top})`) | |
//y grid | |
const yGridUpdate = grid.selectAll(".yGrid").data([null]); | |
const yGridEnter = yGridUpdate.enter().append("g").classed("yGrid", true); | |
const yGrid = yGridUpdate.merge(yGridEnter).call( d3.axisLeft().scale(yScale).tickSizeInner(-pw).tickFormat(() => null) ); | |
//x grid | |
const xGridUpdate = grid.selectAll(".xGrid").data([null]); | |
const xGridEnter = xGridUpdate.enter().append("g").classed("xGrid", true); | |
xGridUpdate.merge(xGridEnter).call( d3.axisBottom().scale(xScale).tickSizeInner(-ph).tickFormat( ()=> null) ) | |
.attr("transform", `translate(0, ${ph})`); | |
//clippPath setting //推定値ように切り抜き位置を調整 | |
clipPaht | |
.attr("transform", `translate(${xScale(estimateThreshold)}, ${m.top})`) | |
.attr("width", pw - xScale(estimateThreshold)) | |
.attr("height", ph) | |
.attr("fill", "black") | |
//plot layer | |
plot.attr("transform", `translate(${m.left}, ${m.top})`) | |
//実測値line | |
const rLineUpdate = plot.selectAll(".real").data([data]); | |
const rLineEnter = rLineUpdate.enter().append("path").classed("real", true); | |
const rLine = rLineUpdate.merge(rLineEnter) | |
.attr("d", lineGen); | |
//推定値line | |
const eLineUpdate = plot.selectAll(".estimate").data([data]); | |
const eLineEnter = eLineUpdate.enter().append("path").classed("estimate", true); | |
const eLine = eLineUpdate.merge(eLineEnter) | |
.attr("d", lineGen) | |
.attr("clip-path", "url(#lineClip)") //破線(白)を実測値のラインに重ねて推定値のラインを表現している。 | |
; | |
} | |
//divエレメントをドラッグでリサイズできるようにする。 | |
const dispatch = d3.dispatch("resize"); | |
dispatch.on("resize", render); | |
setResizeControler(); | |
function setResizeControler(){ | |
const drag = d3.drag() | |
.on("drag", resized) | |
d3.select("#chart") | |
.call(drag); | |
function resized(e){ | |
const s = d3.event.sourceEvent; | |
const w = (s.pageX < 300) ? 300 : s.pageX; | |
const h = (s.pageY < 200) ? 200 : s.pageY; | |
d3.select(this) | |
.style("width", `${w}px`) | |
.style("height", `${h}px`) | |
.attr("data-test", "test") | |
dispatch.call("resize"); | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment