yoppa.org


芸大 – 情報編集(Web) 2014

Processingによるデータの可視化 – 時系列のデータを可視化する

今日の目標

  • 時系列のデータファイル(タブ区切りのテキスト)を読みこみ、Processingで可視化する
  • 「ビジュアライジング・データ」の4章、「時系列」のサンプルプログラムを解析
  • まずは普通にグラフ化する手法を学ぶ

Visualizing Dataのサンプルプログラムをダウンロード

  • 書籍「ビジュアライジング・データ」のサンプルプログラム一式をダウンロードする
  • 今日の内容のベースとなる、時系列のデータをプロットするプログラムもこの中に収録されている
  • ダウンロードURL http://benfry.com/writing/archives/3

シンプルなプロット表示を理解する

時系列のデータを点の連なりで表現

  • 「ch04-milkteacoffee」→「figure_01_just_points」を開く
  • まずは実行してみる
  • 点によるシンプルなプロットが表示される

  • このプロットは、dataフォルダ内にある、tab区切りのデータファイルを読み込んでいる
  • データの内容は下記のとおり

プログラムのおおまかな構造

  • プログラムの構造 – メインのプログラム(figure_01_just_points.pde)と、FloatTableクラス(FloatTable.pde)から構成
  • メインプログラム – FloatTableクラスを利用してデータを読み込み、データをプロットする点を描画
  • FloatTableクラス – 表形式のデータファイルを読み込んで、その値を利用しやすい形に変換する。また、最大値や最小値の算出などデータを扱うための様々な便利な機能を備えている

FloatTableクラス

  • FloatTableクラスは、tsv(タブ区切り)形式の表データをファイルから読み込み、データの最大値と最小値を求めたり、余計なデータを除去したりと、様々な機能を内包している
  • FloatTableクラスのプロパティ(属性)とメソッド(機能)をまとめてみる
  • プロパティ(属性)
    • rowCount – 行の数 (横)
    • columnCount – 列の数 (縦)
    • data[][] – 読み込まれたデータ data[行][列]
    • rowNames[] – 行につけられたラベル
    • columnNames[] – 列につけられたラベル
  • メソッド
    • FloatTable(String filename) – コンストラクタ。指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
    • void scrubQuotes(String[] array) – データから、引用符(")を除去する
    • int getRowCount() – 行の数を数える
    • String getRowName(int rowIndex) – 指定した行の名前を返す
    • String[] getRowNames() – 全ての行の名前を返す
    • int getRowIndex(String name) – 行の名前から行番号を返す
    • int getColumnCount() – 列の数を数える
    • String getColumnName(int colndex) – 指定した列の名前を返す
    • String[] getColumnNames() – 全ての列の名前を返す
    • float getFloat(int rowIndex, int col) – 指定した行と列の値をFloat型で返す
    • boolean isValid(int row, int col) – 指定した行と列の値が存在するかどうかを調べる
    • float getColumnMin(int col) – 指定した列の最小値を求める
    • float getColumnMax(int col) – 指定した列の最大値を求める
    • float getRowMin(int col) – 指定した行の最小値を求める
    • float getRowMax(int col) – 指定した行の最大値を求める
    • float getTableMax() – 表全体の最大値を求める
  • コメントを付加したソースコードは下記のとおり

[code language=”java”]
//ファイルの最初の行は、各列の名前を示したヘッダーでなければならない
//最初の列は、各行のタイトルでなくてはならない
//その他の値は、全てfloat型の値であると理解される
//例えば、getFloat(0, 0)と指定すると、最初の行の一番始めのコラムの値が返される
//ファイルは、テキスト形式、タブ区切りで書くこと
//空白の行は無視される
//空白文字は無視される

class FloatTable {
int rowCount;
int columnCount;
float[][] data;
String[] rowNames;
String[] columnNames;

//コンストラクタ
//指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
FloatTable(String filename) {
String[] rows = loadStrings(filename);

String[] columns = split(rows[0], TAB);
columnNames = subset(columns, 1); //一番左上のラベルは無視
scrubQuotes(columnNames);
columnCount = columnNames.length;

rowNames = new String[rows.length-1];
data = new float[rows.length-1][];

//最初の行はヘッダーなので無視して、次の行から読みこむ
for (int i = 1; i < rows.length; i++) {
if (trim(rows[i]).length() == 0) {
continue; //空白行は無視する
}
if (rows[i].startsWith("#")) {
continue; //コメント行(#のついた行)は無視する
}

//タブ区切りでデータを読み込む
String[] pieces = split(rows[i], TAB);
//引用符を除去
scrubQuotes(pieces);

//行のタイトル(最初の列)をコピー
rowNames[rowCount] = pieces[0];
//最初の列(タイトル)は無視して、次の列からデータをdata配列にコピー
data[rowCount] = parseFloat(subset(pieces, 1));

//行のカウントを1つ増加
rowCount++;
}
//必要であれば、data配列の大きさをリサイズ
data = (float[][]) subset(data, 0, rowCount);
}

//データから引用符を除去
void scrubQuotes(String[] array) {
for (int i = 0; i < array.length; i++) {
if (array[i].length() > 2) {
if (array[i].startsWith("\&quot;") && array[i].endsWith("\&quot;")) {
array[i] = array[i].substring(1, array[i].length() – 1);
}
}
//ダブルクォートをシングルクォートに
array[i] = array[i].replaceAll("&quot;\&quot;", "\&quot;");
}
}

//行の数を数える
int getRowCount() {
return rowCount;
}

//指定した列番号の行の名前を返す
String getRowName(int rowIndex) {
return rowNames[rowIndex];
}

//全ての行の名前を返す
String[] getRowNames() {
return rowNames;
}

//行の名前から行番号を返す
//見つからなかった場合は、-1を返す
int getRowIndex(String name) {
for (int i = 0; i < rowCount; i++) {
if (rowNames[i].equals(name)) {
return i;
}
}
//println("No row named ‘" + name + "’ was found");
return -1;
}

//列の数を数える
int getColumnCount() {
return columnCount;
}

//指定した列の名前を返す
String getColumnName(int colIndex) {
return columnNames[colIndex];
}

//全ての列の名前を返す
String[] getColumnNames() {
return columnNames;
}

//指定した行と列の値をFloat型で返す
float getFloat(int rowIndex, int col) {
//異常な値を引数が指定されていた再には、例外処理をしている
if ((rowIndex < 0) || (rowIndex >= data.length)) {
throw new RuntimeException("There is no row " + rowIndex);
}
if ((col < 0) || (col >= data[rowIndex].length)) {
throw new RuntimeException("Row " + rowIndex + " does not have a column " + col);
}
return data[rowIndex][col];
}

//指定した行と列の値が存在するかどうかを調べる
boolean isValid(int row, int col) {
if (row < 0) return false;
if (row >= rowCount) return false;
//if (col >= columnCount) return false;
if (col >= data[row].length) return false;
if (col < 0) return false;
return !Float.isNaN(data[row][col]);
}

//指定した列の最小値を求める
float getColumnMin(int col) {
float m = Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
return m;
}

//指定した列の最大値を求める
float getColumnMax(int col) {
float m = -Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
return m;
}

//指定した行の最小値を求める
float getRowMin(int row) {
float m = Float.MAX_VALUE;
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
return m;
}

//指定した行の最大値を求める
float getRowMax(int row) {
float m = -Float.MAX_VALUE;
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
return m;
}

//表全体の最小値を求める
float getTableMin() {
float m = Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
}
return m;
}

//表全体の最大値を求める
float getTableMax() {
float m = -Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
}
return m;
}
}
[/code]

メインプログラムの構造

  • メインプログラム(figure_01_just_points.pde)では、このFloatTableクラスを活用して、時系列のデータを点によってプロットしている
  • void setup()
    • 画面の初期設定
    • タブ区切りのデータファイル、"milk-tea-coffe.tsv"をデータとして、FloatTableクラスをインスタンス化 → data
    • データを読み込んだFloatTableのインスタンスdataを利用して、初期化に必要な値を取得
    • 年 – 行の名前から
    • プロットの開始年 – 行の最小値
    • プロットの終了年 – 行の最大値
    • プロットする画面の領域を計算
  • void draw()
    • プロット画面の背景を生成
    • drawDataPoints() 関数を呼び出し、読み出すコラムは0
  • void drawDataPoints()
    • データの行数を計算
    • 指定したコラムの値を取り出し
    • 画面内に納まるように、値を整える(map関数)
    • 値を整えた座標に点を描画
  • map() 関数
    • map(値, 変換前の値の最小値, 変換前の値の最大値, 変換後の値の最小値, 変換後の値の最大値)
    • 数値の範囲を別の範囲に変換する
  • 例:プロットするデータの最小値が dataMin、最大値が dataMax のときに、y座標 plotY2 から plotY1 の範囲にちょうど納まるように値 value を変換する
    • map(value, dataMin, dataMax, plotY2, plotY1);

[code language=”java”]
FloatTable data; //FloatTableクラスのインスタンス
float dataMin, dataMax; //表示するデータの最大値と最小値
float plotX1, plotY1; //プロットする画面のX座標とY座標の最小値
float plotX2, plotY2; //プロットする画面のX座標とY座標の最大値
int yearMin, yearMax; //データの年の最小値と最大値
int[] years; //年の配列

void setup() {
size(720, 405);

//読み込むデータファイルを指定して、FloatTableクラスをインスタンス化
data = new FloatTable("milk-tea-coffee.tsv");
//列の名前(年)を配列yearsに読み込む
years = int(data.getRowNames());
//最小の年は、years配列の先頭
yearMin = years[0];
//最大の年は、years配列の最後
yearMax = years[years.length – 1];

//データの最小値は0に設定
dataMin = 0;
//全ての数値から最大値を求める
dataMax = data.getTableMax();

//プロットする画面の座標の最大値と最小値を設定
plotX1 = 50;
plotX2 = width – plotX1;
plotY1 = 60;
plotY2 = height – plotY1;

smooth();
}

void draw() {
//画面サイズを指定
background(224);

//プロットするエリアを白い四角形で表示
fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);

//最初の列(milk)のデータをプロット
strokeWeight(5);
stroke(#5679C1);
drawDataPoints(0);
}

//点の並びでデータを表示する関数
//引数には表示したいデータの列を入れる
void drawDataPoints(int col) {
//行の数(=データの数)を数える
int rowCount = data.getRowCount();
//データの数だけくりかえし
for (int row = 0; row < rowCount; row++) {
//もし正しい数値だったら(データの範囲が正しければ)
if (data.isValid(row, col)) {
//行と列を指定して、データの数値をとりだす
float value = data.getFloat(row, col);
//プロットする画面の幅(x座標)にちょうど納まるように、値を変換
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
//プロットする画面の高さ(y座標)にちょうど納まるように、値を変換
float y = map(value, dataMin, dataMax, plotY2, plotY1);
//変換した値を座標にして、点を描画
point(x, y);
}
}
}
[/code]

情報を表示

  • 現在表示しているデータの内容や数値を文字や図形で記述する
  • 表示しているデータの内容 → Milk
  • 垂直方向の目盛(消費量)
  • 垂直方向の消費量の数値
  • 水平方向の目盛(年)
  • 水平方向の年の値
  • ラベル → Gallons cosumed per caita, Year
  • キーボードで”]”を入力すると次の列のデータに変更
  • キーボードで”[“を入力すると前の列のデータに変更

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;
int volumeIntervalMinor = 5;

PFont plotFont;

void setup() {
size(720, 405);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = 0;
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

plotX1 = 120;
plotX2 = width – 80;
labelX = 50;
plotY1 = 60;
plotY2 = height – 70;
labelY = height – 25;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(224);

fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);

//タイトルを表示
drawTitle();
//それぞれの軸のラベル(消費量/年)を表示
drawAxisLabels();
//横軸の値(年)を表示
drawYearLabels();
//縦軸の値(消費量)を表示
drawVolumeLabels();

stroke(#5679C1);
strokeWeight(5);
//指定した列(currentColumn)のデータをプロット
drawDataPoints(currentColumn);
}

//タイトルを表示
void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

//それぞれの軸のラベル(消費量/年)を表示
void drawAxisLabels() {
fill(0);
textSize(13);
textLeading(15);

textAlign(CENTER, CENTER);
text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
textAlign(CENTER);
text("Year", (plotX1+plotX2)/2, labelY);
}

//横軸の値(年)を表示
void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER, TOP);

// Use thin, gray lines to draw the grid
stroke(224);
strokeWeight(1);

for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + 10);
line(x, plotY1, x, plotY2);
}
}
}

//縦軸の値(消費量)を表示
void drawVolumeLabels() {
fill(0);
textSize(10);

stroke(128);
strokeWeight(1);

for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
if (v % volumeIntervalMinor == 0) {
float y = map(v, dataMin, dataMax, plotY2, plotY1);
if (v % volumeInterval == 0) {
if (v == dataMin) {
textAlign(RIGHT);
} else if (v == dataMax) {
textAlign(RIGHT, TOP);
} else {
textAlign(RIGHT, CENTER);
}
text(floor(v), plotX1 – 10, y);
line(plotX1 – 4, y, plotX1, y);
} else {
//line(plotX1 – 2, y, plotX1, y);
}
}
}
}

//指定した列の値を点の並びでプロット
void drawDataPoints(int col) {
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = map(value, dataMin, dataMax, plotY2, plotY1);
point(x, y);
}
}
}

//キー入力で表示する列を切り替える
void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
} else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

線分で表示する

  • 点でプロットするのではなく、線でそれぞれのデータを結んでいく
  • void drawDataLine(int col) – 指定した列のデータを線分(vertex)で結んでいる

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont;

void setup() {
size(720, 405);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = 0;
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

plotX1 = 120;
plotX2 = width – 80;
labelX = 50;
plotY1 = 60;
plotY2 = height – 70;
labelY = height – 25;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(224);

fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);

drawTitle();
drawAxisLabels();
drawYearLabels();
drawVolumeLabels();

strokeWeight(5);
noFill();
stroke(#5679C1);
//線でデータを結ぶ
drawDataLine(currentColumn);
}

void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

void drawAxisLabels() {
fill(0);
textSize(13);
textLeading(15);

textAlign(CENTER, CENTER);
text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
textAlign(CENTER);
text("Year", (plotX1+plotX2)/2, labelY);
}

void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER);

stroke(224);
strokeWeight(1);

for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
line(x, plotY1, x, plotY2);
}
}
}

int volumeIntervalMinor = 5;

void drawVolumeLabels() {
fill(0);
textSize(10);
textAlign(RIGHT);

stroke(128);
strokeWeight(1);

for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
if (v % volumeIntervalMinor == 0) {
float y = map(v, dataMin, dataMax, plotY2, plotY1);
if (v % volumeInterval == 0) {
float textOffset = textAscent()/2;
if (v == dataMin) {
textOffset = 0;
} else if (v == dataMax) {
textOffset = textAscent();
}
text(floor(v), plotX1 – 10, y + textOffset);
line(plotX1 – 4, y, plotX1, y);
} else {
//line(plotX1 – 2, y, plotX1, y);
}
}
}
}

//線でデータを結ぶ
void drawDataLine(int col) {
beginShape();
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = map(value, dataMin, dataMax, plotY2, plotY1);
vertex(x, y);
}
}
endShape();
}

void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
} else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

より自由な発想で視覚化してみる

  • この授業の趣旨は、科学的に厳密な視覚化ではない
  • より自由な発想で、データをもとに形態を生成してみる
  • バイオVJ?

応用1:円の大きさ量でを表現してみる

  • 数値の変化を円の直径にあてはめて、円の大きさの変化によって、情報を表現してみる

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont;

void setup() {
size(1024, 360);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = data.getTableMin();
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

plotX1 = 10;
plotX2 = width – 10;
labelX = 10;
plotY1 = 40;
plotY2 = height – 40;
labelY = height – 40;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(255);

drawTitle();
drawYearLabels();

noStroke();
fill(63,127,255,30);
//円の大きさで、データを視覚化
drawDataCircleSize(currentColumn);
}

//円の大きさでデータを視覚化する
void drawDataCircleSize(int col) {
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//円の直径を、取得した値をもとに計算
float diameter = map(value, dataMin, dataMax, 0, 200);
//円を描く
ellipse(x, y, diameter, diameter);
}
}
}

void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER);

stroke(224);
strokeWeight(1);

for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
line(x, plotY1, x, plotY2);
}
}
}

void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
} else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

応用2:色の透明度で量を表現してみる

  • 描画する図形の透明度によって、データを表現してみる
  • 数値が高くなるほど、透明度が下がるようにする

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont;

void setup() {
size(1024, 360);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = data.getTableMin();
dataMax = data.getTableMax();

plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height – 30;
labelY = height – 30;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(0);

drawTitle();
drawYearLabels();

noStroke();
//色の濃度(alpha)でデータを視覚化する
drawDataAlpha(currentColumn);
}

//色の濃度(alpha)でデータを視覚化する
void drawDataAlpha(int col) {
rectMode(CENTER);
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//データを色の濃度と対応させている
float a = map(value, dataMin, dataMax, 0, 255);
//四角形の幅を算出
float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
//計算した濃度で色を指定
fill(31,127,255,a);
//四角形を描画
rect(x, y, w, plotY2-plotY1);
}
}
}

void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

void drawYearLabels() {
fill(255);
textSize(10);
textAlign(CENTER);

stroke(224);
strokeWeight(1);

for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
//line(x, plotY1, x, plotY2);
}
}
}

void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
} else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

応用3:色相で量を表現してみる

  • 透明度ではなく、色相で表現するように変更してみる
  • 低い数値 → 青(色相=240)、高い数値 → 赤(色相=360) というようにマッピングしてみる

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont;

void setup() {
size(1024, 360);
colorMode(HSB,360,100,100,100);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = data.getTableMin();
dataMax = data.getTableMax();

plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height – 30;
labelY = height – 30;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(0);

drawTitle();
drawYearLabels();

noStroke();

//色相でデータを視覚化
drawDataHue(currentColumn);
}

//色相でデータを視覚化
void drawDataAlpha(int col) {
rectMode(CENTER);
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//取得したデータで色相を算出
float h = map(value, dataMin, dataMax, 240, 360);
float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
//算出した色相で色を設定
fill(h,100,100,70);
//四角形を描画
rect(x, y, w, plotY2-plotY1);
}
}
}

void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

void drawYearLabels() {
fill(255);
textSize(10);
textAlign(CENTER);

stroke(224);
strokeWeight(1);

for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
//line(x, plotY1, x, plotY2);
}
}
}

void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
}
else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

応用4:放射状に配置してみる

  • 値の変化の様子を放射状に配置してみる

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont;

void setup() {
size(640, 640);
colorMode(HSB,360,100,100,100);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = 0;
dataMax = data.getTableMax();

plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height – 30;
labelY = height – 30;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(0);

drawTitle();
stroke(63,127,255,180);
translate(width/2, height/2);
//放射状に情報を表示
drawCirclePlot(currentColumn);
}

//放射状に情報を表示
void drawCirclePlot(int col){
//線の太さを設定
strokeWeight(6);
//線の終端の形状を四角に
strokeCap(SQUARE);
//年を表示するテキストのサイズ
textSize(8);
int rowCount = data.getRowCount();
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
//全ての情報を表示した際にちょうど一周するように角度を算出
//360° = 2π(ラジアン)
float rot = map(years[row], yearMin, yearMax+1, 0.0, 2.0*PI);
float x = 0;
float y = map(value, dataMin, dataMax, 0, width*0.6);
//数値によって色相を変更する
float h = map(value, dataMin, dataMax, 240, 360);
stroke(h,100,100,50);
pushMatrix();
//算出した角度だけ回転
rotate(rot);
//情報の量に応じて、線の長さを変える
line(0, 0, 0, -y);
//10年ごとに年を表示
if(int(years[row])%10 == 0){
text(years[row], -10, -y-8);
}
popMatrix();
}
}
}

void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 – 10);
}

void keyPressed() {
if (key == ‘[‘) {
currentColumn–;
if (currentColumn < 0) {
currentColumn = columnCount – 1;
}
}
else if (key == ‘]’) {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
[/code]

応用5:アニメーションで変化を表現する

  • 値が変化する様子を、円の大きさがアニメーションすることで表現してみる

[code language=”java”]
FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

int currentRow = 0;

PFont plotFont;

void setup() {
size(640, 280);
colorMode(HSB,360,100,100,100);
frameRate(10);

data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();

years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length – 1];

dataMin = 0;
dataMax = data.getTableMax();

plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height – 30;
labelY = height – 30;

plotFont = createFont("SansSerif", 20);
textFont(plotFont);

smooth();
}

void draw() {
background(0);
noStroke();

//項目名を表示
drawTitles();
//現在の年を表示
drawCurrentYear();
//値の変化をアニメーションで表現
animateEllipsePlot();
}

//値の変化をアニメーションで表現
void animateEllipsePlot(){
//それぞれの列の値を取得
float value0 = data.getFloat(currentRow, 0);
float value1 = data.getFloat(currentRow, 1);
float value2 = data.getFloat(currentRow, 2);

//フォントの設定
textSize(10);
textAlign(CENTER);

//1つ目の円
float r0 = map(value0, dataMin, dataMax, 0, width/3);
fill(0,70,70,70);
ellipse(width/6,height/2,r0,r0);
//円の中心に値を表示
fill(0,0,100,100);
text(r0,width/6,height/2+5);

//2つ目の円
float r1 = map(value1, dataMin, dataMax, 0, width/3);
fill(240,70,70,70);
ellipse(width/2,height/2,r1,r1);
//円の中心に値を表示
fill(0,0,100,100);
text(r1,width/2,height/2+5);

//3つ目の円
float r2 = map(value2, dataMin, dataMax, 0, width/3);
fill(120,70,70,70);
ellipse(width/6*5,height/2,r2,r2);
//円の中心に値を表示
fill(0,0,100,100);
text(r2,width/6*5,height/2+5);

//年を1つ進める
currentRow++;
//もし最後の年まできたら、最初から繰り返す
if(currentRow > years.length-1){
currentRow = 0;
}
}

void drawTitles(){
textSize(12);
textAlign(CENTER);
fill(0,0,100,100);

String title0 = data.getColumnName(0);
text(title0, width/6, height-20);

String title1 = data.getColumnName(1);
text(title1, width/2, height-20);

String title2 = data.getColumnName(2);
text(title2, width/6*5, height-20);
}

void drawCurrentYear(){
textSize(20);
textAlign(LEFT);
fill(0,0,100,100);

int currentYear = years[currentRow];
text(currentYear, 10,24);
}
[/code]

サンプルファイルのダウンロード

今回の授業の全てのサンプルのソースコードは、下記のリンクよりダウンロードしてください。