🍱
大量のデータをいい感じに面積で表現するアレをSwiftUIで実装
はじめに
上の画像をSwiftUIで実装します。上の画像は都道府県の面積を表現しています。
やり方
大きい方からいくつかを選ぶ(ここではそれを塊という)
縦と横の長い方を選ぶ(長い方の端に塊を描くので)
その塊が全体の何%か計算し、塊を描く領域を決定。
その領域内に塊内の大きい方から並べる。
残りに対して同じことをする
ポイント
このアルゴリズムのポイントはひとつ。
塊を選ぶときにいくつ選ぶか
です。私の場合は次のようにしました。
塊の中で一番小さいセル(あるいは最後に選ばれたセル)を出来るだけ正方形にする
処理としては、セルをひとつずつ増やしていって、最後に選ばれたものが(前回の最後のセルと比べて)正方形から遠のいたらその案は却下する、というやり方をします。
どれだけ正方形か比べるところは
let oldRatio = oldW / oldH
let newRatio = newW / newH
let oldRatio2 = max(oldRatio, 1.0 / oldRatio)
let newRatio2 = max(newRatio, 1.0 / newRatio)
のような感じで oldRatio2
と newRatio2
の大小を比べるやり方です。小さい方が正方形に近い。
ソースコード
まずはレイアウトのために定義された型とレイアウト計算関数
enum Direction {
case h
case v
}
class LayoutData {
var direction: Direction = .h
var content: [(index: Int, w: Double, h: Double)] = []
var child: LayoutData? = nil
}
func calculateLayout(w: Double, h: Double, data: [Double], from: Int) -> LayoutData {
let returnData = LayoutData()
let dataToArea = w * h / data[from...].reduce(0.0, +)
if w < h {
returnData.direction = .v //縦に2つ並べる。ひとつ目はいくつかの四角(横に並べる)。二つ目は子。
} else {
returnData.direction = .h
}
let mainLength = min(w, h) //個々の長方形を並べる方向がmain
var currentIndex = from //この値を増やしていく
var area = data[currentIndex] * dataToArea //グループ全体の面積(増やしていく)
var crossLength = area / mainLength //mainに直交するのがcross
// セルが正方形に近いか調べている
var cellRatio = mainLength / crossLength
cellRatio = max(cellRatio, 1.0 / cellRatio)
while currentIndex + 1 < data.count {
let newIndex = currentIndex + 1
let newArea = area + data[newIndex] * dataToArea
let newCrossLength = newArea / mainLength
var newCellRatio = data[newIndex] * dataToArea / newCrossLength / newCrossLength
newCellRatio = max(newCellRatio, 1.0 / newCellRatio)
if newCellRatio < cellRatio {
//付け足した方が正方形に近かった
currentIndex = newIndex
area = newArea
crossLength = newCrossLength
cellRatio = newCellRatio
} else {
break
}
}
switch returnData.direction {
case .h:
for i in from...currentIndex {
returnData.content.append((
index: i,
w: crossLength,
h: data[i] * dataToArea / crossLength))
}
case .v:
for i in from...currentIndex {
returnData.content.append((
index: i,
w: data[i] * dataToArea / crossLength,
h: crossLength))
}
}
if currentIndex != data.count - 1 {
switch returnData.direction {
case .h:
returnData.child = calculateLayout(
w: w - crossLength,
h: h,
data: data,
from: currentIndex + 1)
case .v:
returnData.child = calculateLayout(
w: w,
h: h - crossLength,
data: data,
from: currentIndex + 1)
}
}
return returnData
}
次にView
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
let ld = calculateLayout(
w: proxy.size.width,
h: proxy.size.height,
data: prefectureData.map({ (name: String, area: Double) in
area
}),
from: 0
)
TreeMapView(ld: ld)
}
.ignoresSafeArea()
}
}
struct TreeMapView: View {
let ld: LayoutData
var body: some View {
if ld.direction == .h {
HStack(spacing: 0.0) {
VStack(spacing: 0.0) {
ForEach(0..<ld.content.count, id: \.self) { i in
Rectangle()
.foregroundColor(randomColor())
.frame(width: ld.content[i].w,
height: ld.content[i].h)
.overlay {
Text(prefectureData[ld.content[i].index].name)
}
}
}
if let child = ld.child {
TreeMapView(ld: child)
}
}
} else {
VStack(spacing: 0.0) {
HStack(spacing: 0.0) {
ForEach(0..<ld.content.count, id: \.self) { i in
Rectangle()
.foregroundColor(randomColor())
.frame(width: ld.content[i].w,
height: ld.content[i].h)
.overlay {
Text(prefectureData[ld.content[i].index].name)
}
}
}
if let child = ld.child {
TreeMapView(ld: child)
}
}
}
}
func randomColor() -> Color {
Color(hue: Double.random(in: 0.0...1.0),
saturation: Double.random(in: 0.08...0.18),
brightness: Double.random(in: 0.90...1.0))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
最後に都道府県データ
let prefectureData: [(name: String, area: Double)] =
[
(name: "北海道", area: 83457.00),
(name: "岩手県", area: 15278.89),
(name: "福島県", area: 13782.76),
(name: "長野県", area: 13562.23),
(name: "新潟県", area: 12583.83),
(name: "秋田県", area: 11636.28),
(name: "岐阜県", area: 10621.17),
(name: "青森県", area: 9644.55),
(name: "山形県", area: 9323.46),
(name: "鹿児島県", area: 9188.82),
(name: "広島県", area: 8479.70),
(name: "兵庫県", area: 8396.16),
(name: "静岡県", area: 7780.50),
(name: "宮崎県", area: 7735.99),
(name: "熊本県", area: 7404.79),
(name: "宮城県", area: 7285.77),
(name: "岡山県", area: 7113.23),
(name: "高知県", area: 7105.16),
(name: "島根県", area: 6707.96),
(name: "栃木県", area: 6408.28),
(name: "群馬県", area: 6362.33),
(name: "大分県", area: 6339.74),
(name: "山口県", area: 6114.09),
(name: "茨城県", area: 6095.72),
(name: "三重県", area: 5777.31),
(name: "愛媛県", area: 5678.33),
(name: "愛知県", area: 5165.12),
(name: "千葉県", area: 5156.61),
(name: "福岡県", area: 4978.51),
(name: "和歌山県", area: 4726.29),
(name: "京都府", area: 4613.21),
(name: "山梨県", area: 4465.37),
(name: "富山県", area: 4247.61),
(name: "福井県", area: 4189.88),
(name: "石川県", area: 4185.67),
(name: "徳島県", area: 4146.74),
(name: "長崎県", area: 4105.47),
(name: "滋賀県", area: 4017.36),
(name: "埼玉県", area: 3798.08),
(name: "奈良県", area: 3691.09),
(name: "鳥取県", area: 3507.28),
(name: "佐賀県", area: 2439.65),
(name: "神奈川県", area: 2415.85),
(name: "沖縄県", area: 2276.49),
(name: "東京都", area: 2188.67),
(name: "大阪府", area: 1899.28),
(name: "香川県", area: 1876.55),
]
入れ子によって構造が肥大化するのが気になりますが、そのそも人間が見て認識できる程度のデータの個数であろうということでよしとしました。
何か指摘があったらコメントよろしくお願いします。
調べたらこの手法はツリーマップというらしい。
Discussion