yoppa.org


immediate bitwave

芸大 – 情報編集(Web) 2014

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

今日の目標

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

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

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

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

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

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

  • このプロットは、dataフォルダ内にある、tab区切りのデータファイルを読み込んでいる
  • データの内容は下記のとおり
Year	Milk	Tea	Coffee
1910	32.2	9.6	21.7
1911	31.3	10.2	19.7
1912	34.4	9.6	25.5
1913	33.1	8.5	21.2
1914	31.1	8.9	21.8
1915	29	9.6	25
1916	28	9.6	27.1
... (中略) ...
2002	21.9	7.8	18.1
2003	21.6	7.5	18.5
2004	21.2	7.3	18.8

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

  • プログラムの構造 – メインのプログラム(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() – 表全体の最大値を求める
  • コメントを付加したソースコードは下記のとおり
//ファイルの最初の行は、各列の名前を示したヘッダーでなければならない
//最初の列は、各行のタイトルでなくてはならない
//その他の値は、全て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("\"") && array[i].endsWith("\"")) {
	  array[i] = array[i].substring(1, array[i].length() - 1);
	}
      }
      //ダブルクォートをシングルクォートに
      array[i] = array[i].replaceAll(""\"", "\"");
    }
  }
  
  //行の数を数える
  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;
  }
}

メインプログラムの構造

  • メインプログラム(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);
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); 
    } 
  } 
}

情報を表示

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

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;
    }
  }
}

線分で表示する

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

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;
    }
  }
}

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

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

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

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

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;
    }
  }
}

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

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

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;
    }
  }
}

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

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

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;
    }
  }
}

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

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

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;
    }
  }
}

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

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

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);
}

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

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