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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
sepal_lengthsepal_widthpetal_lengthpetal_widthspecies
05.13.51.40.2setosa
14.93.01.40.2setosa
24.73.21.30.2setosa
34.63.11.50.2setosa
45.03.61.40.2setosa
\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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
speciessepal_lengthweight
0setosa4.3000000.222676
1setosa4.3029350.228662
2setosa4.3058710.234639
3setosa4.3088060.240684
4setosa4.3117420.246886
\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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
vc1c2
00.496714Ab
1-0.138264Bb
20.647689Aa
31.523030Aa
4-0.234153Ca
\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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
vc1c2
00.496714Ab
1-0.138264NaNb
2NaNAa
31.523030ANaN
4-0.234153Ca
\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): """