前回のあらすじ。XML で記述されているファイルを取り出したらβ2が公開されて、仕様が変わっちゃってて困ったよ。
なんか、Silverlight でなくてもええやん??って感じなんだけど、Silverlight なのっ!!
前回、XML ファイルからクラス化した情報を、グラフにする。検索すればコンポーネントが見つかるのだけど、あえてそれらは使わない。
「グラフにする」とは、どういう事か、というところを考える。
線を引けば良いわけだけど、じゃぁ、どうやって引くの?データの個数だけ、横に区切りがある。値の最大値にあわせて、縦にもいくつか区切りがある。その区切りにあわせてプロットすればよい。
いうのは簡単だぁ!
まず、横。グラフを描画する範囲。これを便宜上、「キャンバス」と呼ぶことにします。キャンバスの横幅(ピクセル数)をデータの個数で割れば、データとデータの間に何ピクセル置けばよいか、求まります。
同じく縦も、キャンバスの縦幅(ピクセル数)を、データの最大値で割れば、データ1単位あたり、何ピクセル取ればよいか求まります。
こうして求めたものを、単位ピクセル数とします。
データ配列のn番目に横の単位ピクセル数を掛け、値に縦の単位ピクセル数を掛けると、座標が求まります。
ところで、「値」ってどれ?
とりあえず、「累積稼働時間」としたけど、他の線を重ねたりするかもしれない。そんなわけで、インターフェイスを定義してみた。
public interface IGraphConvertible
{
    double Value {
        get;
    }
}
で、第3回では List<Log> としたのだけれど、これを、このインターフェイスのリストにしてみる。すると、「インターフェイスは実体化できません」と怒られた。そりゃそうか。で、abstruct クラスを作ってみた。
public abstract class GraphConvertibleObject : IGraphConvertible
{
    #region IGraphConvertible メンバ
    public abstract double Value {
        get;
    }
    #endregion
}
仮想クラスも実体化できないからなぁ。最初っから virtual なメソッドにすればいいんじゃね?とか思いながら動かすと、動いた。・・・ふ~ん。まぁ、いいや。
で、コレクションが変更になる。
public class GraphConvertibleCollection
    : System.Collections.Generic.List<GraphConvertibleObject>
{
}
当然、Log クラスも変更。
class Log
{
    public Log() {
        List = new GraphConvertibleCollection();
    }
    [XmlArrayItem(typeof(Warning))]
    public GraphConvertibleCollection List() {
        get;
        set;
    }
}
おっと、肝心の Warning クラスを忘れていた。
class Warning : GraphConvertibleObject
{
    [XmlAttruibute]
    public Int64 TotalRuntime() {
        get;
        set;
    }
    [XmlAttribute]
    public Int64 ErrorCode() {
        get;
        set;
    }
    [XmlText]
    public string LogMessage() {
        get;
        set;
    }
    public override double Value {
        get { return this.TotalRuntime; }
    }
}
それでは、グラフ化…というか、折れ線化するところ。
static public PointCollection NormalSeriesLine(
    GraphConvertibleCollection dataCollection, Size canvasSize, Size divideCount) {
    // 1
    PointCollection points = new PointCollection();
    // 2
    double stepX, stepY;
    if (divideCount.Width == 0) {
        stepX = canvasSize.Width / (double)dataCollection.Count;
    } else {
        stepX = canvasSize.Width / (double)divideCount.Width;
    }
    // 3
    if (divideCount.Height == 0) {
        // 4
        double maxY = 0;
        foreach (IGraphConvertible w in dataCollection) {
            if (w.Value > maxY) {
                maxY = w.Value;
            }
        }
        stepY = canvasSize.Height / (double)maxY;
    } else {
        stepY = canvasSize.Height / (double)divideCount.Height;
    }
    // 5
    points.Add(new Point(0, canvasSize.Height));
    // 6
    int cnt = 0;
    foreach (IGraphConvertible w in dataCollection) {
        long x = Convert.ToInt32(Math.Floor(++cnt * stepX));
        long y = Convert.ToInt32(Math.Floor(w.Value * stepY));
        // 7
        y = Convert.ToInt32(canvasSize.Height - y);
        // 8
        points.Add(new Point(x, y));
    }
    // 9
    return points;
}
引数は、dataCollection がデータのリスト。canvasSize は、描画範囲。divideCount は、それを縦横それぞれいくつに分割するか。複数の線を重ねるとき、あるデータ集団は10個あるけど、あるデータ集団は8個しかデータがないかもしれない。そもそも、今回もn回の故障履歴を元に、n+1回目の故障がどれくらいのタイミングで発生するかをグラフで見るものだから、故障回数とグラフの右端は等しいわけではない。そのため、別個に指定できるようにした。
1...PolyLine を返さずに PointCollection にしたのは、それぞれの頂点がアニメーションしたらおもしろいと思いません?後で変更できるように、とりあえず、PointCollection にしておく。
2...横軸の単位ピクセル数を求める処理。divideCount が0なら、データ数を用いることにする。
3...縦軸の単位ピクセル数を求める処理。
4...縦軸の場合、「データの数」なんてわかりやすいものはないので、全データをなめて最大値を求める。最小値は0なの。0以下になる場合は自分で考えよう。
5...これは、不要かも。(0, 0) の点を追加。
6...それぞれのデータを、座標に変換する処理。
7...普通、見慣れているグラフは上に行くほど Y の値が大きくなります。しかし、コンピュータの座標系は、左上が (0, 0) です。そのため、Y = 0 の線を軸に、反転させてやる必要があります。その処理。Matrix とかいうのがあって、それを指定すると一点一点反転させなくてもいいのだけれど、字を描いたとき、字まで反転しちゃうので、却下。
8...PointCollection に追加して。
9...返しておしまい。
 
おまけ
多角形をアニメーションさせる方法。元ネタ→Animating A Polygon?(英語)
PolyLine じゃ無理のようで、パスになりました。パスについては、[Silverlight 奮戦記] (2) 線を描くの回をご覧ください。
{
    Storyboard lineStory = new Storyboard();
    foreach (LineSegment line in figure.Segments) {
    PointAnimation anime = CreatePointAnimation(new Point(line.Point.X, canvas1.ActualHeight), line.Point);
        lineStory.Children.Add(anime);
        Storyboard.SetTarget(anime, line);
        Storyboard.SetTargetProperty(anime, new PropertyPath("Point"));
    }
    LayoutRoot.Resources.Add("lineStory", lineStory);
    lineStory.Begin();
}
private PointAnimation CreatePointAnimation(Point from, Point to) {
    PointAnimation anime = new PointAnimation();
    anime.AutoReverse = false;
    anime.BeginTime = new TimeSpan(1);
    anime.From = from;
    anime.To = to;
    anime.RepeatBehavior = new RepeatBehavior(1.0);
    anime.Duration = new Duration(TimeSpan.FromSeconds(3.0));
    return anime;
}
	
		投稿日時 : 2008年7月30日 21:05