倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

はじめての幻獣 Griffon 研 (28) : マルチ MVC への道 (5) : 複数のグラフを描画する

今回はすべてのグラフを描画する「paintAll」処理を実装します(一覧)。 マルチ MVC とは直接関係ありませんが・・・。

今回実装するのは以下のソース:

  • FunctionPlotter/griffon-app/models/MonolineFunctionModel.groovy
  • FunctionPlotter/src/main/functionplotter/FunctionDataFactory.groovy
  • FunctionPlotter/griffon-app/views/FunctionPlotterView.groovy
  • FunctionPlotter/griffon-app/charts/CoordinateChart.groovy
  • FunctionPlotter/griffon-app/controllers/FunctionPlotterController.groovy

以下、順に見ていきましょう。

MonolineFunctionModel.groovy


MonolineFunctionModel クラスは、プロパティに関しては「はじめての幻獣 Griffon 研 (25) : マルチ MVC への道 (2) : MVC グループをコーディングする」で作成したものと同じです。 これに加えて、後で見る FunctionDataFactory クラスを作成するメソッドを新たに定義します:

package functionplotter

class MonolineFunctionModel{

    @Bindable String name
    @Bindable String function
    @Bindable int samples = 1000

    def createDataFactory(){
        return new FunctionDataFactory(name:name, function:function, samples:samples)
    }
}

FunctionDataFactory.groovy


FunctionDataFactory クラスは、MonolineFunctionModel クラスの持つ情報から JFreeChart でグラフ描画を行うのに必要な XYSeries オブジェクトを生成するクラスです。 処理自体はあまり複雑ではありませんが、コストのかかる処理を EDT 外で行うためにクラスとして抽出しています*1

package functionplotter

import org.jfree.data.general.DatasetUtilities
import org.jfree.data.xy.XYSeries

class FunctionDataFactory{

    String name
    String function
    int samples

    XYSeries createXYSeries(double from, double to){
        def func = new ScriptFunction2D(this.function)
        return DatasetUtilities.sampleFunction2DToSeries(func, from, to, this.samples, this.name)
    }
}

FunctionPlotterView.groovy


FunctionPlotterView クラスに関しては、大きな変更点は前々回前回にみた controlPanel, functionPanel の部分です(ただし、今回はその部分を省略)。 細かい点では migLayout() の使い方をチラホラ変えてます。

package functionplotter

JFrame.defaultLookAndFeelDecorated = true
JDialog.defaultLookAndFeelDecorated = true

actions{
    action(id:'paintAll',
        name: 'Paint All',
        closure: controller.paintAll,
        mnemonic: 'P',
        accelerator: 'ctrl P')

    ...    // その他のアクション
}

application(title: 'Function Plotter', location: [50,50], size: [1050,600],
               defaultCloseOperation:WindowConstants.EXIT_ON_CLOSE, ...){
    menuBar(){
        menu(mnemonic:'C', 'Chart'){
            menuItem action:paintAll
        }
        menu(mnemonic:'F', 'Function'){
            menuItem action:addFunction
        }
        glue()
        menu(mnemonic:'H', 'Help'){
            menuItem action:showLaf
            menuItem action:showInfo
        }
    }

    panel(border:emptyBorder(6)){
        migLayout()
        
        panel(id:'controlPanel', ...){ ... }    // 前回参照。

        panel(border:titledBorder(title:'Function Plot')){
           migLayout()

            panel{
                chart(id:'coordinate', CoordinateChart.class)
            }

            panel(constraints:'south'){
                migLayout(columnConstraints:'50[20][30]520[20][30]')
                domainSpinner('from', 0d)
                domainSpinner('to', Math.PI*2.0d)
            }

            panel(constraints:'west'){
                migLayout(layoutConstraints:'wrap 1', 
                              columnConstraints:'[40]', 
                              rowConstraints:'[15][15]320[15][15]')
                rangeSpinner('max', 1d)
                rangeSpinner('min', -1d)
            }
        }
    }
}

def domainSpinner(label, value){
    this.label(label)
    spinner(value:bind(target:model, label), stateChanged:controller.paintAll, 
            model:spinnerNumberModel(value:value))
}

def rangeSpinner(label, value){
    this.label(label)
    spinner(value:bind(target:model, label), stateChanged:controller.adjustRange, 
            model:spinnerNumberModel(value:value))
}

CoordinateChart.groovy


「Paint All」処理で複数のグラフを描画できるようにするので、チャートには凡例 (legend) を表示させるようにした方がいいでしょう。 この設定はグラフ生成を行うスクリプト CoordinateChart.groovy で行います。

package functionplotter

import org.jfree.chart.plot.PlotOrientation as PO

xylinechart(XAxisLabel: 'X', YAxisLabel: 'Y', orientation:PO.VERTICAL, legend:true) {

    xyplot {
        foregroundAlpha(1.0f)
    }
}

変更点は xylinechart ノードに「legend:true」を設定しただけです。

FunctionPlotterController.groovy


最後は、実際にグラフを描画する処理。 これはもちろん FunctionPlotterController に paintAll として実装します。

package functionplotter

import com.thecoderscorner.groovychart.chart.ChartBuilder
import org.jfree.data.xy.XYSeriesCollection

class FunctionPlotterController {

    def model
    def view

    private List functionModelList = []

    def paintAll = { evt = null ->
        def dataFactories = this.functionModelList.collect{ it.createDataFactory() }
        def props = model.copyProperties()

        doOutside{
            def ds = new XYSeriesCollection()
            dataFactories.each{ factory ->
                ds.addSeries(factory.createXYSeries(props.from, props.to))
            }

            def chart = createChart(CoordinateChart.class)

            chart.plot.with{
                dataset = ds
                rangeAxis.with{
                    lowerBound = props.min
                    upperBound = props.max
                }
            }

            doLater{
                view.coordinate.chart = chart
            }
        }
    }

    /** CoordinateChart.groovy (正確には CoordinateChart クラス) から JFreeChart オブジェクトを生成 */
    static createChart(Class chartScriptClass){
        def chartScript = chartScriptClass.newInstance()
        def builder = new ChartBuilder()
        chartScript.metaClass.methodMissing = { String name, args -> builder.invokeMethod(name, args) }
        return chartScript.run().chart
    }

    ...    // その他
}
  • 基本的には上で定義した FunctionDataFactory と JFreeChart API を使ってるだけです。
  • functionModelList は MonolineFunctionModel のリストです。 これは前回にも使ってましたが、「MonolineFunction」 MVC グループを生成した際にその Model をこのリストに格納するようにしてあります。 グラフ描画の際にこれらの Model から FunctionDataFactory オブジェクトを生成して使用します。

実行結果


上記の実装を行って「関数描画アプリケーション」を実行するとこんな感じになります(関数 g(x) = cos(x) を追加して描画):

ちょっと画像が小さいので見にくいですが。
Griffon in Action

Griffon in Action