diff --git a/demo/jvm-batik/src/main/kotlin/frontendContextDemo/scripts/QQ.kt b/demo/jvm-batik/src/main/kotlin/frontendContextDemo/scripts/QQ.kt index 9d117fa0c..9d7afe742 100644 --- a/demo/jvm-batik/src/main/kotlin/frontendContextDemo/scripts/QQ.kt +++ b/demo/jvm-batik/src/main/kotlin/frontendContextDemo/scripts/QQ.kt @@ -64,7 +64,7 @@ object QQ { p.show() } run { - val p = qqPlot(gData, sample = "y", group = "g", alpha = 0.8, color = "black", shape = 21) + + val p = qqPlot(gData, sample = "y", group = "g", alpha = 0.8, color = "black", shape = 21, marginal = "box:tr") + ggtitle("Bistro grouping") + xlab("Normal distribution quantiles") + scaleColorBrewer(type = "qual", palette = "Set1") + diff --git a/docs/examples/jupyter-notebooks/f-4.9.0/qq_plot_marginal.ipynb b/docs/examples/jupyter-notebooks/f-4.9.0/qq_plot_marginal.ipynb new file mode 100644 index 000000000..c007a0038 --- /dev/null +++ b/docs/examples/jupyter-notebooks/f-4.9.0/qq_plot_marginal.ipynb @@ -0,0 +1,1610 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "189a4ddc-2628-4c6f-a2fc-f23ddc52185c", + "metadata": {}, + "source": [ + "# Parameter `marginal` of the Q-Q Plot" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7025b5b4-5e88-48e2-a677-c369ad601fb8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%useLatestDescriptors\n", + "%use dataframe\n", + "%use lets-plot" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "56979abe-5025-49cc-ab8f-98bf51fbf184", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Lets-Plot Kotlin API v.0.0.0-SNAPSHOT. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.4.5.1." + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "LetsPlot.getInfo()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9d4abfc5-a145-4c7f-a7ef-212f1ec8735e", + "metadata": {}, + "outputs": [ + { + "data": { + "application/kotlindataframe+json": "{\"nrow\":5,\"ncol\":12,\"columns\":[\"untitled\",\"manufacturer\",\"model\",\"displ\",\"year\",\"cyl\",\"trans\",\"drv\",\"cty\",\"hwy\",\"fl\",\"class\"],\"kotlin_dataframe\":[{\"untitled\":1,\"manufacturer\":\"audi\",\"model\":\"a4\",\"displ\":1.8,\"year\":1999,\"cyl\":4,\"trans\":\"auto(l5)\",\"drv\":\"f\",\"cty\":18,\"hwy\":29,\"fl\":\"p\",\"class\":\"compact\"},{\"untitled\":2,\"manufacturer\":\"audi\",\"model\":\"a4\",\"displ\":1.8,\"year\":1999,\"cyl\":4,\"trans\":\"manual(m5)\",\"drv\":\"f\",\"cty\":21,\"hwy\":29,\"fl\":\"p\",\"class\":\"compact\"},{\"untitled\":3,\"manufacturer\":\"audi\",\"model\":\"a4\",\"displ\":2.0,\"year\":2008,\"cyl\":4,\"trans\":\"manual(m6)\",\"drv\":\"f\",\"cty\":20,\"hwy\":31,\"fl\":\"p\",\"class\":\"compact\"},{\"untitled\":4,\"manufacturer\":\"audi\",\"model\":\"a4\",\"displ\":2.0,\"year\":2008,\"cyl\":4,\"trans\":\"auto(av)\",\"drv\":\"f\",\"cty\":21,\"hwy\":30,\"fl\":\"p\",\"class\":\"compact\"},{\"untitled\":5,\"manufacturer\":\"audi\",\"model\":\"a4\",\"displ\":2.8,\"year\":1999,\"cyl\":6,\"trans\":\"auto(l5)\",\"drv\":\"f\",\"cty\":16,\"hwy\":26,\"fl\":\"p\",\"class\":\"compact\"}]}", + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "\n", + "

DataFrame: rowsCount = 5, columnsCount = 12

\n", + "
untitledmanufacturermodeldisplyearcyltransdrvctyhwyflclass
1audia41,80000019994auto(l5)f1829pcompact
2audia41,80000019994manual(m5)f2129pcompact
3audia42,00000020084manual(m6)f2031pcompact
4audia42,00000020084auto(av)f2130pcompact
5audia42,80000019996auto(l5)f1626pcompact
\n", + " \n", + " \n", + " " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "val df = DataFrame.readCSV(\"https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/mpg.csv\")\n", + "val dataMap = df.toMap()\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "855f2503-1275-4881-8140-2a4ff3337886", + "metadata": {}, + "source": [ + "## Default Plot" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "980cba2b-b223-476a-a375-bf23765a6ecb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gggrid(listOf(\n", + " qqPlot(dataMap, sample = \"hwy\") + ggtitle(\"Q-Q Plot\"),\n", + " qqPlot(dataMap, x = \"cty\", y = \"hwy\") + ggtitle(\"Q-Q-2 Plot\")\n", + "))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b04768ea-5ce2-4fd0-83c3-ec4e80c07e3d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "val marginals = listOf(\"none\", \"hist:tr\", \"dens:tr,hist:bl\", \"box : tr : .05, dens : bl\")\n", + "gggrid(marginals.map { marginal ->\n", + " qqPlot(dataMap, sample = \"hwy\", distribution = \"exp\", marginal=marginal) +\n", + " ggtitle(\"marginal = '${marginal}'\")\n", + "}, ncol = 2)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "codemirror_mode": "text/x-kotlin", + "file_extension": ".kt", + "mimetype": "text/x-kotlin", + "name": "kotlin", + "nbconvert_exporter": "", + "pygments_lexer": "kotlin", + "version": "1.9.23" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlot.kt b/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlot.kt index 71c835c05..5c650dc5c 100644 --- a/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlot.kt +++ b/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlot.kt @@ -42,6 +42,24 @@ package org.jetbrains.letsPlot.bistro.qq * If it is specified and color-parameters isn't then different groups will have different colors. * @param showLegend default = true. * false - do not show legend for this layer. + * @param marginal default = "dens:tr". + * Description of marginal layers packed to string value. + * Different marginals are separated by the ',' char. + * Parameters of a marginal are separated by the ':' char. + * + * First parameter of a marginal is a geometry name. + * Possible values: "dens"/"density", "hist"/"histogram", "box"/"boxplot". + * + * Second parameter is a string specifying which sides of the plot the marginal layer will appear on. + * Possible values: 't' (top), 'b' (bottom), 'l' (left), 'r' (right). + * + * Third parameter (optional) is size of marginal. + * To suppress marginals use `marginal="none"`. + * + * Examples: + * - "hist:tr:0.3", + * - "dens:tr,hist:bl", + * - "box:tr:.05, hist:bl, dens:bl". * @param color Color of a points. * For more info see: [aesthetics.html#color-and-fill](https://lets-plot.org/kotlin/aesthetics.html#color-and-fill). * @param fill Color to paint shape's inner points. @@ -74,6 +92,7 @@ fun qqPlot( quantiles: Pair? = null, group: String? = null, showLegend: Boolean = true, + marginal: String? = null, color: String? = null, fill: String? = null, alpha: Number? = null, @@ -92,6 +111,7 @@ fun qqPlot( quantiles, group, showLegend, + marginal, color, fill, alpha, diff --git a/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlotBuilder.kt b/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlotBuilder.kt index 76bd40787..d55d3a88d 100644 --- a/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlotBuilder.kt +++ b/plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/bistro/qq/QQPlotBuilder.kt @@ -5,137 +5,56 @@ package org.jetbrains.letsPlot.bistro.qq -import org.jetbrains.letsPlot.Stat -import org.jetbrains.letsPlot.core.plot.base.Aes -import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.QQPlotOptionsBuilder.Companion.DEF_DISTRIBUTION -import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.QQPlotOptionsBuilder.Companion.DEF_LINE_COLOR -import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.QQPlotOptionsBuilder.Companion.DEF_LINE_SIZE -import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.QQPlotOptionsBuilder.Companion.DEF_POINT_ALPHA -import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.QQPlotOptionsBuilder.Companion.DEF_POINT_SIZE -import org.jetbrains.letsPlot.geom.geomQQ -import org.jetbrains.letsPlot.geom.geomQQ2 -import org.jetbrains.letsPlot.geom.geomQQ2Line -import org.jetbrains.letsPlot.geom.geomQQLine -import org.jetbrains.letsPlot.intern.Feature -import org.jetbrains.letsPlot.intern.GenericAesMapping +import org.jetbrains.letsPlot.core.spec.Option.Plot.BISTRO +import org.jetbrains.letsPlot.core.spec.back.transform.bistro.qq.Option.QQ +import org.jetbrains.letsPlot.intern.OptionsMap import org.jetbrains.letsPlot.intern.Plot -import org.jetbrains.letsPlot.intern.asPlotData -import org.jetbrains.letsPlot.intern.layer.stat.QQ2StatMapping -import org.jetbrains.letsPlot.intern.layer.stat.QQStatMapping -import org.jetbrains.letsPlot.label.labs +import org.jetbrains.letsPlot.intern.filterNonNullValues import org.jetbrains.letsPlot.letsPlot -import org.jetbrains.letsPlot.scale.scaleColorDiscrete -import org.jetbrains.letsPlot.scale.scaleFillDiscrete internal class QQPlotBuilder( private val data: Map<*, *>, - private val sample: String? = null, - private val x: String? = null, - private val y: String? = null, - distribution: String? = null, - private val dParams: List? = null, - private val quantiles: Pair? = null, - private val group: String? = null, - private val showLegend: Boolean = true, - private val color: String? = null, - private val fill: String? = null, - alpha: Number? = null, - size: Number? = null, - private val shape: Int? = null, - lineColor: String? = null, - lineSize: Number? = null, - private val linetype: Any? = null + private val sample: String?, + private val x: String?, + private val y: String?, + private val distribution: String?, + private val dParams: List?, + private val quantiles: Pair?, + private val group: String?, + private val showLegend: Boolean, + private val marginal: String?, + private val color: String?, + private val fill: String?, + private val alpha: Number?, + private val size: Number?, + private val shape: Int?, + private val lineColor: String?, + private val lineSize: Number?, + private val linetype: Any? ) { - private val myDistribution = distribution ?: DEF_DISTRIBUTION - private val myLineColor = lineColor ?: if (group == null) DEF_LINE_COLOR else null - private val myAlpha = alpha ?: DEF_POINT_ALPHA - private val mySize = size ?: DEF_POINT_SIZE - private val myLineSize = lineSize ?: DEF_LINE_SIZE - fun build(): Plot { - val mapping: GenericAesMapping.() -> Unit = { - if (this@QQPlotBuilder.group != null) { - group = this@QQPlotBuilder.group - color = this@QQPlotBuilder.group - fill = this@QQPlotBuilder.group - } - } - return letsPlot(asPlotData(data), mapping) + layers() + scales() - } - - private fun layers() = if (sample != null) qqLayers() else qq2Layers() - - private fun qqLayers(): Feature { - val mapping = qqMapping() - return geomQQ( - showLegend = showLegend, - color = color, - fill = fill, - alpha = myAlpha, - size = mySize, - shape = shape, - stat = Stat.qq(myDistribution, dParams, mapping) - ) + geomQQLine( - showLegend = showLegend, - color = myLineColor, - size = myLineSize, - linetype = linetype, - stat = Stat.qqLine(myDistribution, dParams, quantiles, mapping) - ) - } - - private fun qq2Layers(): Feature { - val mapping = qq2Mapping() - return geomQQ2( - showLegend = showLegend, - color = color, - fill = fill, - alpha = myAlpha, - size = mySize, - shape = shape, - stat = Stat.qq2(mapping) - ) + geomQQ2Line( - showLegend = showLegend, - color = myLineColor, - size = myLineSize, - linetype = linetype, - stat = Stat.qq2Line(quantiles, mapping) - ) - } - - private fun qqMapping(): QQStatMapping.() -> Unit { - require(x == null) - { "Parameter x shouldn't be specified when parameter sample is." } - require(y == null) - { "Parameter y shouldn't be specified when parameter sample is." } - - val mapping: QQStatMapping.() -> Unit = { - sample = this@QQPlotBuilder.sample - } - return mapping - } - - private fun qq2Mapping(): QQ2StatMapping.() -> Unit { - require(x != null) - { "Parameter x should be specified when parameter sample isn't." } - require(y != null) - { "Parameter y should be specified when parameter sample isn't." } - - val mapping: QQ2StatMapping.() -> Unit = { - x = this@QQPlotBuilder.x - y = this@QQPlotBuilder.y - } - return mapping - } - - private fun scales(): Feature { - val scaleNames = mapOf( - Aes.X to if (sample != null) "\"$myDistribution\" distribution quantiles" else "$x quantiles", - Aes.Y to if (sample != null) "$sample quantiles" else "$y quantiles", + return letsPlot(data) + OptionsMap( + kind = BISTRO, + name = QQ.NAME, + options = mapOf( + QQ.SAMPLE to sample, + QQ.X to x, + QQ.Y to y, + QQ.DISTRIBUTION to distribution, + QQ.DISTRIBUTION_PARAMETERS to dParams, + QQ.QUANTILES to quantiles, + QQ.GROUP to group, + QQ.SHOW_LEGEND to showLegend, + QQ.MARGINAL to marginal, + QQ.POINT_COLOR to color, + QQ.POINT_FILL to fill, + QQ.POINT_ALPHA to alpha, + QQ.POINT_SIZE to size, + QQ.POINT_SHAPE to shape, + QQ.LINE_COLOR to lineColor, + QQ.LINE_SIZE to lineSize, + QQ.LINE_TYPE to linetype + ).filterNonNullValues() ) - return labs( - x = scaleNames[Aes.X], - y = scaleNames[Aes.Y] - ) + scaleColorDiscrete() + scaleFillDiscrete() } } \ No newline at end of file