block by shimizu e4d9e315e6db367d26fdb1ee3188b3a4

Estimation Line

Full Screen

ラインチャートで推定値を表現するサンプル。

index.html

<!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>