Skip to content

Instantly share code, notes, and snippets.

@GrahamLea
Created February 26, 2013 07:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save GrahamLea/5036561 to your computer and use it in GitHub Desktop.
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.
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