groovy和map_具有Groovy和Apache POI的FreeChart

groovy和map

本文的重点是向您展示如何从如下所示的Excel电子表格中解析数据:

并将其变成一系列如下所示的图形:

最近,我一直在寻找机会与JFreeChart进行一些练习,最终查看了加拿大政府作为“开放数据”计划的一部分发布的数据集。

特定的数据集标题为“按所有权,种类种植的幼苗数量” ,并以Excel电子表格形式提供,因此需要Apache POI库才能读取数据。通常,至少在我看来根据经验,Excel电子表格主要用于人类消费,这增加了解析的复杂程度。 幸运的是,电子表格确实遵循一种重复模式,可以很容易地解决这个问题,因此这不是不可克服的。 尽管如此,我们还是希望从Excel中获取数据,以使其更易于机器使用,因此第一步是将其转换为JSON表示形式。 一旦采用这种更具可移植性的形式,我们便可以使用JFreeChart将数据轻松转换为图形可视化。

电子表格格式

Excel作为一种工作场所工具已经非常完善,可以提高个人生产率,并且绝对是普通上班族的福音。 问题在于,一旦有了数据,通常就会被困在那里。 数据倾向于基于人类的审美观而不是基于可解析性来进行布局,这意味着除非您想使用Excel本身进行进一步的分析,否则没有太多选择。 导出为csv等更中性的格式会遇到相同的问题-即,如果不设计自定义解析器,就无法连贯地读取数据。 在这种特殊情况下,分析电子表格时必须考虑以下因素:

  • 合并的单元格,其中一列表示多个连续行的固定值。
  • 列标题不代表所有实际列。 在这里,每个省的“注释”列紧随其“数据”列之后。 由于标题单元格跨这两个列合并,因此不能直接将其用于解析数据。
  • 数据分为多个域,这些域导致格式的重复。
  • 数据包含结果可用的数字和不可用的文本的混合。 文本条目的含义在电子表格末尾的表格中描述。
  • 在整个文档中都重复了节标题和标题,显然是试图与某些打印布局相匹配,或者可能只是试图为滚动长文档的人员提供一些帮助。

电子表格中的数据首先按省政府土地,私人土地,联邦土地划分为报告,最后按所有土地的总数划分。

在这些部分的每个部分中,每年都会报告所有省和地区的每种树种的数据,以及加拿大这些数字的总计。

这些种类数据表中的每一个都具有相同的行/列结构,这使我们能够创建一个足以从它们各自中分别读取数据的解析结构。

将电子表格转换为JSON

为了解析Excel文档,我使用Apache POI库和Groovy包装器类来协助处理。 包装器类非常简单,但是允许我们抽象出处理Excel文档的大部分机制。 全文可从作者Goran Ehrsson的博客文章中获得 。 关键好处是能够基于简单映射中提供的“偏移”和“最大”参数指定要处理的文件窗口。 这是在电子表格末尾读取文本符号表数据的示例。

我们定义了一个Map,该Map指出要读取的工作表,要开始的行(偏移)以及要处理的行数。 ExcelBuilder类(根本不是真正的构建器)采用File对象的路径,并在后台将其读入POI HSSFWorkbook ,然后通过对eachLine方法的调用来引用该POI HSSFWorkbook

public static final Map SYMBOLS = [sheet: SHEET1, offset: 910, max: 8]
...
    final ExcelBuilder excelReader = new ExcelBuilder(data.absolutePath)
    Map<String, String> symbolTable = [:]
    excelReader.eachLine(SYMBOLS) { HSSFRow row ->
        symbolTable[row.getCell(0).stringCellValue] = row.getCell(1).stringCellValue
    }

最终,当我们将其转换为JSON时,它将如下所示:

'Symbols': {
        '...': 'Figures not appropriate or not applicable',
        '..': 'Figures not available',
        '--': 'Amount too small to be expressed',
        '-': 'Nil or zero',
        'p': 'Preliminary figures',
        'r': 'Revised figures',
        'e': 'Estimated by provincial or territorial forestry agency',
        'E': 'Estimated by the Canadian Forest Service or by Statistics Canada'
    }

现在,处理其他数据块变得有些棘手。 第一列由2个合并的单元格组成,除一个头外,其他所有头实际上代表两列信息:一个计数和一个可选的表示法。 合并的列由一个简单的EMPTY占位符处理,而多余的列则通过处理标题列表来处理。

public static final List<String> HEADERS = ['Species', 'EMPTY', 'Year', 'NL', 'PE', 'NS', 'NB', 'QC', 'ON', 'MB', 'SK', 'AB',
    'BC', 'YT', 'NT *a', 'NU', 'CA']
/**
* For each header add a second following header for a 'notes' column
* @param strings
* @return expanded list of headers
*/
private List<String> expandHeaders(List<String> strings)
{
    strings.collect {[it, '${it}_notes']}.flatten()
}

每个数据块都对应于特定的树种,并按年份和省份或地区分类。 每个物种都由一个地图表示,该地图定义了信息在文档中的包含位置,因此我们可以遍历这些地图的集合并非常容易地汇总数据。 这组常量和代码足以解析文档中的所有数据。

public static final int HEADER_OFFSET = 3
        public static final int YEARS = 21
        public static final Map PINE = [sheet: SHEET1, offset: 6, max: YEARS, species: 'Pine']
        public static final Map SPRUCE = [sheet: SHEET1, offset: 29, max: YEARS, species: 'Spruce']
        public static final Map FIR = [sheet: SHEET1, offset: 61, max: YEARS, species: 'Fir']
        public static final Map DOUGLAS_FIR = [sheet: SHEET1, offset: 84, max: YEARS, species: 'Douglas-fir']
        public static final Map MISCELLANEOUS_SOFTWOODS = [sheet: SHEET1, offset: 116, max: YEARS, species: 'Miscellaneous softwoods']
        public static final Map MISCELLANEOUS_HARDWOODS = [sheet: SHEET1, offset: 139, max: YEARS, species: 'Miscellaneous hardwoods']
        public static final Map UNSPECIFIED = [sheet: SHEET1, offset: 171, max: YEARS, species: 'Unspecified']
        public static final Map TOTAL_PLANTING = [sheet: SHEET1, offset: 194, max: YEARS, species: 'Total planting']
        public static final List<Map> PROVINCIAL = [PINE, SPRUCE, FIR, DOUGLAS_FIR, MISCELLANEOUS_SOFTWOODS, MISCELLANEOUS_HARDWOODS, UNSPECIFIED, TOTAL_PLANTING]
        public static final List<String> AREAS = HEADERS[HEADER_OFFSET..-1]

        ...

        final Closure collector = { Map species ->
            Map speciesMap = [name: species.species]
            excelReader.eachLine(species) {HSSFRow row ->
                //ensure that we are reading from the correct place in the file
                if (row.rowNum == species.offset)
                {
                    assert row.getCell(0).stringCellValue == species.species
                }
                //process rows
                if (row.rowNum > species.offset)
                {
                    final int year = row.getCell(HEADERS.indexOf('Year')).stringCellValue as int
                    Map yearMap = [:]
                    expandHeaders(AREAS).eachWithIndex {String header, int index ->
                        final HSSFCell cell = row.getCell(index + HEADER_OFFSET)
                        yearMap[header] = cell.cellType == HSSFCell.CELL_TYPE_STRING ? cell.stringCellValue : cell.numericCellValue
                    }
                    speciesMap[year] = yearMap.asImmutable()
                }
            }
            speciesMap.asImmutable()
        }

定义的收集器“封闭”将返回四个组(省,私有土地,联邦和总计)之一的所有物种数据的地图。 唯一区分这些组的是它们在文件中的偏移量,因此我们可以通过更新第一个偏移量的方式为每个结构定义映射。

public static final List<Map> PROVINCIAL = [PINE, SPRUCE, FIR, DOUGLAS_FIR, MISCELLANEOUS_SOFTWOODS, MISCELLANEOUS_HARDWOODS, UNSPECIFIED, TOTAL_PLANTING]
    public static final List<Map> PRIVATE_LAND = offset(PROVINCIAL, 220)
    public static final List<Map> FEDERAL = offset(PROVINCIAL, 441)
    public static final List<Map> TOTAL = offset(PROVINCIAL, 662)

    private static List<Map> offset(List<Map> maps, int offset)
    {
        maps.collect { Map map ->
            Map offsetMap = new LinkedHashMap(map)
            offsetMap.offset = offsetMap.offset + offset
            offsetMap
        }
    }

最后,我们可以使用收集器 Closure遍历这些简单的地图结构,最后得到代表所有数据的单个地图。

def parsedSpreadsheet = [PROVINCIAL, PRIVATE_LAND, FEDERAL, TOTAL].collect {
            it.collect(collector)
        }
        Map resultsMap = [:]
        GROUPINGS.eachWithIndex {String groupName, int index ->
            resultsMap[groupName] = parsedSpreadsheet[index]
        }
        resultsMap['Symbols'] = symbolTable

而且,JsonBuilder类提供了一种简单的方法,可以将任何映射转换为准备写出结果的JSON文档。

Map map = new NaturalResourcesCanadaExcelParser().convertToMap(data)
        new File('src/test/resources/NaturalResourcesCanadaNewSeedlings.json').withWriter {Writer writer ->
            writer << new JsonBuilder(map).toPrettyString()
        }


将JSON解析为JFreeChart折线图

好的,现在我们已经将数据转换为一种更易使用的格式,是时候对其进行可视化了。 对于这种情况,我使用了JFreeChart库和GroovyChart项目的组合,该项目为使用JFreeChart API提供了很好的DSL语法。 目前看来它尚未处于开发阶段,但是除了jar尚未发布到可用存储库这一事实之外,这完全取决于这项任务。

我们将为表示的十四个区域中的每个区域创建四个图表,总共共有56个图形。 所有这些图都包含所跟踪的八种树种中每条的情节线。 这意味着总体而言,我们需要创建448个不同的时间序列。 我没有对这花费多长时间进行任何正式的计时,但是总的来说,它花费了不到十秒钟的时间来产生所有这些。 只是为了好玩,我添加了GPar以并行化图表的创建,但是由于将图像写入磁盘将是此过程中最昂贵的部分,因此我认为它不会使速度大大提高。

首先,使用JsonSlurper可以很容易地从文件中读取JSON数据。

def data
        new File(jsonFilename).withReader {Reader reader ->
            data = new JsonSlurper().parse(reader)
        }
        assert data

这是一个样本的JSON数据在一年中的样子的示例,该数据首先按四个主要类别之一分类,然后按树种分类,然后按年份分类,最后按省或地区分类。

{
    'Provincial': [
        {
            'name': 'Pine',
            '1990': {
                'NL': 583.0,
                'NL_notes': '',
                'PE': 52.0,
                'PE_notes': '',
                'NS': 4.0,
                'NS_notes': '',
                'NB': 4715.0,
                'NB_notes': '',
                'QC': 33422.0,
                'QC_notes': '',
                'ON': 51062.0,
                'ON_notes': '',
                'MB': 2985.0,
                'MB_notes': '',
                'SK': 4671.0,
                'SK_notes': '',
                'AB': 8130.0,
                'AB_notes': '',
                'BC': 89167.0,
                'BC_notes': 'e',
                'YT': '-',
                'YT_notes': '',
                'NT *a': 15.0,
                'NT *a_notes': '',
                'NU': '..',
                'NU_notes': '',
                'CA': 194806.0,
                'CA_notes': 'e'
            },
    ...

构建图表是一个简单的过程,只需遍历已解析数据的结果图即可。 在这种情况下,我们将忽略“注释”数据,但已将其包含在数据集中,以备日后使用。 我们也只是忽略任何非数字值。

GROUPINGS.each { group ->
            withPool {
                AREAS.eachParallel { area ->
                    ChartBuilder builder = new ChartBuilder();
                    String title = sanitizeName('$group-$area')
                    TimeseriesChart chart = builder.timeserieschart(title: group,
                            timeAxisLabel: 'Year',
                            valueAxisLabel: 'Number of Seedlings(1000s)',
                            legend: true,
                            tooltips: false,
                            urls: false
                    ) {
                        timeSeriesCollection {
                            data.'$group'.each { species ->
                                Set years = (species.keySet() - 'name').collect {it as int}
                                timeSeries(name: species.name, timePeriodClass: 'org.jfree.data.time.Year') {
                                    years.sort().each { year ->
                                        final value = species.'$year'.'$area'
                                        //check that it's a numeric value
                                        if (!(value instanceof String))
                                        {
                                            add(period: new Year(year), value: value)
                                        }
                                    }
                                }
                            }
                        }
                    }
...
}

然后,我们对JFreeChart应用一些附加格式,以增强输出样式,将图像插入背景,并修复打印配色方案。

JFreeChart innerChart = chart.chart
                    String longName = PROVINCE_SHORT_FORM_MAPPINGS.find {it.value == area}.key
                    innerChart.addSubtitle(new TextTitle(longName))
                    innerChart.setBackgroundPaint(Color.white)
                    innerChart.plot.setBackgroundPaint(Color.lightGray.brighter())
                    innerChart.plot.setBackgroundImageAlignment(Align.TOP_RIGHT)
                    innerChart.plot.setBackgroundImage(logo)
                    [Color.BLUE, Color.GREEN, Color.ORANGE, Color.CYAN, Color.MAGENTA, Color.BLACK, Color.PINK, Color.RED].eachWithIndex { color, int index ->
                        innerChart.XYPlot.renderer.setSeriesPaint(index, color)
                    }

然后,我们将每个图表写到一个公式化的png文件中。

def fileTitle = '$FILE_PREFIX-${title}.png'
                    File outputDir = new File(outputDirectory)
                    if (!outputDir.exists())
                    {
                        outputDir.mkdirs()
                    }
                    File file = new File(outputDir, fileTitle)
                    if (file.exists())
                    {
                        file.delete()
                    }
                    ChartUtilities.saveChartAsPNG(file, innerChart, 550, 300)

为了将所有内容捆绑在一起,使用MarkupBuilder创建了一个html页面,以展示所有结果(按省或地区组织)。

def buildHtml(inputDirectory)
    {
        File inputDir = new File(inputDirectory)
        assert inputDir.exists()
        Writer writer = new StringWriter()
        MarkupBuilder builder = new MarkupBuilder(writer)
        builder.html {
            head {
                title('Number of Seedlings Planted by Ownership, Species')
                style(type: 'text/css') {
                    mkp.yield(CSS)
                }
            }
            body {
                ul {
                    AREAS.each { area ->
                        String areaName = sanitizeName(area)
                        div(class: 'area rounded-corners', id: areaName) {
                            h2(PROVINCE_SHORT_FORM_MAPPINGS.find {it.value == area}.key)
                            inputDir.eachFileMatch(~/.*$areaName\.png/) {
                                img(src: it.name)
                            }
                        }
                    }
                }
                script(type: 'text/javascript', src: 'https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', '')
                script(type: 'text/javascript') {
                    mkp.yield(JQUERY_FUNCTION)
                }
            }
        }
        writer.toString()
    }

生成的html页面假定所有图像都位于同一文件夹中,每个省/地区显示四张图像,并且出于娱乐目的,使用JQuery将单击处理程序附加到每个标题上。 单击标题,该div中的图像将设置为背景动画。 我确信可以使用实际的JQuery进行改进,但是可以达到目的。 这是html输出的示例:

<ul>
      <div class='area rounded-corners' id='NL'>
        <h2>Newfoundland and Labrador</h2>
        <img src='naturalResourcesCanadaNewSeedlings-Federal-NL.png' />
        <img src='naturalResourcesCanadaNewSeedlings-PrivateLand-NL.png' />
        <img src='naturalResourcesCanadaNewSeedlings-Provincial-NL.png' />
        <img src='naturalResourcesCanadaNewSeedlings-Total-NL.png' />
      </div>
    ...

在Firefox中,结果页面如下所示。


源代码和链接

源代码可在GitHub上获得最后的html页面也是如此 。 从Excel到html页面中嵌入的图表所需的全部资源都略少于300行代码,而且我认为对于两个小时的工作而言,结果并不算太糟糕。 最后,JSON结果也托管在该项目的GitHub页面上 ,以供其他可能想要研究数据的人使用。

与该主题相关的一些阅读材料:

相关链接:

  1. Groovy inspect()/ Eval用于外部化数据
  2. Groovy反向映射排序轻松完成
  3. 使用Groovy脚本可以做的五件事

参考:来自The Japtain on…博客的JCG合作伙伴 Kelly Robinson的JFreeChart与Groovy和Apache POI


翻译自: https://www.javacodegeeks.com/2012/08/freechart-with-groovy-and-apache-poi.html

groovy和map

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值