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() – 表全体の最大値を求める
  • コメントを付加したソースコードは下記のとおり
//ファイルの最初の行は、各列の名前を示したヘッダーでなければならない
//最初の列は、各行のタイトルでなくてはならない
//その他の値は、全て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;
  }
}

メインプログラムの構造

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

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

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