Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Qiitaトレンド
<!-- index.html -->
<html>
<head>
<title>Qiitaトレンド</title>
<meta charset="UTF-8"/>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link href="http://nvd3.org/assets/css/nv.d3.css" rel="stylesheet" type="text/css">
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-47473588-2', 'auto');
ga('send', 'pageview');
</script>
<style>
body {
overflow-y:scroll;
}
text {
font: 12px sans-serif;
}
svg {
display: block;
}
body {
background-color: #f8f8f8;
}
header h1 {
color: #79b74a;
}
.tagList {
margin-bottom: 16px;
}
.tag {
display: inline;
margin-right: 8px;
border-radius: 2px;
border-left: solid 4px;
padding: 8px 4px 8px 8px;
background-color: #fff;
}
.tag .tag-name {
font-size: 16px;
margin-right: 16px;
}
.tag i {
margin: 2px;
color: #aaa;
background-color: #eee;
padding: 4px;
font-size: 8px;
border-radius: 2px;
cursor: pointer;
}
.tag i:hover {
color: #000;
}
.chart {
background-color: #fff;
max-width: 900px;
}
.chart svg {
height: 300px;
min-width: 400px;
}
.mask {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.mask > div {
position: fixed;
top: 50%;
left: 50%;
width: 200px;
height: 100px;
margin-top: -50px;
margin-left: -100px;
background-color: rgba(0, 0, 0, 0);
color: rgba(255, 255, 255, 255);
z-index: 2;
text-align: center;
}
</style>
<script src="https://fb.me/react-0.13.1.js"></script>
<script src="https://fb.me/JSXTransformer-0.13.1.js"></script>
<script src="https://code.jquery.com/jquery-1.10.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://nvd3.org/assets/js/nv.d3.js"></script>
<script type="text/jsx" src="qiita-trend.jsx"></script>
</head>
<body>
<header>
<h1>Qiitaトレンド</h1>
</header>
<div id="content"></div>
</body>
</html>
var dateFormat = d3.time.format("%Y/%m/%d");
var color = d3.scale.category10();
function groupDate(date) {
// var d = new Date(date.getFullYear(), date.getMonth(), date.getDate()),
// w = d.getDay(),
// i = 24 * 60 * 60 * 1000;
// return new Date(d - i * w);
return new Date(date.getFullYear(), date.getMonth(), 1);
}
//QiitaAPIの日時文字列をDateに変換する
function parseQiitaDate(date) {
//QiitaAPIの日時文字列"2008-5-1 2:00:00 +0900"がFFでパースできないので
//"2008/5/1 2:00:00 +0900"にしてからDateオブジェクトを生成する
//http://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htm
return new Date(date.replace(/-/g, '/'));
}
//タグの情報を表示するコンポーネント
var Tag = React.createClass({
render: function() {
var tag = this.props.tag;
return (
<div className="tag" style={{ borderColor: color(tag.index) }}>
<span className="tag-name">{tag.name}</span>
<i className="fa fa-plus" onClick={this.props.onMore} title="追加読み込み"></i>
<i className="fa fa-times" onClick={this.props.onRemove} title="削除"></i>
</div>
);
}
});
//タグリスト
var TagList = React.createClass({
render: function() {
var tagNodes = this.props.data.map(function(tag) {
var onMore = function() { this.props.onMore(tag) }.bind(this);
var onRemove = function() { this.props.onRemove(tag) }.bind(this);
return (
<Tag tag={tag} onMore={onMore} onRemove={onRemove} />
);
}.bind(this));
return (
<div className="tagList">
{ tagNodes }
</div>
);
}
});
//タグ入力フォーム
var TagForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var name = React.findDOMNode(this.refs.name).value.trim();
if (!name) return;
this.props.onSubmit(name);
React.findDOMNode(this.refs.name).value = '';
},
render: function() {
return (
<div className="tagForm">
<form onSubmit={this.handleSubmit} >
<input type="text" ref="name" autoFocus placeholder="タグを追加"/>
</form>
</div>
);
}
});
//チャートコンポーネント
var Chart = React.createClass({
createDataset: function() {
var firstDate = new Date(d3.min(this.props.data, function(d) { return d.firstDate })),
lastDate = dateFormat.parse(dateFormat(new Date()));
var dataset = this.props.data.map(function(tag, i) {
//投稿リストを集計する
var items = d3.nest()
.key(function(d) { return +groupDate(d) })
.sortKeys(function(d1, d2) { return d1 - d2 })
.rollup(function(d) { return d.length })
.entries(tag.data);
//投稿がない日付のデータを0件で埋める
var fill0 = [];
for (var date = new Date(tag.firstDate); date <= lastDate ; date.setDate(date.getDate() + 1)) {
fill0.push({
key: +groupDate(date),
values: 0
});
}
var values = d3.nest()
.key(function(d) { return d.key })
.sortKeys(function(d1, d2) { return d1 - d2 })
.rollup(function(d) { return d3.sum(d, function(d) { return d.values }) })
.entries(d3.merge([items, fill0]));
return {
key: tag.name,
values: values,
color: color(tag.index)
};
});
return dataset;
},
componentDidMount: function() {
this.showChart();
},
componentDidUpdate: function() {
this.showChart();
},
showChart: function() {
var dataset = this.createDataset();
//console.log(dataset);
nv.addGraph(function() {
var chart = this.chart = nv.models.lineChart()
.margin({left: 80, right: 40}) //Adjust chart margins to give the x-axis some breathing room.
.useInteractiveGuideline(true) //We want nice looking tooltips and a guideline!
.transitionDuration(350) //how fast do you want the lines to transition?
.showLegend(false) //Show the legend, allowing users to turn on/off line series.
.showYAxis(true) //Show the y-axis
.showXAxis(true) //Show the x-axis
.x(function(d) { return new Date(+(d.key)) })
.y(function(d) { return d.values })
;
chart.xAxis
.axisLabel("投稿日時")
.tickFormat(function(d) { return dateFormat(new Date(d)) });
chart.yAxis
.axisLabel('投稿数')
.tickFormat(d3.format(',.5d'));
d3.select(React.findDOMNode(this.refs.svg))
.datum(dataset)
.transition()
.duration(0)
.call(chart);
nv.utils.windowResize(chart.update);
}.bind(this));
},
render: function() {
return (
<div className="chart">
<svg ref="svg"></svg>
</div>
);
},
});
//メインコンポーネント
var QiitaTrend = React.createClass({
getInitialState: function() {
return { data: [] };
},
loadTagItems: function(tag) {
this.refs.mask.mask();
return new Promise(function(resolve, reject) {
$.ajax({
url: 'http://qiita.com/api/v1/tags/' + tag.name + '/items',
data: {
per_page: 100,
page: tag.pageCount + 1
},
dataType: 'json',
success: function(data) {
this.refs.mask.unmask();
resolve(data);
}.bind(this),
error: function(xhr, status, err) {
this.refs.mask.unmask();
reject({status: status, err: err});
}.bind(this)
});
}.bind(this));
},
loadFailed: function(result) {
alert(result.err + '(' + result.status + ')');
},
parseTagItems: function(tag, data) {
tag.pageCount++;
tag.data = tag.data.concat(data.map(function(item) {
return parseQiitaDate(item.created_at);
}));
tag.data.sort(function(a, b) { return +a - +b });
tag.firstDate = tag.data.length > 0 ? tag.data[0] : null;
return tag;
},
add: function(name) {
var tag = {
name: name,
pageCount: 0,
data: [],
firstDate: null,
index: this.state.data.length
};
this.load(tag);
},
load: function(tag) {
this.loadTagItems(tag).then(
function(data) {
this.parseTagItems(tag, data);
this.state.data.indexOf(tag) == -1 && this.state.data.push(tag);
this.setState({});
}.bind(this),
function(xhr, status, err) {
this.loadFailed(xhr, status, err);
}.bind(this)
);
},
remove: function(tag) {
var data = this.state.data.filter(function(d) { return d != tag });
data.forEach(function(tag, i) {
tag.index = i;
});
this.setState({ data: data });
},
render: function() {
var chart = this.state.data.length != 0 ? <Chart data={this.state.data} ref="chart"/> : null;
return (
<div className="qiitaTrend">
<Mask display={false} ref="mask"/>
<TagForm onSubmit={this.add} />
<TagList data={this.state.data} onMore={this.load} onRemove={this.remove} />
{ chart }
</div>
);
}
});
//ロードマスク
var Mask = React.createClass({
getInitialState: function() {
return { enable: this.props.enable };
},
mask: function() {
this.setState({ enable: true });
},
unmask: function() {
this.setState({ enable: false });
},
render: function() {
var display = this.state.enable ? 'block' : 'none';
return (
<div className="mask" style={{ display: display }} ref="mask">
<div>読み込み中...</div>
</div>
);
}
});
React.render(
<QiitaTrend/>,
document.getElementById('content')
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.