Created
February 26, 2013 07:06
-
-
Save GrahamLea/5036561 to your computer and use it in GitHub Desktop.
A Scala script that takes in a CSV from stdin and write a bubble chart as a PNG to stdout.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io.Source | |
import java.awt.font.GlyphVector | |
import java.awt.{Font, Graphics2D, Color} | |
import java.awt.RenderingHints._ | |
import java.awt.geom.{Rectangle2D, Ellipse2D} | |
import java.awt.image.BufferedImage | |
import java.io.{File, ByteArrayInputStream} | |
import javax.imageio.ImageIO | |
// Change anything in this block to change how the input is interpreted | |
val containsColumnHeaders = true | |
val containsRowHeaders = true | |
val reverseRows = true | |
val reverseColumns = false | |
// Change anything in this block to change how the chart appears | |
val cellSizePixels = 150 | |
val cellSpacingPixels = 8 | |
val imageMarginPixels = 20 | |
val maxColour = new Color(32, 32, 192) | |
val zeroColour = new Color(255, 255, 255) | |
val fontSize = cellSizePixels / 5 | |
val fontFamily = Font.SANS_SERIF | |
val bandColour = new Color(0, 0, 0, 0.1f) | |
val circleOutlineColour = Color.gray | |
val halfCellSize = cellSizePixels / 2 | |
val halfCellSpacing = cellSpacingPixels / 2 | |
val fullCellSize = cellSizePixels + cellSpacingPixels | |
val greenBase = zeroColour.getGreen | |
val redBase = zeroColour.getRed | |
val blueBase = zeroColour.getBlue | |
val greenRange = maxColour.getGreen - zeroColour.getGreen | |
val redRange = maxColour.getRed - zeroColour.getRed | |
val blueRange = maxColour.getBlue - zeroColour.getBlue | |
val inputStream = System.in | |
//val inputStream = new ByteArrayInputStream( | |
//""",0-1 years,1-2 years,2-3 years,3-4 years,4-5 years,5-6 years,8-9 years,None | |
// |1 - Disgracefully less productive in Scala,5,1,,,,,, | |
// |2 - A lot less productive in Scala,11,4,2,1,,,,1 | |
// |3 - A little less productive in Scala,7,,2,,,1,,2 | |
// |4 - About the same,10,1,2,1,1,,,3 | |
// |5 - A little more productive in Scala,41,18,2,,,,,13 | |
// |6 - A lot more productive in Scala,83,41,15,5,4,,,26 | |
// |7 - Amazingly more productive in Scala,45,43,21,9,4,,1,12""".getBytes) | |
val lines = Source.fromInputStream(inputStream).getLines().toBuffer | |
var cells = lines map { _.split(',').toBuffer } | |
if (containsColumnHeaders) | |
cells = cells.drop(1) | |
if (containsRowHeaders) | |
cells = cells map { _.drop(1) } | |
def toIntOption(s: String): Option[Int] = try { Some(s.toInt) } catch { case e: NumberFormatException => None } | |
var values = cells map { _.map { toIntOption } map { _.getOrElse(0) } } | |
if (reverseRows) | |
values = values.reverse | |
if (reverseColumns) | |
values = values map { _.reverse } | |
val maxValue = math.sqrt(values.flatten.max / math.Pi) | |
val circlesAndColoursAndValues = | |
for ((row, rowIndex) <- values.zipWithIndex) yield { | |
for ((value, columnIndex) <- row.zipWithIndex) yield { | |
val ratio = (math.sqrt(value / math.Pi) / maxValue).toFloat | |
(new Ellipse2D.Float(columnIndex * fullCellSize + (halfCellSize * (1 - ratio)), | |
rowIndex * fullCellSize + (halfCellSize * (1 - ratio)), | |
ratio * cellSizePixels, ratio * cellSizePixels), | |
new Color((redBase + ratio * redRange).toInt, (greenBase + ratio * greenRange).toInt, (blueBase + ratio * blueRange).toInt), | |
value) | |
} | |
} | |
val rowCount = circlesAndColoursAndValues.length | |
val columnCount = (circlesAndColoursAndValues map { _.length }).max | |
val chartWidth = (cellSizePixels + cellSpacingPixels) * columnCount | |
val chartHeight = (cellSizePixels + cellSpacingPixels) * rowCount | |
val imageWidth = imageMarginPixels * 2 + chartWidth | |
val imageHeight = imageMarginPixels * 2 + chartHeight | |
val image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB) | |
val graphics = image.getGraphics.asInstanceOf[Graphics2D] | |
graphics.setColor(Color.white) | |
graphics.fillRect(0, 0, imageWidth, imageHeight) | |
graphics.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON) | |
graphics.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY) | |
graphics.translate(imageMarginPixels, imageMarginPixels) | |
graphics.setColor(bandColour) | |
for (row <- 0 to (rowCount - 1)) { | |
if (row % 2 != 0) { | |
graphics.fillRect(0, (cellSizePixels + cellSpacingPixels) * row, chartWidth, cellSizePixels + cellSpacingPixels) | |
} | |
} | |
for (column <- 0 to (columnCount - 1)) { | |
if (column % 2 != 0) { | |
graphics.fillRect((cellSizePixels + cellSpacingPixels) * column, 0, cellSizePixels + cellSpacingPixels, chartHeight) | |
} | |
} | |
val font: Font = graphics.getFont.deriveFont(70) | |
graphics.setFont(font) | |
graphics.translate(cellSpacingPixels / 2, cellSpacingPixels / 2) | |
for ((circle, colour, value) <- circlesAndColoursAndValues.flatten.filterNot(_._3 == 0)) { | |
graphics.setColor(colour) | |
graphics.fill(circle) | |
graphics.setColor(circleOutlineColour) | |
graphics.draw(circle) | |
val glyphVector: GlyphVector = font.createGlyphVector(graphics.getFontRenderContext, value.toString) | |
val textBounds: Rectangle2D = glyphVector.getVisualBounds | |
val (originX, originY) = (circle.getCenterX.toInt - textBounds.getWidth.toInt / 2, circle.getCenterY.toInt + textBounds.getHeight.toInt * 2) | |
graphics.setColor(Color.black) | |
for (x <- List(-1, 0, 1); y <- List(-1, 0, 1)) { | |
graphics.drawGlyphVector(glyphVector, originX + x, originY + y) | |
} | |
graphics.setColor(Color.white) | |
graphics.drawGlyphVector(glyphVector, originX, originY) | |
} | |
ImageIO.write(image, "PNG", System.out) | |
//ImageIO.write(image, "PNG", new File(args(0))) | |
//ImageIO.write(image, "PNG", new File("Test.png")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment