smile.plot.vega.VegaLite.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2010-2021 Haifeng Li. All rights reserved.
*
* Smile is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Smile is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Smile. If not, see .
*/
package smile.plot.vega
import smile.data._
import smile.json._
/** Vega-Lite specifications are JSON objects that describe a diverse range
* of interactive visualizations. Besides using a single view specification
* as a standalone visualization, Vega-Lite also provides operators for
* composing multiple view specifications into a layered or multi-view
* specification. These operators include layer, facet, concat, and repeat.
*/
trait VegaLite {
/** The specification */
val spec: JsObject
override def toString: String = spec.prettyPrint
// ====== Properties of Top-Level Specifications ======
/** CSS color property to use as the background of the entire view. */
def background(color: String): VegaLite = {
spec.background = color
this
}
/** Specifies padding for all sides.
* The visualization padding, in pixels, is from the edge of the
* visualization canvas to the data rectangle.
*/
def padding(size: Int): VegaLite = {
spec.padding = size
this
}
/** Specifies padding for each side.
* The visualization padding, in pixels, is from the edge of the
* visualization canvas to the data rectangle.
*/
def padding(left: Int, top: Int, right: Int, bottom: Int): VegaLite = {
spec.padding = JsObject(
"left" -> left,
"top" -> top,
"right" -> right,
"bottom" -> bottom
)
this
}
/** Sets the overall size of the visualization. The total size of
* a Vega-Lite visualization may be determined by multiple factors:
* specified width, height, and padding values, as well as content
* such as axes, legends, and titles.
*
* @param `type` The sizing format type. One of "pad", "fit", "fit-x",
* "fit-y", or "none". See the [[https://vega.github.io/vega-lite/docs/size.html#autosize autosize type]]
* documentation for descriptions of each.
* @param resize A boolean flag indicating if autosize layout should
* be re-calculated on every view update.
* @param contains Determines how size calculation should be performed,
* one of "content" or "padding". The default setting
* ("content") interprets the width and height settings
* as the data rectangle (plotting) dimensions, to which
* padding is then added. In contrast, the "padding"
* setting includes the padding within the view size
* calculations, such that the width and height settings
* indicate the total intended size of the view.
*/
def autosize(`type`: String = "pad", resize: Boolean = false, contains: String = "content"): VegaLite = {
spec.autosize = JsObject(
"type" -> `type`,
"resize" -> resize,
"contains" -> contains
)
this
}
/** Sets Vega-Lite configuration object that lists configuration properties
* of a visualization for creating a consistent theme. This property can
* only be defined at the top-level of a specification.
*/
def config(properties: JsObject): VegaLite = {
spec.config = properties
this
}
/** Optional metadata that will be passed to Vega. This object is completely
* ignored by Vega and Vega-Lite and can be used for custom metadata.
*/
def usermeta(data: JsValue): VegaLite = {
spec.usermeta = data
this
}
// ====== Common Properties ======
/** Sets the name of the visualization for later reference. */
def name(name: String): VegaLite = {
spec.name = name
this
}
/** Sets the description of this mark for commenting purpose. */
def description(description: String): VegaLite = {
spec.description = description
this
}
/** Sets a descriptive title to a chart. */
def title(title: String): VegaLite = {
spec.title = title
this
}
/** Sets a JSON array describing the data source. Set to null to ignore
* the parent’s data source. If no data is set, it is derived from
* the parent.
*/
def data(json: JsArray): VegaLite = {
if (json == null || json == JsNull || json == JsUndefined) spec.remove("data")
else spec.data = JsObject("values" -> json)
this
}
/** Sets an array of objects describing the data source.
*/
def data(rows: JsObject*): VegaLite = {
data(JsArray(rows: _*))
}
/** Sets a data frame describing the data source.
*/
def data(df: DataFrame): VegaLite = {
data(df.toJSON)
}
/** Sets the url of the data source.
*
* @param url A URL from which to load the data set.
* @param format Type of input data: "json", "csv", "tsv", "dsv".
* Default value: The default format type is determined
* by the extension of the file URL. If no extension is
* detected, "json" will be used by default.
*/
def data(url: String, format: JsValue = JsUndefined): VegaLite = {
spec.data = JsObject("url" -> JsString(url))
format match {
case format: JsObject => spec.data.format = format
case format: JsString => spec.data.format = JsObject("type" -> format)
case _ => ()
}
this
}
/** An array of data transformations such as filter and new field
* calculation. Data transformations in Vega-Lite are described
* via either view-level transforms (the transform property) or
* field transforms inside encoding (bin, timeUnit, aggregate,
* sort, and stack).
*
* When both types of transforms are specified, the view-level
* transforms are executed first based on the order in the
* array. Then the inline transforms are executed in this order:
* bin, timeUnit, aggregate, sort, and stack.
*/
def transform(transforms: JsArray): VegaLite = {
spec.transform = transforms
this
}
/** An array of data transformations such as filter and new field
* calculation. Data transformations in Vega-Lite are described
* via either view-level transforms (the transform property) or
* field transforms inside encoding (bin, timeUnit, aggregate,
* sort, and stack).
*
* When both types of transforms are specified, the view-level
* transforms are executed first based on the order in the
* array. Then the inline transforms are executed in this order:
* bin, timeUnit, aggregate, sort, and stack.
*/
def transform(transforms: JsObject*): VegaLite = {
spec.transform = JsArray(transforms: _*)
this
}
/** Returns the HTML of plot specification with Vega Embed. */
def embed: String = {
s"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""".stripMargin
}
/** Returns the HTML wrapped in an iframe to render in notebooks.
* @param id the iframe HTML id.
*/
def iframe(id: String = java.util.UUID.randomUUID.toString): String = {
val src = xml.Utility.escape(embed)
s"""
|
|
""".stripMargin
}
}
object VegaLite {
/** The schema of Vega-Lite. */
val $schema = "https://vega.github.io/schema/vega-lite/v5.json"
/** The MIME type of Vega-Lite. */
val mime: String = "application/vnd.vegalite.v5+json"
/** Returns a single view specification with inline data. */
def apply(rows: JsObject*): View = {
new View {
override val spec: JsObject = of(JsArray(rows: _*))
}
}
/** Returns a single view specification with inline data. */
def apply(json: JsArray): View = {
new View {
override val spec: JsObject = of(json)
}
}
/** Returns a single view specification with inline data. */
def apply(df: DataFrame): View = {
new View {
override val spec: JsObject = of(df)
}
}
/** Returns a single view specification with data from a URL.
*
* @param url A URL from which to load the data set.
* @param format Type of input data: "json", "csv", "tsv", "dsv".
* Default value: The default format type is determined
* by the extension of the file URL. If no extension is
* detected, "json" will be used by default.
*/
def apply(url: String, format: JsValue = JsUndefined): View = {
new View {
override val spec: JsObject = of(url, format)
}
}
/** Returns a single view specification to be used in a composition.
* @param init Initial specification.
*/
def view(init: JsObject = JsObject()): View = {
new View {
override val spec: JsObject = init
}
}
/** Returns a facet specification with inline data. */
def facet(rows: JsObject*): Facet = {
new Facet {
override val spec: JsObject = of(JsArray(rows: _*))
}
}
/** Returns a facet specification with inline data. */
def facet(json: JsArray): Facet = {
new Facet {
override val spec: JsObject = of(json)
}
}
/** Returns a facet specification with inline data. */
def facet(df: DataFrame): Facet = {
facet(df.toJSON)
}
/** Returns a facet specification with data from a URL.
*
* @param url An URL from which to load the data set.
* @param format Type of input data: "json", "csv", "tsv", "dsv".
* Default value: The default format type is determined
* by the extension of the file URL. If no extension is
* detected, "json" will be used by default.
*/
def facet(url: String, format: JsValue = JsUndefined): Facet = {
new Facet {
override val spec: JsObject = of(url, format)
}
}
/** Returns a layered view specification. */
def layer(layers: View*): Layer = {
new Layer {
override val spec: JsObject = of()
super.layer(layers: _*)
}
}
/** Returns a layered view specification. */
def layer(json: JsArray, layers: View*): Layer = {
new Layer {
override val spec: JsObject = of(json)
super.layer(layers: _*)
}
}
/** Returns a layered view specification. */
def layer(df: DataFrame, layers: View*): Layer = {
layer(df.toJSON, layers: _*)
}
/** Returns a layered view specification. */
def layer(url: String, format: JsValue, layers: View*): Layer = {
new Layer {
override val spec: JsObject = of(url, format)
super.layer(layers: _*)
}
}
/** Horizontal concatenation. Put multiple views into a column. */
def hconcat(views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of()
spec.hconcat = JsArray(views.map(_.spec): _*)
}
}
/** Horizontal concatenation. Put multiple views into a column. */
def hconcat(json: JsArray, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(json)
spec.hconcat = JsArray(views.map(_.spec): _*)
}
}
/** Horizontal concatenation. Put multiple views into a column. */
def hconcat(df: DataFrame, views: VegaLite*): ViewLayoutComposition = {
hconcat(df.toJSON, views: _*)
}
/** Horizontal concatenation. Put multiple views into a column. */
def hconcat(url: String, format: JsValue, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(url, format)
spec.hconcat = JsArray(views.map(_.spec): _*)
}
}
/** Vertical concatenation. Put multiple views into a row. */
def vconcat(views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of()
spec.vconcat = JsArray(views.map(_.spec): _*)
}
}
/** Vertical concatenation. Put multiple views into a row. */
def vconcat(json: JsArray, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(json)
spec.vconcat = JsArray(views.map(_.spec): _*)
}
}
/** Vertical concatenation. Put multiple views into a row. */
def vconcat(df: DataFrame, views: VegaLite*): ViewLayoutComposition = {
vconcat(df.toJSON, views: _*)
}
/** Vertical concatenation. Put multiple views into a row. */
def vconcat(url: String, format: JsValue, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(url, format)
spec.vconcat = JsArray(views.map(_.spec): _*)
}
}
/** General (wrappable) concatenation. Put multiple views into
* a flexible flow layout.
*/
def concat(columns: Int, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of()
spec.columns = columns
spec.concat = JsArray(views.map(_.spec): _*)
}
}
/** General (wrappable) concatenation. Put multiple views into
* a flexible flow layout.
*/
def concat(json: JsArray, columns: Int, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(json)
spec.columns = columns
spec.concat = JsArray(views.map(_.spec): _*)
}
}
/** General (wrappable) concatenation. Put multiple views into
* a flexible flow layout.
*/
def concat(df: DataFrame, columns: Int, views: VegaLite*): ViewLayoutComposition = {
concat(df.toJSON, columns, views: _*)
}
/** General (wrappable) concatenation. Put multiple views into
* a flexible flow layout.
*/
def concat(url: String, format: JsValue, columns: Int, views: VegaLite*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(url, format)
spec.columns = columns
spec.concat = JsArray(views.map(_.spec): _*)
}
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param fields The fields that should be used for each entry.
*/
def repeat(json: JsArray, view: VegaLite, fields: String*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(json)
spec.repeat = JsObject("layer" -> JsArray(fields.map(JsString(_)): _*))
spec.spec = view.spec
}
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param fields The fields that should be used for each entry.
*/
def repeat(df: DataFrame, view: VegaLite, fields: String*): ViewLayoutComposition = {
repeat(df.toJSON, view, fields: _*)
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param fields The fields that should be used for each entry.
*/
def repeat(url: String, format: JsValue, view: VegaLite, fields: String*): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(url, format)
spec.repeat = JsObject("layer" -> JsArray(fields.map(JsString(_)): _*))
spec.spec = view.spec
}
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param row An array of fields to be repeated vertically.
* @param column An array of fields to be repeated horizontally.
*/
def repeat(json: JsArray, view: VegaLite, row: Seq[String], column: Seq[String]): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(json)
spec.repeat = JsObject(
"row" -> JsArray(row.map(JsString(_)): _*),
"column" -> JsArray(column.map(JsString(_)): _*)
)
spec.spec = view.spec
}
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param row An array of fields to be repeated vertically.
* @param column An array of fields to be repeated horizontally.
*/
def repeat(df: DataFrame, view: VegaLite, row: Seq[String], column: Seq[String]): ViewLayoutComposition = {
repeat(df.toJSON, view, row, column)
}
/** Creates a view for each entry in an array of fields. This operator
* generates multiple plots like facet. However, unlike facet it allows
* full replication of a data set in each view.
*
* @param row An array of fields to be repeated vertically.
* @param column An array of fields to be repeated horizontally.
*/
def repeat(url: String, format: JsValue, view: VegaLite, row: Seq[String], column: Seq[String]): ViewLayoutComposition = {
new ViewLayoutComposition {
override val spec: JsObject = of(url, format)
spec.repeat = JsObject(
"row" -> JsArray(row.map(JsString(_)): _*),
"column" -> JsArray(column.map(JsString(_)): _*)
)
spec.spec = view.spec
}
}
/** Scatterplot Matrix (SPLOM). */
def splom(df: DataFrame, color: String = ""): ViewLayoutComposition = {
val fields = df.names.filter(_ != color).toIndexedSeq
val spec = json"""{
| "mark": "point",
| "encoding": {
| "x": {
| "field": {"repeat": "column"},
| "type": "quantitative",
| "scale": {"zero": false}
| },
| "y": {
| "field": {"repeat": "row"},
| "type": "quantitative",
| "scale": {"zero": false}
| }
| }
|}"""
if (color.nonEmpty)
spec.encoding.color = JsObject(
"field" -> color,
"type" -> "nominal"
)
repeat(df, view(spec), fields.reverse, fields)
}
/** Returns a specification.
* @param properties The properties of top-Level specifications
* or common properties.
*/
private def of(properties: (String, JsValue)*): JsObject = {
val vega = JsObject(
"$schema" -> $schema,
"config" -> JsObject(
"view" -> JsObject(
"continuousWidth" -> 400,
"continuousHeight" -> 400
)
)
)
properties.foreach { case (field , value) =>
vega(field) = value
}
vega
}
/** Returns a specification object. */
private def of(df: JsArray): JsObject = {
of("data" -> JsObject("values" -> df))
}
/** Returns a specification object. */
private def of(df: DataFrame): JsObject = {
of("data" -> JsObject("values" -> df.toJSON))
}
/** Returns a specification object. */
private def of(url: String, format: JsValue = JsUndefined): JsObject = {
val data = JsObject("url" -> JsString(url))
format match {
case format: JsObject => data.format = format
case format: JsString => data.format = JsObject("type" -> format)
case _ => ()
}
of("data" -> data)
}
}