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);