システムとモデリング

modelica, Julia, Design Structure Matrix, SysML, 他モデリング全般について。

DSM-Design Structure MatrixをD3.jsで描画してみる

DSM(Design Structure Matrix)はシステムのモデリング手法の1つで、要素間の関係を行列形式で表すものです。モデリング手法は各種存在しますが、DSMはその中でもグラフ理論・行列計算というしっかりした数学的バックグラウンドがあり、またシンプルな手法で汎用性が高いため製品設計からプロジェクトマネジメントまで幅広く応用でき個人的に注目しています。

DSMの例は以下のようなものです。 f:id:Otepipi:20190426223646p:plain 出典

勉強する過程でDSMを描画する方法を探していたのですが、難航しました。ヒートマップでと考えたのですが、セルに値も表示させたいとなるとやり方がわかりませんでした。JuliaではPlotsで以下のようにできるようですが、こちらの環境ではpyplotが入らず再現できませんでした。

Annotations and line widths in Plots.jl heatmaps - Visualization - JuliaLang

なので今回はJavaScriptライブラリD3.jsに似たようなものがありましたのでそれで描画してみます。 ↓の例題のほぼ丸パクリですが……

bl.ocks.org

再現したDSM

wikipediaDSM項に掲載されている以下のDSMを再現してみます。 f:id:Otepipi:20190426224207p:plain

再現結果は以下の表になります。まだまだ再現度が低いため今後も研究していかなければいけませんね。 f:id:Otepipi:20190426224321p:plain

コードは以下になりますが、JavaScripはあまり触ったことがないので内容はほぼ理解できていません。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Correlation Matrix</title>
    <link rel="stylesheet" type="text/css" href="style.css"/>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
    <div style="display:inline-block;" id="legend"></div>
    <div style="display:inline-block; float:left" id="container"></div>
    <script src="main.js"></script>

    <script>

    var correlationMatrix = [
        [0,1,0,0,0,1,0],
        [0,0,0,1,0,0,0],
        [1,0,0,0,0,0,1],
        [0,0,0,0,1,0,0],
        [0,1,0,0,0,1,0],
        [0,0,1,0,0,0,0],
        [1,0,0,0,1,0,0]
        
    ];

    var labels = ['Element A', 'Element B', 'Element C', 'Element D', 'Element E', 'Element F', 'Element G'];

    Matrix({
        container : '#container',
        data      : correlationMatrix,
        labels    : labels,
        start_color : '#ffffff',
        end_color : '#3498db'
    });

</script>
</body>

main.js

function Matrix(options) {
    var margin = {top: 50, right: 50, bottom: 100, left: 100},
        width = 350,
        height = 350,
        data = options.data,
        container = options.container,
        labelsData = options.labels,
        startColor = options.start_color,
        endColor = options.end_color;

    var widthLegend = 100;

    if(!data){
        throw new Error('Please pass data');
    }

    if(!Array.isArray(data) || !data.length || !Array.isArray(data[0])){
        throw new Error('It should be a 2-D array');
    }

    var maxValue = d3.max(data, function(layer) { return d3.max(layer, function(d) { return d; }); });
    var minValue = d3.min(data, function(layer) { return d3.min(layer, function(d) { return d; }); });

    var numrows = data.length;
    var numcols = data[0].length;

    var svg = d3.select(container).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var background = svg.append("rect")
        .style("stroke", "black")
        .style("stroke-width", "2px")
        .attr("width", width)
        .attr("height", height);

    var x = d3.scale.ordinal()
        .domain(d3.range(numcols))
        .rangeBands([0, width]);

    var y = d3.scale.ordinal()
        .domain(d3.range(numrows))
        .rangeBands([0, height]);

    var colorMap = d3.scale.linear()
        .domain([minValue,maxValue])
        .range([startColor, endColor]);

    var row = svg.selectAll(".row")
        .data(data)
        .enter().append("g")
        .attr("class", "row")
        .attr("transform", function(d, i) { return "translate(0," + y(i) + ")"; });

    var cell = row.selectAll(".cell")
        .data(function(d) { return d; })
            .enter().append("g")
        .attr("class", "cell")
        .attr("transform", function(d, i) { return "translate(" + x(i) + ", 0)"; });

    cell.append('rect')
        .attr("width", x.rangeBand())
        .attr("height", y.rangeBand())
        .style("stroke-width", 0);

    cell.append("text")
        .attr("dy", ".32em")
        .attr("x", x.rangeBand() / 2)
        .attr("y", y.rangeBand() / 2)
        .attr("text-anchor", "middle")
        .style("fill", function(d, i) { return d >= maxValue/2 ? 'white' : 'black'; })
        .text(function(d, i) { return d; });

    row.selectAll(".cell")
        .data(function(d, i) { return data[i]; })
        .style("fill", colorMap);

    var labels = svg.append('g')
        .attr('class', "labels");

    var columnLabels = labels.selectAll(".column-label")
        .data(labelsData)
        .enter().append("g")
        .attr("class", "column-label")
        .attr("transform", function(d, i) { return "translate(" + x(i) + "," + height + ")"; });

    columnLabels.append("line")
        .style("stroke", "black")
        .style("stroke-width", "1px")
        .attr("x1", x.rangeBand() / 2)
        .attr("x2", x.rangeBand() / 2)
        .attr("y1", 0)
        .attr("y2", 5);

    columnLabels.append("text")
        .attr("x", 0)
        .attr("y", y.rangeBand() / 2)
        .attr("dy", ".82em")
        .attr("text-anchor", "end")
        .attr("transform", "rotate(-60)")
        .text(function(d, i) { return d; });

    var rowLabels = labels.selectAll(".row-label")
        .data(labelsData)
      .enter().append("g")
        .attr("class", "row-label")
        .attr("transform", function(d, i) { return "translate(" + 0 + "," + y(i) + ")"; });

    rowLabels.append("line")
        .style("stroke", "black")
        .style("stroke-width", "1px")
        .attr("x1", 0)
        .attr("x2", -5)
        .attr("y1", y.rangeBand() / 2)
        .attr("y2", y.rangeBand() / 2);

    rowLabels.append("text")
        .attr("x", -8)
        .attr("y", y.rangeBand() / 2)
        .attr("dy", ".32em")
        .attr("text-anchor", "end")
        .text(function(d, i) { return d; });

    var key = d3.select("#legend")
    .append("svg")
    .attr("width", widthLegend)
    .attr("height", height + margin.top + margin.bottom);

    var legend = key
    .append("defs")
    .append("svg:linearGradient")
    .attr("id", "gradient")
    .attr("x1", "100%")
    .attr("y1", "0%")
    .attr("x2", "100%")
    .attr("y2", "100%")
    .attr("spreadMethod", "pad");

    legend
    .append("stop")
    .attr("offset", "0%")
    .attr("stop-color", endColor)
    .attr("stop-opacity", 1);

    legend
    .append("stop")
    .attr("offset", "100%")
    .attr("stop-color", startColor)
    .attr("stop-opacity", 1);

    key.append("rect")
    .attr("width", widthLegend/2-10)
    .attr("height", height)
    .style("fill", "url(#gradient)")
    .attr("transform", "translate(0," + margin.top + ")");

    var y = d3.scale.linear()
    .range([height, 0])
    .domain([minValue, maxValue]);

    var yAxis = d3.svg.axis()
    .scale(y)
    .orient("right");

    key.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(41," + margin.top + ")")
    .call(yAxis)
}

style.css

.axis text {
    font: 10px sans-serif;
}

.axis line, .axis path {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
}

今回はここまでにします。