Simple Converter Pattern in KO
HTML
<script src="http://sukobuto.com/cdn/knockout-es5.min.js"></script>
<h1>SVG with Knockout-ES5</h1>
<a href="http://vuejs.org/examples/svg.html">Vue.js SVG Example</a> を Knockout-ES5 に移植しました。
<p>
ViewModel から View の要件を取り除き、View に押し込めるために Converter (window.vc) を作りました。<br/>
Knockout では式をバインドできるので、「変換しつつバインド」することができます。
</p>
<script type="text/html" id="porygraph-template">
<svg width="200" height="200">
<polygon data-bind="attr: { points: vc.stats2Points(stats) }"></polygon>
<circle class="chartbase" cx="100" cy="100" r="80"></circle>
<circle class="chartbase light" cx="100" cy="100" r="60"></circle>
<circle class="chartbase" cx="100" cy="100" r="40"></circle>
<circle class="chartbase light" cx="100" cy="100" r="20"></circle>
<!--ko foreach: stats-->
<line class="chartbase" x1="100" y1="100"
data-bind="attr: {
x2: vc.value2Point(100, $index(), $parent.stats.length).x,
y2: vc.value2Point(100, $index(), $parent.stats.length).y,
}"></line>
<text data-bind="attr: {
x: vc.value2Point(value, $index(), $parent.stats.length).x - 4,
y: vc.value2Point(value, $index(), $parent.stats.length).y + 4
}, text: label"></text>
<!--/ko-->
</svg>
</script>
<div id="demo" data-bind="click: function(d, e) { someText = vc.click2Text(e) }">
<div class="chart" data-bind="template: { name: 'porygraph-template', data: $data }"></div>
<div class="chart small" data-bind="template: { name: 'porygraph-template', data: $data }"></div>
<div data-bind="foreach: stats">
<div>
<label data-bind="text: label"></label>
<input type="range" data-bind="value: value" min="0" max="100" />
<span data-bind="text: value"></span>
<button data-bind="click: $parent.remove">X</button>
</div>
</div>
<button data-bind="click:...
CSS
body {
font-family: Helvetica Neue, Arial, sans-serif;
}
.chart {
display: inline-block;
width: 200px;
height: 200px;
}
.chart.small {
-webkit-transform: scale(0.7);
-ms-transform: scale(0.7);
transform: scale(0.7);
}
polygon {
fill: rgba(255, 136, 136, 0.5);
stroke: #f88;
stroke-width: 2px;
}
line.chartbase {
stroke: #999;
opacity: .7;
}
circle.chartbase {
fill: transparent;
stroke: #999;
opacity: .7;
}
circle.chartbase.light {
opacity: .3;
}
text {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 10px;
fill: #666;
}
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
JavaScript
// View Converter
window.vc = (function() {
var root = this;
// stat の値からスターチャートの頂点座標に変換
root.value2Point = function(value, index, total) {
console.log(arguments);
var x = 0,
y = -value * 0.8,
angle = Math.PI * 2 / total * index,
cos = Math.cos(angle),
sin = Math.sin(angle),
tx = x * cos - y * sin + 100,
ty = x * sin + y * cos + 100
return {
x: tx,
y: ty
}
}
// stat の配列からスターチャートのパス頂点情報に変換
root.stats2Points = function(stats) {
return stats.map(function(stat) {
var point = root.value2Point(stat.value, stats.indexOf(stat), stats.length);
return point.x + ',' + point.y;
}).join(' ');
};
// クリックイベントからてきとうなテキストに変換
root.click2Text = function(e) {
return 'クリック位置 x=' + e.offsetX + ' y=' + e.offsetY;
};
return root;
})();
function AppViewModel() {
var self = this
, statsObs
;
self.stats = [];
self.statInput = "";
self.someText = "";
self.magnified = false;
ko.track(self);
self.addStat = function(data) {
self.stats.push(new StatViewModel(data.label, data.value));
}
self.createStat = function() {
self.stats.push(new StatViewModel(self.statInput, 100));
}
self.reverse = function() {
self.stats.reverse();
}
self.remove = function(stat) {
self.stats.remove(stat);
}
}
function StatViewModel(label, value) {
var self = this;
self.label = label;
self.value = value;
ko.track(self);
}
App = new AppViewModel();
ko.applyBindings(window.App);
[
{ label: 'A', value: 57 },
{ label: 'B', value: 77 },
{ label: 'C', value: 70 },
{ label: 'D', value: 100 },
{ label: 'E', value: 71 },
{ label: 'F', value: 77 }
].forEach(App.addStat);