ラテ欄(番組表)を作る

次のようなJSONを投げて、適当にサイズを設定したらラテ欄みたいなものを生成したくなった。

data = [
    { "title": "アニゲマスター", "day": "Sat", "station": "文化放送", "start": "21:00", "end": "23:00"  },
    { "title": "ドリカン", "day": "Sat", "station": "文化放送", "start": "23:00", "end": "24:00"  }
];

コードは以下、gistsにもあります。

https://gist.github.com/schuhiti/7393a3262f3bb4f8413a#file-rateran-js

var w = 840;
var h = 630;
var lineWidth = 1;
var textOffset = 40;
var hOffset = 20;
var wOffset = 4;

var data = [
        { "title": "アニゲマスター", "day": "Sat", "station": "文化放送", "start": "21:00", "end": "23:00"  },
        { "title": "ドリカン", "day": "Sat", "station": "文化放送", "start": "23:00", "end": "24:00"  },
        { "title": "ツイパラ3", "day": "Sat", "station": "文化放送", "start": "24:00", "end": "24:30"  },
        { "title": "ポリケロ", "day": "Sat", "station": "文化放送", "start": "24:30", "end": "25:00"  },
        { "title": "電撃大賞", "day": "Sat", "station": "文化放送", "start": "26:00", "end": "26:30"  },
        { "title": "マルチ天国", "day": "Sat", "station": "文化放送", "start": "25:00", "end": "25:30"  },
        { "title": "プリンセスクラブ", "day": "Sat", "station": "文化放送", "start": "26:30", "end": "27:00"  },
        { "title": "あぁぁ新天地", "day": "Sun", "station": "文化放送", "start": "23:30", "end": "24:00"  },
        { "title": "少コミ♡Night", "day": "Sun", "station": "文化放送", "start": "24:00", "end": "24:30"  },
        { "title": "銀河にほえろ", "day": "Sun", "station": "文化放送", "start": "24:30", "end": "25:00"  },
        { "title": "みんなでた~る", "day": "Sun", "station": "TBS", "start": "25:30", "end": "26:00"  },
        { "title": "ぴんくドラゴン", "day": "Mon", "station": "AM KOBE", "start": "25:00", "end": "25:30"  },
        { "title": "HAPPY♪プリン", "day": "Tue", "station": "AM KOBE", "start": "25:00", "end": "25:30"  },
        { "title": "はぁとのためいき", "day": "Tue", "station": "AM KOBE", "start": "24:30", "end": "25:00"  },
        { "title": "輝け!大和魂!!", "day": "Wed", "station": "AM KOBE", "start": "25:00", "end": "25:30"  },
        { "title": "ミラクル!!しゅびっち!!", "day": "Thu", "station": "AM KOBE", "start": "25:00", "end": "25:30"  },
        { "title": "うさぎのみみたぶ", "day": "Fri", "station": "AM KOBE", "start": "25:00", "end": "25:30"  },
        { "title": "覇王塾F", "day": "Fri", "station": "文化放送", "start": "24:00", "end": "24:30"  },
        { "title": "アニスク", "day": "Fri", "station": "文化放送", "start": "24:30", "end": "25:00"  },
];

var days = { "Sun": 0, "Mon": 1, "Tue": 2, "Wed": 3, "Thu": 4, "Fri": 5, "Sat": 6 };
var wScale = d3.time.scale()
    .domain([0, 7])
    .range([wOffset, w - wOffset]);
var rectWidth = wScale(1) - wScale(0);

var today = '2014-01-01T';
var tomorrow = '2014-01-02T';
var minDate = new Date(today+'21:00');
var maxDate = new Date(tomorrow+'03:00');
var hScale = d3.time.scale()
    .domain([minDate, maxDate])
    .range([textOffset + hOffset , h - hOffset]);
var rectHeight = h;

var svg = d3.select("#radio-tv-program").append("svg")
    .attr("width", w)
    .attr("height", h);

// day of week
var dayOfWeek = [
    {"day": "Sun", "color": "Red"},
    {"day": "Mon", "color": "Orange"},
    {"day": "Tue", "color": "Yellow"},
    {"day": "Wed", "color": "Cyan"},
    {"day": "Thu", "color": "BurlyWood"},
    {"day": "Fri", "color": "Green"},
    {"day": "Sat", "color": "Blue"}
];
var header = svg.selectAll(".bg")
    .data(dayOfWeek);
var rect_back = header.enter().append("rect")
    .attr("class", "bg")
    .attr("width", rectWidth)
    .attr("height", rectHeight)
    .attr("opacity", 0.35)
    .attr("fill", function(d){return d.color})
    .attr("y", 0)
    .attr("x", function(d, i){
        return wScale(i) - wOffset;
    });

var text_days = header.enter().append("text")
    .attr("class", "bg")
    .attr("font-size", "12px")
    .attr("font-weight", "bold")
    .attr("y",  "12px")
    .attr("x", function(d, i){
        return wScale(i);
    })
    .text(function(d){ return d.day; });

// Programs
var graph = svg.selectAll(".select")
    .data(data);

var texts_time = graph.enter().append("text")
    .attr("class", "select")
    .attr("font-size", "12px")
    .attr("y", function(d,i) {
        var hm = String(d.start).split(':');
        var h = hm[0];
        var d = today + h + ":"+ hm[1];
        if (h > 24) {
            d = tomorrow + "0" + (h-24) + ":"+ hm[1];
        }
        return hScale(new Date(d)) + 2;
    })
    .attr("x", function(d){
        return wScale(days[d.day]);
    })
    .text(function(d) { return d.start; });

var texts_title = graph.enter().append("text")
    .attr("class", "select")
    .attr("font-size", "11px")
    .attr("y", function(d,i) {
        var hm = String(d.start).split(':');
        var h = hm[0];
        var d = today + h + ":"+ hm[1];
        if (h > 24) {
            d = tomorrow + "0" + (h-24) + ":"+ hm[1];
        }
        return hScale(new Date(d)) + 16;
    })
    .attr("x", function(d){
        return wScale(days[d.day]);
    })
    .text(function(d) { return d.title; });

var yAxisScale = d3.time.scale()
    .domain([minDate, maxDate])
    .range([hOffset*2, h - (hOffset*2)]);;

var axis = d3.svg.axis()
    .scale(yAxisScale)
    .ticks(d3.time.minutes, 30)
    .orient("left");

svg.append("g")
    .attr("class", "yAxis")
    .attr("transform", "translate(0, 10)")
    .call(axis);

d3.selectAll(".yAxis .tick line")
    .attr("stroke", "black")
    .attr("stroke-width", lineWidth)
    .attr("opacity", .8)
    .attr("x1", 0)
    .attr("x2", w - (wOffset*2));