朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

すっきり表組み・見出しを縦に回転

はじめまして、開発部の8lukaです。小ネタですが便利なテクニックのご紹介です。

はじめに:大きな対応表を画面におさめたい

たとえば、商品とその構成品目。たとえば、ロールとその権限。たとえば、飲み会の参加者と日程候補。 こういった対応関係は、表に可視化することができます。

f:id:an8luka:20190225142224p:plain
対応表の例 - 商品とその構成品目(Googleスプレッドシート - Chrome72)

対応関係が整列されて理解しやすい反面、列の数が増えてくるとあっという間に横幅が膨れて、画面におさまらなくなったりレイアウトが崩れたりしてしまいます。

f:id:an8luka:20190225142348p:plain
横幅が広がってしまった対応表の例(Googleスプレッドシート - Chrome72)

見出しを短くして詳細を別の場所に書く手もありますが、見る場所が分散して不親切な表になってしまうかもしれません。

列見出しを縦に回転させる

このようなとき、たとえばExcelやGoogle スプレッドシートでは列見出しを縦に回転させて横幅を圧縮できます。

f:id:an8luka:20190225143136p:plain
縦に回転させる方法(Googleスプレッドシート - Chrome72)
f:id:an8luka:20190225143411p:plain
縦に回転させた結果(Googleスプレッドシート - Chrome72)

横幅を2.7倍、面積比で見ても1.6倍ほど小さくすることができました。

ようやく本題です。同じことをHTMLのtableで実現する簡単な方法はあるのでしょうか?

CSSのtransform

CSSのtransformプロパティを利用すると、要素を簡単に回転できます。これが利用できそうです。手元で試行錯誤してもなかなかうまくいかなかったのですが ブラウザーの対応 の表がまさにこれで実現されていることに気付き、 同様の方法で縦に回転した列見出しを作ることができました。

f:id:an8luka:20190226163624p:plain
CSS transformプロパティによる回転(結果 - Chrome72)

以下、ソースコードです。

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8" />
    <title>CSS transformプロパティによる回転</title>
    <style>
    table {
        border-collapse: collapse;
        border: solid black 2px;
    }
    td {
        border: solid black 1px;
        padding: 5px;
    }
    tr.header-row td {
        height: 200px;
        vertical-align: bottom;
    }
    .vertical-text {
        display: inline-block;
        transform: rotate(-90deg);
        transform-origin: 10px 10px;
        width: 20px; height: 20px;
        padding: 0;
        white-space: nowrap;
    }
    </style>
</head>
<body>

    <table>
        <tr class="header-row">
            <td></td>
            <td><span class="vertical-text">品目A(モバイルルータA)</span></td>
            <td><span class="vertical-text">品目B(共通ACアダプタ)</span></td>
            <td><span class="vertical-text">品目C(A専用クレードル)</span></td>
            <td><span class="vertical-text">品目D(モバイルルータD)</span></td>
            <td><span class="vertical-text">品目E(D専用クレードル)</span></td>
        </tr>
        <tr> <td>商品1(品目A 単品)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品2(品目A+品目B セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品3(品目A+品目C セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品4(品目A+品目B+品目C セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品5(品目D 単品)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品6(品目D+品目B セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品7(品目D+品目E セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品8(品目D+品目B+品目E セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
    </table>
</body>
</html>

幅や高さの数字が埋まっているので、内容がほとんど決まっていてあまり変わらないような場合にはこの方法が便利そうです。

回転を実現しているのは tarnsform: rotate(-90deg); だけなのですが、少々調整が必要になります。特に、以下2点が重要です:

  • white-space: nowrap; で折り返しを禁止します。 これがないと「折り返された状態のテキスト」を回転させることになってしまい、望む表示が得られません。
  • 回転後の内容が十分入る大きさのボックスを、外側の要素で確保します。(この場合は tdheight: 200px;

レンダリング結果とともにまとめると、次のとおりです:

CSS Chrome72でのレンダリング結果
transform: rotate による回転前 transform: rotate による回転後
- - f:id:an8luka:20190311152651p:plain f:id:an8luka:20190311152338p:plain
f:id:an8luka:20190311155415p:plain f:id:an8luka:20190311155516p:plain
f:id:an8luka:20190311154007p:plain f:id:an8luka:20190311154051p:plain

インラインSVG

2019年現在、IE11を含む主要ブラウザはSVG画像の表示に対応しています。しかも、HTMLの中に直接ソースを記述できます。こちらも便利な予感がするので実験してみましょう。

f:id:an8luka:20190314235112p:plain
SVGによるテキストの回転 - Chrome72

テキストの回転はできましたが、以下のソースでもわかるとおり width height viewBoxと延べ6個の数値をそれぞれのsvg要素に指定する必要があります。これではCSSより扱いやすいとは言い難いです。

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8" />
    <title>SVGによるテキストの回転</title>
    <style>
    table {
        border-collapse: collapse;
        border: solid black 2px;
    }
    td {
        border: solid black 1px;
        padding: 5px;
    }
    tr.header-row td {
        vertical-align: bottom;
    }
    </style>

</head>
<body>

    <table>
        <tr class="header-row">
            <td>
                <svg width="20" height="200" viewBox="-18 -200 20 200">
                    <text transform="rotate(-90)">回転したテキストを</text>
                </svg>
            </td>
            <td>
                <svg width="20" height="200" viewBox="-18 -200 20 200">
                    <text transform="rotate(-90)">SVGで描いてみたよ</text>
                </svg>
            </td>
        </tr>
    </table>

</body>
</html>

JavaScriptにサイズを設定させる

内容のテキストに合った数値をJavaScriptに計算してもらいましょう。

まずは、SVG要素を生成する関数を定義します。ファイル名は何でもよいですが、ここでは verticaltextsvg.js とします。

// このプログラムは CC0 のもとパブリックドメインです:
// https://creativecommons.org/publicdomain/zero/1.0/legalcode.ja
var createVerticalTextSvg = function() {

    // この関数が呼ばれたscript要素
    var script_e = document.scripts[document.scripts.length - 1];
    // その data-text 属性を拾う
    // 普通に引数で渡してもよいけれど、属性から拾う方がXSS対策の悩みを減らせる
    var content_t = script_e.getAttribute("data-text");

    // SVGの要素を生成して
    var text_e = document.createElementNS("http://www.w3.org/2000/svg", "text");
    text_e.textContent = content_t;
    var svg_e = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg_e.appendChild(text_e);

    // そのバウンディングボックスを取りたいのだが、まずは
    // これを済ませないと getBBox が有効な値を返さない
    script_e.parentNode.insertBefore(svg_e, script_e);
    // SVGさえできてしまえば script 要素はどこからも使われないので消しておく
    script_e.parentNode.removeChild(script_e);

    // 回転行列を定義して text要素に適用する
    // rotate 指定でもよいけれど、この後の再利用の都合で行列になっている方が便利
    var rot_m = [ 0, -1, 1, 0, 0, 0 ];
    text_e.setAttribute("transform", "matrix(" + rot_m.join(",") + ")");

    // 満を持してバウンディングボックスを取得する
    // ただし、これは transform 適用前の値
    var bb = text_e.getBBox();

    // なのでバウンディングボックスの4頂点を自力で回転させる
    var rot_vs = [
        [ bb.x,            bb.y             ],
        [ bb.x + bb.width, bb.y             ],
        [ bb.x + bb.width, bb.y + bb.height ],
        [ bb.x,            bb.y + bb.height ]
    ].map(function(v) {
        return [
            rot_m[0]*v[0] + rot_m[2]*v[1] + rot_m[4],
            rot_m[1]*v[0] + rot_m[3]*v[1] + rot_m[5] ];
    });

    // 新しいバウンディングボックスを
    // svg の width height viewBox に設定する
    var rot_xs = rot_vs.map( function(v) { return v[0]; } );
    var rot_ys = rot_vs.map( function(v) { return v[1]; } );

    var rot_min_x = Math.min.apply(null, rot_xs);
    var rot_max_x = Math.max.apply(null, rot_xs);
    var rot_min_y = Math.min.apply(null, rot_ys);
    var rot_max_y = Math.max.apply(null, rot_ys);

    var rot_w = rot_max_x - rot_min_x;
    var rot_h = rot_max_y - rot_min_y;

    svg_e.setAttribute("width", rot_w);
    svg_e.setAttribute("height", rot_h);
    svg_e.setAttribute("viewBox", [
        rot_min_x, rot_min_y,
        rot_w, rot_h ].join(","));

    return svg_e;
};

ポイントが2つあります。

あとは呼び出すだけです。定義部でちょっと頑張った分、呼び出しは簡単です。

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8" />
    <title>JS + SVGによるテキストの回転</title>
    <style>
    table {
        border-collapse: collapse;
        border: solid black 2px;
    }
    td {
        border: solid black 1px;
        padding: 5px;
    }
    tr.header-row td {
        vertical-align: bottom;
    }
    </style>
    <script src="verticaltextsvg.js"></script>
</head>
<body>

    <table>
        <tr class="header-row">
            <td></td>
            <td><script data-text="品目A(モバイルルータA)">createVerticalTextSvg()</script></td>
            <td><script data-text="品目B(共通ACアダプタ)">createVerticalTextSvg()</script></td>
            <td><script data-text="品目C(A専用クレードル)">createVerticalTextSvg()</script></td>
            <td><script data-text="品目D(モバイルルータD)">createVerticalTextSvg()</script></td>
            <td><script data-text="品目E(D専用クレードル)">createVerticalTextSvg()</script></td>
        </tr>
        <tr> <td>商品1(品目A 単品)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品2(品目A+品目B セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品3(品目A+品目C セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品4(品目A+品目B+品目C セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品5(品目D 単品)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品6(品目D+品目B セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品7(品目D+品目E セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
        <tr> <td>商品8(品目D+品目B+品目E セット)</td>
             <td></td> <td></td> <td></td> <td></td> <td></td>
        </tr>
    </table>
</body>
</html>

ブラウザで見ると、中身のテキストに合わせてぴったりの表になっているのがわかります。

f:id:an8luka:20190304123442p:plain
JS + SVGによるテキストの回転 - Chrome72

おわりに

以上、表の見出しを縦に回転させて横幅を抑える方法のご紹介でした。 実はこの記事にも使われていますので、よかったら探してみてくださいね。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。