diff --git a/docs/f-21-12/notebooks/geom_violin.ipynb b/docs/f-21-12/notebooks/geom_violin.ipynb
new file mode 100644
index 00000000000..c1f244d0778
--- /dev/null
+++ b/docs/f-21-12/notebooks/geom_violin.ipynb
@@ -0,0 +1,1242 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "\n",
+ "from lets_plot import *\n",
+ "from lets_plot.mapping import as_discrete\n",
+ "LetsPlot.setup_html()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Test datasets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " sepal_length | \n",
+ " sepal_width | \n",
+ " petal_length | \n",
+ " petal_width | \n",
+ " species | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 5.1 | \n",
+ " 3.5 | \n",
+ " 1.4 | \n",
+ " 0.2 | \n",
+ " setosa | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 4.9 | \n",
+ " 3.0 | \n",
+ " 1.4 | \n",
+ " 0.2 | \n",
+ " setosa | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 4.7 | \n",
+ " 3.2 | \n",
+ " 1.3 | \n",
+ " 0.2 | \n",
+ " setosa | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4.6 | \n",
+ " 3.1 | \n",
+ " 1.5 | \n",
+ " 0.2 | \n",
+ " setosa | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5.0 | \n",
+ " 3.6 | \n",
+ " 1.4 | \n",
+ " 0.2 | \n",
+ " setosa | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " sepal_length sepal_width petal_length petal_width species\n",
+ "0 5.1 3.5 1.4 0.2 setosa\n",
+ "1 4.9 3.0 1.4 0.2 setosa\n",
+ "2 4.7 3.2 1.3 0.2 setosa\n",
+ "3 4.6 3.1 1.5 0.2 setosa\n",
+ "4 5.0 3.6 1.4 0.2 setosa"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "iris_df = pd.read_csv(\"https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/iris.csv\")\n",
+ "\n",
+ "iris_df.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " species | \n",
+ " sepal_length | \n",
+ " weight | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " setosa | \n",
+ " 4.300000 | \n",
+ " 0.222676 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " setosa | \n",
+ " 4.302935 | \n",
+ " 0.228662 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " setosa | \n",
+ " 4.305871 | \n",
+ " 0.234639 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " setosa | \n",
+ " 4.308806 | \n",
+ " 0.240684 | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " setosa | \n",
+ " 4.311742 | \n",
+ " 0.246886 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " species sepal_length weight\n",
+ "0 setosa 4.300000 0.222676\n",
+ "1 setosa 4.302935 0.228662\n",
+ "2 setosa 4.305871 0.234639\n",
+ "3 setosa 4.308806 0.240684\n",
+ "4 setosa 4.311742 0.246886"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def construct_violin_df(df, xname, yname, n=512):\n",
+ " from functools import reduce\n",
+ "\n",
+ " from scipy.stats import gaussian_kde\n",
+ "\n",
+ " def get_weights(values):\n",
+ " def nrd0_bw(kde):\n",
+ " iqr = np.quantile(kde.dataset, .75) - np.quantile(kde.dataset, .25)\n",
+ " std = np.std(kde.dataset)\n",
+ " size = kde.dataset.size\n",
+ " if iqr > 0:\n",
+ " return .9 * min(std, iqr / 1.34) * (size ** -.2)\n",
+ " if std > 0:\n",
+ " return .9 * std * (size ** -.2)\n",
+ "\n",
+ " yrange = np.linspace(values.min(), values.max(), n)\n",
+ "\n",
+ " return {yname: yrange, 'weight': gaussian_kde(values, bw_method=nrd0_bw)(yrange)}\n",
+ "\n",
+ " def reducer(agg_df, xval):\n",
+ " weights = get_weights(df[df[xname] == xval][yname])\n",
+ " y = weights[yname]\n",
+ " x = [xval] * y.size\n",
+ " w = weights['weight']\n",
+ "\n",
+ " return pd.concat([agg_df, pd.DataFrame({xname: x, yname: y, 'weight': w})], ignore_index=True)\n",
+ "\n",
+ " return reduce(reducer, df[xname], pd.DataFrame(columns=[xname, yname, 'weight']))\n",
+ "\n",
+ "violin_df = construct_violin_df(iris_df, 'species', 'sepal_length')\n",
+ "violin_df.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " v | \n",
+ " c1 | \n",
+ " c2 | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 0.496714 | \n",
+ " A | \n",
+ " b | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " -0.138264 | \n",
+ " B | \n",
+ " b | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 0.647689 | \n",
+ " A | \n",
+ " a | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 1.523030 | \n",
+ " A | \n",
+ " a | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " -0.234153 | \n",
+ " C | \n",
+ " a | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " v c1 c2\n",
+ "0 0.496714 A b\n",
+ "1 -0.138264 B b\n",
+ "2 0.647689 A a\n",
+ "3 1.523030 A a\n",
+ "4 -0.234153 C a"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "size = 100\n",
+ "np.random.seed(42)\n",
+ "random_df = pd.DataFrame({\n",
+ " 'v': np.random.normal(size=size),\n",
+ " 'c1': np.random.choice(['A', 'B', 'C'], size=size),\n",
+ " 'c2': np.random.choice(['a', 'b'], size=size)\n",
+ "})\n",
+ "\n",
+ "random_df.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " v | \n",
+ " c1 | \n",
+ " c2 | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 0.496714 | \n",
+ " A | \n",
+ " b | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " -0.138264 | \n",
+ " NaN | \n",
+ " b | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " NaN | \n",
+ " A | \n",
+ " a | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 1.523030 | \n",
+ " A | \n",
+ " NaN | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " -0.234153 | \n",
+ " C | \n",
+ " a | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " v c1 c2\n",
+ "0 0.496714 A b\n",
+ "1 -0.138264 NaN b\n",
+ "2 NaN A a\n",
+ "3 1.523030 A NaN\n",
+ "4 -0.234153 C a"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def mask(p=.1, seed=42):\n",
+ " np.random.seed(seed)\n",
+ " return np.random.choice([True, False], random_df.shape[0], p=[p, 1 - p])\n",
+ "\n",
+ "nullable_df = random_df.copy()\n",
+ "nullable_df.loc[mask(seed=1), 'v'] = np.nan\n",
+ "nullable_df.loc[mask(seed=2), 'c1'] = np.nan\n",
+ "nullable_df.loc[mask(seed=6), 'c2'] = np.nan\n",
+ "\n",
+ "nullable_df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Minimalistic example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(random_df, aes(y='v')) + geom_violin() + ggtitle(\"Simplest example\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Comparison of geoms"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "p_d = ggplot(iris_df) + \\\n",
+ " geom_density(aes(x='sepal_length', fill='species'), color='black', alpha=.7) + \\\n",
+ " facet_grid(x='species') + \\\n",
+ " coord_flip() + \\\n",
+ " ggtitle(\"geom_density()\")\n",
+ "p_v = ggplot(iris_df, aes('species', 'sepal_length')) + \\\n",
+ " geom_violin(aes(fill='species'), alpha=.7) + \\\n",
+ " ggtitle(\"geom_violin()\")\n",
+ "\n",
+ "w, h = 400, 300\n",
+ "bunch = GGBunch()\n",
+ "bunch.add_plot(p_d, 0, 0, w, h)\n",
+ "bunch.add_plot(p_v, w, 0, w, h)\n",
+ "bunch.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Custom density parameters"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "p = ggplot(iris_df, aes('species', 'sepal_length'))\n",
+ "p_default = p + geom_violin() + ggtitle(\"Default\")\n",
+ "p_kernel = p + geom_violin(kernel='epanechikov') + ggtitle(\"kernel='epanechikov'\")\n",
+ "p_bw = p + geom_violin(bw=.1) + ggtitle(\"bw=0.1\")\n",
+ "p_adjust = p + geom_violin(adjust=2) + ggtitle(\"adjust=2\")\n",
+ "\n",
+ "w, h = 400, 300\n",
+ "bunch = GGBunch()\n",
+ "bunch.add_plot(p_default, 0, 0, w, h)\n",
+ "bunch.add_plot(p_kernel, w, 0, w, h)\n",
+ "bunch.add_plot(p_bw, 0, h, w, h)\n",
+ "bunch.add_plot(p_adjust, w, h, w, h)\n",
+ "bunch.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Grouping and tooltips"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(random_df, aes(x='c1', y='v')) + \\\n",
+ " geom_violin(aes(fill='c2'), tooltips=layer_tooltips().line('^x')\n",
+ " .line('category|@c2')\n",
+ " .line('v|@v')\n",
+ " .line('@|@..density..')\n",
+ " .line('count|@..count..')\n",
+ " .line('scaled|@..scaled..')) + \\\n",
+ " ggtitle(\"Grouping and tooltips\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## `coord_flip()`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(iris_df, aes('species', 'sepal_length')) + \\\n",
+ " geom_violin() + \\\n",
+ " coord_flip() + \\\n",
+ " ggtitle(\"Use coord_flip()\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## \"identity\" statistic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(violin_df, aes('species', 'sepal_length')) + \\\n",
+ " geom_violin(aes(weight='weight'), stat='identity') + \\\n",
+ " ggtitle(\"Use 'identity' statistic\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Additional layers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(random_df, aes(as_discrete('c1', order=-1), 'v')) + \\\n",
+ " geom_violin(aes(color='c1', fill='c1'), alpha=.5, size=2, \\\n",
+ " sampling=sampling_group_systematic(2)) + \\\n",
+ " facet_grid(x='c2') + \\\n",
+ " scale_y_continuous(breaks=list(np.linspace(-3, 3, 9))) + \\\n",
+ " scale_color_brewer(type='qual', palette='Set1') + \\\n",
+ " scale_fill_brewer(type='qual', palette='Set1') + \\\n",
+ " ylim(-3, 3) + \\\n",
+ " coord_fixed(ratio=.5) + \\\n",
+ " theme_grey() + \\\n",
+ " ggtitle(\"Some additional aesthetics, parameters and layers\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Dataset with NaN's"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ggplot(nullable_df, aes('c1', 'v')) + geom_violin()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Aes.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Aes.kt
index 2c55d533da2..ec1a50fd742 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Aes.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Aes.kt
@@ -39,6 +39,7 @@ class Aes private constructor(val name: String, val isNumeric: Boolean = true
val SIZE: Aes = Aes("size")
val WIDTH: Aes = Aes("width")
val HEIGHT: Aes = Aes("height")
+ val VIOLINWIDTH: Aes = Aes("violinwidth")
val WEIGHT: Aes = Aes("weight")
val INTERCEPT: Aes = Aes("intercept")
val SLOPE: Aes = Aes("slope")
@@ -153,6 +154,7 @@ class Aes private constructor(val name: String, val isNumeric: Boolean = true
aes == SLOPE ||
aes == WIDTH ||
aes == HEIGHT ||
+ aes == VIOLINWIDTH ||
aes == HJUST ||
aes == VJUST ||
aes == ANGLE ||
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataPointAesthetics.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataPointAesthetics.kt
index 6c6a68b23be..02d3c411177 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataPointAesthetics.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataPointAesthetics.kt
@@ -38,6 +38,8 @@ interface DataPointAesthetics {
fun height(): Double?
+ fun violinwidth(): Double?
+
fun weight(): Double?
fun intercept(): Double?
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomKind.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomKind.kt
index 5aff2f9a848..a1af1860cb4 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomKind.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomKind.kt
@@ -23,6 +23,7 @@ enum class GeomKind {
H_LINE,
V_LINE,
BOX_PLOT,
+ VIOLIN,
LIVE_MAP,
POINT,
RIBBON,
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomMeta.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomMeta.kt
index 77cfdd9af4b..981dbb71ef2 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomMeta.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/GeomMeta.kt
@@ -211,6 +211,18 @@ object GeomMeta {
Aes.WIDTH
)
+ GeomKind.VIOLIN -> listOf(
+ Aes.X,
+ Aes.Y,
+ Aes.VIOLINWIDTH,
+
+ Aes.ALPHA,
+ Aes.COLOR,
+ Aes.FILL,
+ Aes.LINETYPE,
+ Aes.SIZE
+ )
+
GeomKind.RIBBON -> listOf(
Aes.X,
Aes.YMIN, Aes.YMAX,
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Stat.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Stat.kt
index 417dd00221d..2eb003c9e01 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Stat.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Stat.kt
@@ -8,6 +8,8 @@ package jetbrains.datalore.plot.base
interface Stat {
fun apply(data: DataFrame, statCtx: StatContext, messageConsumer: (s: String) -> Unit = {}): DataFrame
+ fun normalize(dataAfterStat: DataFrame): DataFrame
+
fun consumes(): List>
fun hasDefaultMapping(aes: Aes<*>): Boolean
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesInitValue.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesInitValue.kt
index 658128c5db3..fdcd75fa3f5 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesInitValue.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesInitValue.kt
@@ -32,6 +32,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -67,6 +68,7 @@ object AesInitValue {
VALUE_MAP[SIZE] = 0.5 // Line thickness. Should be redefined for other shapes
VALUE_MAP[WIDTH] = 1.0
VALUE_MAP[HEIGHT] = 1.0
+ VALUE_MAP[VIOLINWIDTH] = 0.0
VALUE_MAP[WEIGHT] = 1.0
VALUE_MAP[INTERCEPT] = 0.0
VALUE_MAP[SLOPE] = 1.0
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesVisitor.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesVisitor.kt
index 563eaa79a15..e2135fb1b6c 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesVisitor.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesVisitor.kt
@@ -29,6 +29,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -103,6 +104,9 @@ abstract class AesVisitor {
if (aes == HEIGHT) {
return height()
}
+ if (aes == VIOLINWIDTH) {
+ return violinwidth()
+ }
if (aes == WEIGHT) {
return weight()
}
@@ -207,6 +211,8 @@ abstract class AesVisitor {
protected abstract fun height(): T
+ protected abstract fun violinwidth(): T
+
protected abstract fun weight(): T
protected abstract fun intercept(): T
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsBuilder.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsBuilder.kt
index 1d762aa897f..00412ebb828 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsBuilder.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsBuilder.kt
@@ -32,6 +32,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -398,6 +399,10 @@ class AestheticsBuilder @JvmOverloads constructor(private var myDataPointCount:
return get(HEIGHT)
}
+ override fun violinwidth(): Double {
+ return get(VIOLINWIDTH)
+ }
+
override fun weight(): Double {
return get(WEIGHT)
}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt
index 68e0c41c722..1af0cd854b0 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt
@@ -143,6 +143,12 @@ open class AestheticsDefaults {
return crossBar()
}
+ fun violin(): AestheticsDefaults {
+ return AestheticsDefaults()
+ .update(Aes.COLOR, Color.BLACK)
+ .update(Aes.FILL, Color.WHITE)
+ }
+
fun livemap(displayMode: LivemapConstants.DisplayMode): AestheticsDefaults {
return when (displayMode) {
LivemapConstants.DisplayMode.POINT -> point()
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/TransformVar.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/TransformVar.kt
index c4196b24826..947a3ff84c4 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/TransformVar.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/TransformVar.kt
@@ -24,6 +24,7 @@ object TransformVar {
val SIZE = DataFrame.Variable("transform.SIZE", TRANSFORM)
val WIDTH = DataFrame.Variable("transform.WIDTH", TRANSFORM)
val HEIGHT = DataFrame.Variable("transform.HEIGHT", TRANSFORM)
+ val VIOLINWIDTH = DataFrame.Variable("transform.VIOLINWIDTH", TRANSFORM)
val WEIGHT = DataFrame.Variable("transform.WEIGHT", TRANSFORM)
val INTERCEPT = DataFrame.Variable("transform.INTERCEPT", TRANSFORM)
val SLOPE = DataFrame.Variable("transform.SLOPE", TRANSFORM)
@@ -129,6 +130,10 @@ object TransformVar {
return HEIGHT
}
+ override fun violinwidth(): DataFrame.Variable {
+ return VIOLINWIDTH
+ }
+
override fun weight(): DataFrame.Variable {
return WEIGHT
}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/ViolinGeom.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/ViolinGeom.kt
new file mode 100644
index 00000000000..5ed05c82a2d
--- /dev/null
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/ViolinGeom.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2021. JetBrains s.r.o.
+ * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
+ */
+
+package jetbrains.datalore.plot.base.geom
+
+import jetbrains.datalore.base.geometry.DoubleVector
+import jetbrains.datalore.plot.base.*
+import jetbrains.datalore.plot.base.geom.util.*
+import jetbrains.datalore.plot.base.interact.GeomTargetCollector.TooltipParams
+import jetbrains.datalore.plot.base.interact.TipLayoutHint
+import jetbrains.datalore.plot.base.render.SvgRoot
+
+class ViolinGeom : GeomBase() {
+
+ override fun buildIntern(
+ root: SvgRoot,
+ aesthetics: Aesthetics,
+ pos: PositionAdjustment,
+ coord: CoordinateSystem,
+ ctx: GeomContext
+ ) {
+ buildLines(root, aesthetics, pos, coord, ctx)
+ }
+
+ private fun buildLines(
+ root: SvgRoot,
+ aesthetics: Aesthetics,
+ pos: PositionAdjustment,
+ coord: CoordinateSystem,
+ ctx: GeomContext
+ ) {
+ GeomUtil.withDefined(aesthetics.dataPoints(), Aes.X, Aes.Y, Aes.VIOLINWIDTH)
+ .groupBy(DataPointAesthetics::x)
+ .map { (x, nonOrderedPoints) -> x to GeomUtil.ordered_Y(nonOrderedPoints, false) }
+ .forEach { (_, dataPoints) -> buildViolin(root, dataPoints, pos, coord, ctx) }
+ }
+
+ private fun buildViolin(
+ root: SvgRoot,
+ dataPoints: Iterable,
+ pos: PositionAdjustment,
+ coord: CoordinateSystem,
+ ctx: GeomContext
+ ) {
+ val helper = LinesHelper(pos, coord, ctx)
+ val leftBoundTransform = toLocationBound(-1.0, ctx)
+ val rightBoundTransform = toLocationBound(1.0, ctx)
+
+ val paths = helper.createBands(dataPoints, leftBoundTransform, rightBoundTransform)
+ appendNodes(paths, root)
+
+ helper.setAlphaEnabled(false)
+ appendNodes(helper.createLines(dataPoints, leftBoundTransform), root)
+ appendNodes(helper.createLines(dataPoints, rightBoundTransform), root)
+
+ buildHints(dataPoints, ctx, helper, leftBoundTransform)
+ buildHints(dataPoints, ctx, helper, rightBoundTransform)
+ }
+
+ private fun toLocationBound(
+ sign: Double,
+ ctx: GeomContext
+ ): (p: DataPointAesthetics) -> DoubleVector {
+ return fun (p: DataPointAesthetics): DoubleVector {
+ val x = p.x()!! + ctx.getResolution(Aes.X) / 2 * WIDTH_SCALE * sign * p.violinwidth()!!
+ val y = p.y()!!
+ return DoubleVector(x, y)
+ }
+ }
+
+ private fun buildHints(
+ dataPoints: Iterable,
+ ctx: GeomContext,
+ helper: GeomHelper,
+ boundTransform: (p: DataPointAesthetics) -> DoubleVector
+ ) {
+ val multiPointDataList = MultiPointDataConstructor.createMultiPointDataByGroup(
+ dataPoints,
+ MultiPointDataConstructor.singlePointAppender { p ->
+ boundTransform(p).let { helper.toClient(it, p) }
+ },
+ MultiPointDataConstructor.reducer(0.999, false)
+ )
+ val targetCollector = getGeomTargetCollector(ctx)
+ for (multiPointData in multiPointDataList) {
+ targetCollector.addPath(
+ multiPointData.points,
+ multiPointData.localToGlobalIndex,
+ TooltipParams.params().setColor(HintColorUtil.fromFill(multiPointData.aes)),
+ if (ctx.flipped) {
+ TipLayoutHint.Kind.VERTICAL_TOOLTIP
+ } else {
+ TipLayoutHint.Kind.HORIZONTAL_TOOLTIP
+ }
+ )
+ }
+ }
+
+ companion object {
+ const val HANDLES_GROUPS = true
+ const val WIDTH_SCALE = 0.95
+ }
+
+}
\ No newline at end of file
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/DataPointAestheticsDelegate.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/DataPointAestheticsDelegate.kt
index 1ee2b01e061..b070dee4cf1 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/DataPointAestheticsDelegate.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/DataPointAestheticsDelegate.kt
@@ -70,6 +70,10 @@ open class DataPointAestheticsDelegate(private val p: DataPointAesthetics) :
return p.height()
}
+ override fun violinwidth(): Double? {
+ return p.violinwidth()
+ }
+
override fun weight(): Double? {
return p.weight()
}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/BaseStat.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/BaseStat.kt
index 6a87b44da29..f4cafc98463 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/BaseStat.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/BaseStat.kt
@@ -11,6 +11,9 @@ import jetbrains.datalore.plot.base.Stat
import jetbrains.datalore.plot.base.data.TransformVar
abstract class BaseStat(private val defaultMappings: Map, DataFrame.Variable>) : Stat {
+ override fun normalize(dataAfterStat: DataFrame): DataFrame {
+ return dataAfterStat
+ }
override fun hasDefaultMapping(aes: Aes<*>): Boolean {
return defaultMappings.containsKey(aes)
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStat.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStat.kt
index 96c40c978a2..2dd7eaf93c7 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStat.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStat.kt
@@ -16,7 +16,7 @@ import jetbrains.datalore.plot.common.data.SeriesUtil
/**
* Computes kernel density estimate for 'n' values evenly distributed throughout the range of the input series.
*
- * If size of the input series exceeds the 'fullScalMax' value, then the less accurate but more efficient computation replaces
+ * If size of the input series exceeds the 'fullScanMax' value, then the less accurate but more efficient computation replaces
* highly inefficient 'full scan' computation.
*/
class DensityStat(
@@ -25,7 +25,7 @@ class DensityStat(
private val adjust: Double,
private val kernel: Kernel,
private val n: Int,
- private val fullScalMax: Int
+ private val fullScanMax: Int
) : BaseStat(DEF_MAPPING) {
init {
@@ -72,30 +72,11 @@ class DensityStat(
val statDensity = ArrayList()
val statCount = ArrayList()
val statScaled = ArrayList()
-
- val bandWidth = bandWidth ?: DensityStatUtil.bandWidth(
- bandWidthMethod,
- xs
+ val densityFunction = DensityStatUtil.densityFunction(
+ xs, weights,
+ bandWidth, bandWidthMethod, adjust, kernel, fullScanMax
)
- val kernelFun: (Double) -> Double = DensityStatUtil.kernel(kernel)
- val densityFunction: (Double) -> Double = when (xs.size <= fullScalMax) {
- true -> DensityStatUtil.densityFunctionFullScan(
- xs,
- weights,
- kernelFun,
- bandWidth,
- adjust
- )
- false -> DensityStatUtil.densityFunctionFast(
- xs,
- weights,
- kernelFun,
- bandWidth,
- adjust
- )
- }
-
val nTotal = weights.sum()
for (x in statX) {
val d = densityFunction(x)
@@ -138,11 +119,11 @@ class DensityStat(
val DEF_BW = NRD0
const val DEF_FULL_SCAN_MAX = 5000
+ const val MAX_N = 1024
+
private val DEF_MAPPING: Map, DataFrame.Variable> = mapOf(
Aes.X to Stats.X,
Aes.Y to Stats.DENSITY
)
-
- private const val MAX_N = 1024
}
}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStatUtil.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStatUtil.kt
index be91d21b33a..0762016d095 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStatUtil.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/DensityStatUtil.kt
@@ -70,6 +70,24 @@ object DensityStatUtil {
}
}
+ internal fun densityFunction(
+ values: List,
+ weights: List,
+ bw: Double?,
+ bwMethod: DensityStat.BandWidthMethod,
+ ad: Double,
+ ker: DensityStat.Kernel,
+ fullScanMax: Int
+ ): (Double) -> Double {
+ val bandWidth = bw ?: bandWidth(bwMethod, values)
+ val kernelFun: (Double) -> Double = kernel(ker)
+
+ return when (values.size <= fullScanMax) {
+ true -> densityFunctionFullScan(values, weights, kernelFun, bandWidth, ad)
+ false -> densityFunctionFast(values, weights, kernelFun, bandWidth, ad)
+ }
+ }
+
internal fun densityFunctionFullScan(
xs: List,
weights: List,
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/Stats.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/Stats.kt
index e3934ddf337..e11fc95470b 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/Stats.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/Stats.kt
@@ -26,6 +26,7 @@ object Stats {
val MIDDLE = DataFrame.Variable("..middle..", STAT, "middle")
val UPPER = DataFrame.Variable("..upper..", STAT, "upper")
val WIDTH = DataFrame.Variable("..width..", STAT, "width")
+ val VIOLIN_WIDTH = DataFrame.Variable("..violinwidth..", STAT, "violinwidth")
val CORR = DataFrame.Variable("..corr..", STAT, "corr")
val CORR_ABS = DataFrame.Variable("..corr_abs..", STAT, "corr_abs")
@@ -50,6 +51,7 @@ object Stats {
MIDDLE,
UPPER,
WIDTH,
+ VIOLIN_WIDTH,
SCALED,
GROUP,
CORR,
@@ -180,7 +182,7 @@ object Stats {
adjust: Double = DensityStat.DEF_ADJUST,
kernel: DensityStat.Kernel = DensityStat.DEF_KERNEL,
n: Int = DensityStat.DEF_N,
- fullScalMax: Int = DensityStat.DEF_FULL_SCAN_MAX
+ fullScanMax: Int = DensityStat.DEF_FULL_SCAN_MAX
): DensityStat {
return DensityStat(
bandWidth = bandWidth,
@@ -188,7 +190,7 @@ object Stats {
adjust = adjust,
kernel = kernel,
n = n,
- fullScalMax = fullScalMax
+ fullScanMax = fullScanMax
)
}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/YDensityStat.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/YDensityStat.kt
new file mode 100644
index 00000000000..c7158795f03
--- /dev/null
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/stat/YDensityStat.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2021. JetBrains s.r.o.
+ * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
+ */
+
+package jetbrains.datalore.plot.base.stat
+
+import jetbrains.datalore.base.gcommon.collect.ClosedRange
+import jetbrains.datalore.plot.base.Aes
+import jetbrains.datalore.plot.base.DataFrame
+import jetbrains.datalore.plot.base.StatContext
+import jetbrains.datalore.plot.base.data.TransformVar
+import jetbrains.datalore.plot.common.data.SeriesUtil
+
+class YDensityStat(
+ private val bandWidth: Double?,
+ private val bandWidthMethod: DensityStat.BandWidthMethod,
+ private val adjust: Double,
+ private val kernel: DensityStat.Kernel,
+ private val n: Int,
+ private val fullScanMax: Int
+) : BaseStat(DEF_MAPPING) {
+
+ init {
+ require(n <= DensityStat.MAX_N) {
+ "The input n = $n > ${DensityStat.MAX_N} is too large!"
+ }
+ }
+
+ override fun consumes(): List> {
+ return listOf(Aes.X, Aes.Y, Aes.WEIGHT)
+ }
+
+ override fun apply(data: DataFrame, statCtx: StatContext, messageConsumer: (s: String) -> Unit): DataFrame {
+ if (!hasRequiredValues(data, Aes.Y)) {
+ return withEmptyStatValues()
+ }
+
+ val ys = data.getNumeric(TransformVar.Y)
+ val xs = if (data.has(TransformVar.X)) {
+ data.getNumeric(TransformVar.X)
+ } else {
+ List(ys.size) { 0.0 }
+ }
+ val ws = if (data.has(TransformVar.WEIGHT)) {
+ data.getNumeric(TransformVar.WEIGHT)
+ } else {
+ List(ys.size) { 1.0 }
+ }
+
+ val statData = buildStat(xs, ys, ws)
+
+ val builder = DataFrame.Builder()
+ for ((variable, series) in statData) {
+ builder.putNumeric(variable, series)
+ }
+ return builder.build()
+ }
+
+ override fun normalize(dataAfterStat: DataFrame): DataFrame {
+ val statViolinWidth = if (dataAfterStat.rowCount() == 0) {
+ emptyList()
+ } else {
+ val statDensity = dataAfterStat.getNumeric(Stats.DENSITY).map { it!! }
+ val densityMax = statDensity.maxOrNull()!!
+ statDensity.map { it / densityMax }
+ }
+ return dataAfterStat.builder()
+ .putNumeric(Stats.VIOLIN_WIDTH, statViolinWidth)
+ .build()
+ }
+
+ private fun buildStat(
+ xs: List,
+ ys: List,
+ ws: List
+ ): MutableMap> {
+ val binnedData = (xs zip (ys zip ws))
+ .filter { it.first?.isFinite() == true }
+ .groupBy({ it.first!! }, { it.second })
+ .mapValues { it.value.unzip() }
+
+ val statX = ArrayList()
+ val statY = ArrayList()
+ val statDensity = ArrayList()
+ val statCount = ArrayList()
+ val statScaled = ArrayList()
+
+ for ((x, bin) in binnedData) {
+ val (filteredY, filteredW) = SeriesUtil.filterFinite(bin.first, bin.second)
+ val (binY, binW) = (filteredY zip filteredW)
+ .sortedBy { it.first }
+ .unzip()
+ if (binY.isEmpty()) continue
+ val ySummary = FiveNumberSummary(binY)
+ val rangeY = ClosedRange(ySummary.min, ySummary.max)
+ val binStatY = DensityStatUtil.createStepValues(rangeY, n)
+ val densityFunction = DensityStatUtil.densityFunction(
+ binY, binW,
+ bandWidth, bandWidthMethod, adjust, kernel, fullScanMax
+ )
+ val binStatCount = binStatY.map { densityFunction(it) }
+ val widthsSum = binW.sum()
+ val maxBinCount = binStatCount.maxOrNull()!!
+
+ statX += MutableList(binStatY.size) { x }
+ statY += binStatY
+ statDensity += binStatCount.map { it / widthsSum }
+ statCount += binStatCount
+ statScaled += binStatCount.map { it / maxBinCount }
+ }
+
+ return mutableMapOf(
+ Stats.X to statX,
+ Stats.Y to statY,
+ Stats.DENSITY to statDensity,
+ Stats.COUNT to statCount,
+ Stats.SCALED to statScaled
+ )
+ }
+
+ companion object {
+ private val DEF_MAPPING: Map, DataFrame.Variable> = mapOf(
+ Aes.X to Stats.X,
+ Aes.Y to Stats.Y,
+ Aes.VIOLINWIDTH to Stats.VIOLIN_WIDTH
+ )
+ }
+}
\ No newline at end of file
diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/geom/GeomProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/geom/GeomProvider.kt
index d16e1799997..78376ee15cd 100644
--- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/geom/GeomProvider.kt
+++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/geom/GeomProvider.kt
@@ -228,6 +228,14 @@ abstract class GeomProvider private constructor(val geomKind: GeomKind) {
).build()
}
+ fun violin(): GeomProvider {
+ return GeomProviderBuilder(
+ GeomKind.VIOLIN,
+ AestheticsDefaults.violin(),
+ ViolinGeom.HANDLES_GROUPS
+ ) { ViolinGeom() }.build()
+ }
+
fun livemap(
options: LiveMapOptions
): GeomProvider {
diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/data/DataProcessing.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/data/DataProcessing.kt
index 658ecdb6cc7..ba855192977 100644
--- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/data/DataProcessing.kt
+++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/data/DataProcessing.kt
@@ -149,14 +149,15 @@ object DataProcessing {
// build DataFrame
build()
}
+ val normalizedData = stat.normalize(dataAfterStat)
val groupingContextAfterStat = GroupingContext.withOrderedGroups(
- dataAfterStat,
+ normalizedData,
groupSizeListAfterStat
)
return DataAndGroupingContext(
- dataAfterStat,
+ normalizedData,
groupingContextAfterStat
)
}
diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultMapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultMapperProvider.kt
index 6fec092f202..ba2f184b7b9 100644
--- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultMapperProvider.kt
+++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultMapperProvider.kt
@@ -30,6 +30,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -97,6 +98,7 @@ object DefaultMapperProvider {
this.put(WIDTH, NUMERIC_IDENTITY)
this.put(HEIGHT, NUMERIC_IDENTITY)
this.put(WEIGHT, NUMERIC_IDENTITY)
+ this.put(VIOLINWIDTH, NUMERIC_IDENTITY)
this.put(INTERCEPT, NUMERIC_IDENTITY)
this.put(SLOPE, NUMERIC_IDENTITY)
this.put(XINTERCEPT, NUMERIC_IDENTITY)
diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultNaValue.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultNaValue.kt
index 53e947771d1..31e993460f4 100644
--- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultNaValue.kt
+++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DefaultNaValue.kt
@@ -32,6 +32,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -67,6 +68,7 @@ object DefaultNaValue {
VALUE_MAP.put(SIZE, AesScaling.sizeFromCircleDiameter(1.0))
VALUE_MAP.put(WIDTH, 1.0)
VALUE_MAP.put(HEIGHT, 1.0)
+ VALUE_MAP.put(VIOLINWIDTH, 0.0)
VALUE_MAP.put(WEIGHT, 1.0)
VALUE_MAP.put(INTERCEPT, 0.0)
VALUE_MAP.put(SLOPE, 1.0)
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomInteractionUtil.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomInteractionUtil.kt
index 5f654a85c54..daf8e7cdf9e 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomInteractionUtil.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomInteractionUtil.kt
@@ -279,7 +279,8 @@ object GeomInteractionUtil {
GeomKind.POINT,
GeomKind.JITTER,
GeomKind.CONTOUR,
- GeomKind.DENSITY2D -> return builder.bivariateFunction(GeomInteractionBuilder.NON_AREA_GEOM)
+ GeomKind.DENSITY2D,
+ GeomKind.VIOLIN -> return builder.bivariateFunction(GeomInteractionBuilder.NON_AREA_GEOM)
GeomKind.PATH -> {
when (statKind) {
StatKind.CONTOUR, StatKind.CONTOURF, StatKind.DENSITY2D -> return builder.bivariateFunction(
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProto.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProto.kt
index 43454091368..634c01f8b4e 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProto.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProto.kt
@@ -9,6 +9,7 @@ import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.GeomKind
import jetbrains.datalore.plot.base.GeomKind.*
import jetbrains.datalore.plot.base.GeomMeta
+import jetbrains.datalore.plot.base.geom.ViolinGeom
import jetbrains.datalore.plot.base.pos.PositionAdjustments
import jetbrains.datalore.plot.builder.assemble.PosProvider
import jetbrains.datalore.plot.builder.assemble.geom.DefaultSampling
@@ -45,6 +46,7 @@ open class GeomProto constructor(val geomKind: GeomKind) {
H_LINE -> DefaultSampling.H_LINE
V_LINE -> DefaultSampling.V_LINE
BOX_PLOT -> Samplings.NONE // DefaultSampling.BOX_PLOT
+ VIOLIN -> Samplings.NONE // DefaultSampling.VIOLIN
RIBBON -> DefaultSampling.RIBBON
AREA -> DefaultSampling.AREA
DENSITY -> DefaultSampling.DENSITY
@@ -103,6 +105,8 @@ open class GeomProto constructor(val geomKind: GeomKind) {
crossBarDefaults()
DEFAULTS[BOX_PLOT] =
boxplotDefaults()
+ DEFAULTS[VIOLIN] =
+ violinDefaults()
DEFAULTS[AREA] =
areaDefaults()
DEFAULTS[DENSITY] =
@@ -170,6 +174,13 @@ open class GeomProto constructor(val geomKind: GeomKind) {
return defaults
}
+ private fun violinDefaults(): Map {
+ val defaults = HashMap()
+ defaults["stat"] = "ydensity"
+ defaults["position"] = mapOf(Meta.NAME to "dodge", "width" to ViolinGeom.WIDTH_SCALE)
+ return defaults
+ }
+
private fun areaDefaults(): Map {
val defaults = HashMap()
defaults["stat"] = "identity"
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
index dfe6a5b8903..0eaae1bd6c2 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
@@ -191,6 +191,7 @@ class GeomProtoClientSide(geomKind: GeomKind) : GeomProto(geomKind) {
PROVIDER[GeomKind.H_LINE] = GeomProvider.hline()
PROVIDER[GeomKind.V_LINE] = GeomProvider.vline()
// boxplot - special case
+ PROVIDER[GeomKind.VIOLIN] = GeomProvider.violin()
PROVIDER[GeomKind.RIBBON] = GeomProvider.ribbon()
PROVIDER[GeomKind.AREA] = GeomProvider.area()
PROVIDER[GeomKind.DENSITY] = GeomProvider.density()
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
index e8f7af96f09..e295387ac00 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
@@ -439,6 +439,7 @@ object Option {
private const val H_LINE = "hline"
private const val V_LINE = "vline"
private const val BOX_PLOT = "boxplot"
+ private const val VIOLIN = "violin"
const val LIVE_MAP = "livemap"
const val POINT = "point"
private const val RIBBON = "ribbon"
@@ -478,6 +479,7 @@ object Option {
map[H_LINE] = GeomKind.H_LINE
map[V_LINE] = GeomKind.V_LINE
map[BOX_PLOT] = GeomKind.BOX_PLOT
+ map[VIOLIN] = GeomKind.VIOLIN
map[LIVE_MAP] = GeomKind.LIVE_MAP
map[POINT] = GeomKind.POINT
map[RIBBON] = GeomKind.RIBBON
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatKind.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatKind.kt
index 7ecdb4c0a05..1597470d0a9 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatKind.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatKind.kt
@@ -16,6 +16,7 @@ enum class StatKind {
CONTOUR,
CONTOURF,
BOXPLOT,
+ YDENSITY,
DENSITY,
DENSITY2D,
DENSITY2DF,
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatProto.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatProto.kt
index c26f8e687c0..b91733b7335 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatProto.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/StatProto.kt
@@ -96,6 +96,8 @@ object StatProto {
)
}
+ StatKind.YDENSITY -> return configureYDensityStat(options)
+
StatKind.DENSITY -> return configureDensityStat(options)
StatKind.DENSITY2D -> return configureDensity2dStat(options, false)
@@ -170,6 +172,31 @@ object StatProto {
)
}
+ private fun configureYDensityStat(options: OptionsAccessor): YDensityStat {
+ var bwValue: Double? = null
+ var bwMethod: DensityStat.BandWidthMethod = DensityStat.DEF_BW
+ options[Density.BAND_WIDTH]?.run {
+ if (this is Number) {
+ bwValue = this.toDouble()
+ } else if (this is String) {
+ bwMethod = DensityStatUtil.toBandWidthMethod(this)
+ }
+ }
+
+ val kernel = options.getString(Density.KERNEL)?.let {
+ DensityStatUtil.toKernel(it)
+ }
+
+ return YDensityStat(
+ bandWidth = bwValue,
+ bandWidthMethod = bwMethod,
+ adjust = options.getDoubleDef(Density.ADJUST, DensityStat.DEF_ADJUST),
+ kernel = kernel ?: DensityStat.DEF_KERNEL,
+ n = options.getIntegerDef(Density.N, DensityStat.DEF_N),
+ fullScanMax = options.getIntegerDef(Density.FULL_SCAN_MAX, DensityStat.DEF_FULL_SCAN_MAX)
+ )
+ }
+
private fun configureDensityStat(options: OptionsAccessor): DensityStat {
var bwValue: Double? = null
var bwMethod: DensityStat.BandWidthMethod = DensityStat.DEF_BW
@@ -191,11 +218,10 @@ object StatProto {
adjust = options.getDoubleDef(Density.ADJUST, DensityStat.DEF_ADJUST),
kernel = kernel ?: DensityStat.DEF_KERNEL,
n = options.getIntegerDef(Density.N, DensityStat.DEF_N),
- fullScalMax = options.getIntegerDef(Density.FULL_SCAN_MAX, DensityStat.DEF_FULL_SCAN_MAX),
+ fullScanMax = options.getIntegerDef(Density.FULL_SCAN_MAX, DensityStat.DEF_FULL_SCAN_MAX),
)
}
-
private fun configureDensity2dStat(options: OptionsAccessor, filled: Boolean): AbstractDensity2dStat {
var bwValueX: Double? = null
var bwValueY: Double? = null
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/aes/TypedOptionConverterMap.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/aes/TypedOptionConverterMap.kt
index a42f4a023e8..d8da3dc1bf9 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/aes/TypedOptionConverterMap.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/aes/TypedOptionConverterMap.kt
@@ -29,6 +29,7 @@ import jetbrains.datalore.plot.base.Aes.Companion.SPEED
import jetbrains.datalore.plot.base.Aes.Companion.SYM_X
import jetbrains.datalore.plot.base.Aes.Companion.SYM_Y
import jetbrains.datalore.plot.base.Aes.Companion.UPPER
+import jetbrains.datalore.plot.base.Aes.Companion.VIOLINWIDTH
import jetbrains.datalore.plot.base.Aes.Companion.VJUST
import jetbrains.datalore.plot.base.Aes.Companion.WEIGHT
import jetbrains.datalore.plot.base.Aes.Companion.WIDTH
@@ -64,6 +65,7 @@ internal class TypedOptionConverterMap {
this.put(SIZE, DOUBLE_CVT)
this.put(WIDTH, DOUBLE_CVT)
this.put(HEIGHT, DOUBLE_CVT)
+ this.put(VIOLINWIDTH, DOUBLE_CVT)
this.put(WEIGHT, DOUBLE_CVT)
this.put(INTERCEPT, DOUBLE_CVT)
this.put(SLOPE, DOUBLE_CVT)
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/bistro/util/LayerOptions.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/bistro/util/LayerOptions.kt
index 06d4bb85df9..6529d4b3f11 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/bistro/util/LayerOptions.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/bistro/util/LayerOptions.kt
@@ -42,6 +42,7 @@ class LayerOptions : Options() {
var size: Double? by map(Aes.SIZE)
var width: Double? by map(Aes.WIDTH)
var height: Double? by map(Aes.HEIGHT)
+ var violinwidth: Double? by map(Aes.VIOLINWIDTH)
var weight: Double? by map(Aes.WEIGHT)
var intercept: Double? by map(Aes.INTERCEPT)
var slope: Double? by map(Aes.SLOPE)
diff --git a/plot-demo-common/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Violin.kt b/plot-demo-common/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Violin.kt
new file mode 100644
index 00000000000..b85661bfd26
--- /dev/null
+++ b/plot-demo-common/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Violin.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021. JetBrains s.r.o.
+ * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
+ */
+
+package jetbrains.datalore.plotDemo.model.plotConfig
+
+import jetbrains.datalore.plot.parsePlotSpec
+import jetbrains.datalore.plotDemo.data.Iris
+
+class Violin {
+ fun plotSpecList(): List> {
+ return listOf(
+ basic(),
+ withNan()
+ )
+ }
+
+ private fun basic(): MutableMap {
+ val spec = "{" +
+ " 'kind': 'plot'," +
+ " 'mapping': {" +
+ " 'x': 'target'," +
+ " 'y': 'sepal length (cm)'," +
+ " 'fill': 'target'" +
+ " }," +
+ " 'layers': [" +
+ " {" +
+ " 'geom': 'violin'," +
+ " 'alpha': 0.7" +
+ " }" +
+ " ]" +
+ "}"
+
+ val plotSpec = HashMap(parsePlotSpec(spec))
+ plotSpec["data"] = Iris.df
+ return plotSpec
+
+ }
+
+ private fun withNan(): MutableMap {
+ val spec = "{" +
+ " 'kind': 'plot'," +
+ " 'data' : {'class': ['A', 'A', 'A', null, 'B', 'B', 'B', 'B']," +
+ " 'value': [0, 0, 2, 2, 1, 1, 3, null]" +
+ " }," +
+ " 'mapping': {" +
+ " 'x': 'class'," +
+ " 'y': 'value'" +
+ " }," +
+ " 'layers': [" +
+ " {" +
+ " 'geom': 'violin'" +
+ " }" +
+ " ]" +
+ "}"
+
+ return HashMap(parsePlotSpec(spec))
+
+ }
+}
\ No newline at end of file
diff --git a/plot-demo/src/jvmBatikMain/kotlin/plotDemo/plotConfig/ViolinBatik.kt b/plot-demo/src/jvmBatikMain/kotlin/plotDemo/plotConfig/ViolinBatik.kt
new file mode 100644
index 00000000000..5d9ae14dd8e
--- /dev/null
+++ b/plot-demo/src/jvmBatikMain/kotlin/plotDemo/plotConfig/ViolinBatik.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2021. JetBrains s.r.o.
+ * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
+ */
+
+package jetbrains.datalore.plotDemo.plotConfig
+
+import jetbrains.datalore.plotDemo.model.plotConfig.Violin
+import jetbrains.datalore.vis.demoUtils.PlotSpecsDemoWindowBatik
+
+fun main() {
+ with(Violin()) {
+ PlotSpecsDemoWindowBatik(
+ "Violin plot",
+ plotSpecList()
+ ).open()
+ }
+}
\ No newline at end of file
diff --git a/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/ViolinJfx.kt b/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/ViolinJfx.kt
new file mode 100644
index 00000000000..0d5b66465ca
--- /dev/null
+++ b/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/ViolinJfx.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2021. JetBrains s.r.o.
+ * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
+ */
+
+package jetbrains.datalore.plotDemo.plotConfig
+
+import jetbrains.datalore.plotDemo.model.plotConfig.Violin
+import jetbrains.datalore.vis.demoUtils.PlotSpecsDemoWindowJfx
+
+fun main() {
+ with(Violin()) {
+ PlotSpecsDemoWindowJfx(
+ "Violin",
+ plotSpecList()
+ ).open()
+ }
+}
\ No newline at end of file
diff --git a/python-package/lets_plot/plot/geom.py b/python-package/lets_plot/plot/geom.py
index 1c256337324..da1783192a1 100644
--- a/python-package/lets_plot/plot/geom.py
+++ b/python-package/lets_plot/plot/geom.py
@@ -17,7 +17,7 @@
'geom_contour',
'geom_contourf', 'geom_polygon', 'geom_map',
'geom_abline', 'geom_hline', 'geom_vline',
- 'geom_boxplot',
+ 'geom_boxplot', 'geom_violin',
'geom_ribbon', 'geom_area', 'geom_density',
'geom_density2d', 'geom_density2df', 'geom_jitter',
'geom_freqpoly', 'geom_step', 'geom_rect', 'geom_segment',
@@ -2645,6 +2645,19 @@ def geom_boxplot(mapping=None, *, data=None, stat=None, position=None, show_lege
**other_args)
+def geom_violin(mapping=None, *, data=None, stat=None, position=None, show_legend=None, sampling=None, tooltips=None,
+ **other_args):
+ return _geom('violin',
+ mapping=mapping,
+ data=data,
+ stat=stat,
+ position=position,
+ show_legend=show_legend,
+ sampling=sampling,
+ tooltips=tooltips,
+ **other_args)
+
+
def geom_ribbon(mapping=None, *, data=None, stat=None, position=None, show_legend=None, sampling=None, tooltips=None,
**other_args):
"""