縦横に要素を均等配置するために最適な行数と列数を計算する方法


##この記事について
任意の縦横比のエリアに、任意の数の要素を均等配置する方法を解説します。
この説明だけでは意味がわからないと思うのでまずは↓の動画を見てください。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">「ソーシャルディスタンス見える化くん」を作りました<a href="https://t.co/k23TjWGQTC">https://t.co/k23TjWGQTC</a> <a href="https://t.co/4MT6yIIXW8">pic.twitter.com/4MT6yIIXW8</a></p>&mdash; coboco (@_coboco) <a href="https://twitter.com/_coboco/status/1255024597516185602?ref_src=twsrc%5Etfw">April 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

エリアのサイズと人数を入力すると、エリア内に要素（人）を均等配置した結果が可視化できるサイトです。
非常にシンプルな機能ではあるのですが・・・
要素を均等配置するための行数と列数を決定する方法にとても悩みました。
同じような計算に悩んでいる人のために、計算方法を書き残しておこうと思います。

##前提条件
まず、エリアを二次元表として捉えます。
仮に、横幅20m・縦幅10m・人数30人だった場合、4行8列が最適なサイズとなります。
![qiita用.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152304/a5ddbaf4-0206-80b8-fc11-c79f204e2d03.png)
この例はこれ以降も使っていきます。

###変数定義
各変数は下記のように定義します。（）内は、上記の例における数値を表しています。

・エリア横幅：W（20）
・エリア縦幅：H（10）
・要素数：N（30）
・列数：x（8）
・行数：y（4）

W,H,Nはあらかじめ決定している状態で、xとyの値を求めようとしています。

####前提条件1
まず、エリアのサイズ比と行列の数の比率は同じ値になるのが理想形なので、下記の式で表現できます。

```math
W:H = x:y
```

数式が出てくるだけで体が拒絶反応を起こしてしまう方（私）のために、定数による例も記載しておきます。（以降、併記します）

```math
20:10 = 8:4
```

####前提条件2
行数と列数を掛けた結果は要素数と同じか、少し大きい数になります。

```math
\begin{align}
xy &\geqq N \\
8 \times 4 &\geqq 30
\end{align}
```

例における要素数は30人ですが、8×4 は 32 になりますね。
つまり2枠分の余りスペースが出るということです。

ただしここからの計算のしやすさのために、いったん不等号を等号に置き換えたうえで計算していきます。

```math
xy = N
```

##計算方法
###列数xを求める
横幅W, 縦幅H, 要素数Nはあらかじめ決定している状態ですので、
前提条件2で定義した式に、例で使用した `要素数N=30` を代入します。

```math
\begin{align}
xy &= N \\
xy &= 30
\end{align}
```

xを移項して、yの値を求めます。

```math
\begin{align}
y &= \frac{N}{x} \\
y &= \frac{30}{x}
\end{align}
```

次に、前提条件1で定義した式を使います。（再掲）

```math
\begin{align}
W:H &= x:y
\end{align}
```

`横幅W=20`, `縦幅H=10`, そして先ほど求めた `y` の値を代入します。

```math
\begin{align}
W:H &= x:\frac{N}{x} \\
20:10 &= x:\frac{30}{x}
\end{align}
```


[比例式](https://ja.wikipedia.org/wiki/%E6%AF%94%E4%BE%8B%E5%BC%8F) により、以下の式が成立します。

```math
\begin{align}
Hx &= \frac{WN}{x} \\
10x &= \frac{600}{x}
\end{align}
```

右辺からxを消したいので、両辺に `x` を掛けます。


```math
\begin{align}
Hx^2 &= WN \\
10x^2 &= 600
\end{align}
```


続いて左辺の係数を消したいので、両辺に `1/H`（= `1/10`）を掛けます。

```math
\begin{align}
x^2 &= \frac{WN}{H} \\
x^2 &= 60
\end{align}
```


さらに左辺のべき乗の指数を消したいので、両辺の平方根を求めます。
※ xは列数なのでマイナス値になることはありえません。

```math
\begin{align}
x &= \sqrt{\frac{WN}{H}} \\
x &= \sqrt{60} \\
\end{align}
```

列数xの値が求まりました。（小数点以下第3位を四捨五入）

```math
x = 7.75
```

###行数yを求める
前提条件2で定義した式は下記のようになっていました。（再掲）

```math
xy = N

```

これにより、行数yは、`要素数N / 列数x` で求められることがわかります。

```math
\begin{align}
y &= \frac{N}{x} \\
y &= 30 \div 7.75
\end{align}
```

行数yの値が求まりました。（小数点以下第3位を四捨五入）

```math
y = 3.87
```

###行数と列数を最適化する

さて、行数と列数が求められましたが、小数点が含まれた値になっています。行数と列数は整数でないとおかしいですよね。
これは、前提条件2で不等号を等号に置き換えた影響です。
そこで行数と列数の片方または両方を切り上げし、整数に直してあげる必要があります。

一番簡単な方法は、片方を切り捨て及び切り上げした2値を求め、もう片方を求める方法でしょうか。
（もっと簡単な方法があれば教えてください）

今回は `x = 7.75` でしたので、`x = 7` または `x = 8` と置きます。
次にそれぞれの場合における、yの値を求めます。
`xy ≧ N` を満たすために、yの値は必ず切り上げします。

x=7の場合

```math
\begin{align}
y &= 30 \div 7 \\
& = 4.28571… \\
& = 5
\end{align}
```

x=8の場合

```math
\begin{align}
y &= 30 \div 8 \\
& = 3.75 \\
& = 4
\end{align}
```

`x = 7 , y = 5` または `x = 8 , y = 4` のどちらかの組み合わせが良さそう、ということがわかります。
それぞれの合計要素数は `7 * 5 = 35` , `8 * 4 = 32` となります。
配置したい要素数N=30により近い値を選んだほうが無駄なスペースができませんので、後者を採用したほうが良い、という結論になります。
もし合計要素数が同じ場合は、行数と列数の比率がエリアの縦横比に近いほうを採用する、とすれば良いでしょう。

##ソースコード
実装すると、下記のようなコードになります。

```javascript
// 円を何行何列で描画するのが最適かを求める
function calcMatrix(width, height, people) {
  // 最適列数を計算
  const baseCol = Math.sqrt(width * people / height);
  
  // 最適列数付近の整数を2つ取得
  const tmpCol1 = Math.floor(baseCol) == 0 ? 1 : Math.floor(baseCol);
  const tmpCol2 = Math.ceil(baseCol);
  
  // 最適列数付近の整数から、行数の組み合わせを算出（人数が入り切るように小数点以下繰り上げ）
  const tmpRow1 = Math.ceil(people / tmpCol1);
  const tmpRow2 = Math.ceil(people / tmpCol2);

  // 2パターンの行列の合計要素数が少ない方を採用
  const matrix1 = tmpRow1 * tmpCol1;
  const matrix2 = tmpRow2 * tmpCol2;    
  if (matrix1 < matrix2) {
    return [tmpRow1, tmpCol1];
  } else if (matrix1 > matrix2) {
    return [tmpRow2, tmpCol2];
  } else {
    // 合計要素数が同じ場合は、空間の比率に近い方を採用
    const roomRatio = width / height;
    const tmpRatio1 = tmpCol1 / tmpRow1;
    const tmpRatio2 = tmpCol2 / tmpRow2;
    const diff1 = Math.abs(roomRatio - tmpRatio1);
    const diff2 = Math.abs(roomRatio - tmpRatio2);

    if (diff1 < diff2) {
      return [tmpRow1, tmpCol1];
    } else {
      return [tmpRow2, tmpCol2];
    }
  }
}
```