diff --git a/docs/examples/jupyter-notebooks-dev/as_discrete.ipynb b/docs/examples/jupyter-notebooks-dev/as_discrete.ipynb new file mode 100644 index 00000000000..2cbeaa12857 --- /dev/null +++ b/docs/examples/jupyter-notebooks-dev/as_discrete.ipynb @@ -0,0 +1,886 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from lets_plot import *\n", + "import lets_plot.mapping as pm" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + "
Lets-Plot JS is embedded.
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "LetsPlot.setup_html()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "df = {\n", + " 'x': [0, 5, 10, 15],\n", + " 'y': [0, 5, 10, 15],\n", + " 'a': [1, 2, 3, 4],\n", + " 'b': [4, 5, 6, 7]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# factor, no scale\n", + "p = ggplot(df, aes(x='x', y='y')) + geom_point(aes(color=pm.as_discrete('a')), size=9)\n", + "p" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# factor, scale\n", + "p = ggplot(df, aes(x='x', y='y')) \\\n", + " + geom_point(aes(color='a', fill=pm.as_discrete('b')), shape=21, size=9, stroke=5) \\\n", + " + scale_color_discrete()\n", + "p" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# data in mappings, scale_color_discrete\n", + "p = ggplot() + geom_point(aes(x=[0, 5, 10], y=[0, 5, 10], color=[1, 2, 4]), size=9) + scale_color_discrete()\n", + "p" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Issue 1\n", + "\n", + "`factor` is not working when used in \"ggplot()\" mapping." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p3 = ggplot(df, aes(x='x', y='y', color=pm.as_discrete('a'))) + geom_point(size=9)\n", + "p3" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Issue 2\n", + "`factor` doesn't create groups the way discrete variable does." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "df = {\n", + " 'x': [0, 5, 10, 15],\n", + " 'y': [0, 5, 10, 15],\n", + " 'a': [0, 0, 1, 1],\n", + " 'c': ['a', 'a', 'b', 'b']\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p4 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color=pm.as_discrete('a')), size=3)\n", + "p4" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# expected: 2 lines ('c' is a discrete variable)\n", + "p5 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color='c'), size=3)\n", + "p5" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# expected: 2 lines (`group` is defined manually)\n", + "p6 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color='a', group='a'), size=3)\n", + "p6" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Issue 2a\n", + "Also about groups but with `stat` this time" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "mpg_df = pd.read_csv('https://jetbrains.bintray.com/lets-plot/mpg.csv')\n", + "mpg_plot = ggplot(mpg_df, aes(x='displ', y='hwy'))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cyl_factor = pm.as_discrete('cyl')\n", + "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", + "+ geom_smooth(aes(color=cyl_factor), method='lm', size=1, se=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# expected: seperate regression line for each group\n", + "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", + "+ geom_smooth(aes(color='cyl', group='cyl'), method='lm', size=1, se=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Issue 3\n", + "\"Not an aesthetic 'group'\" error when used with `factor`" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", + "+ geom_smooth(aes(color=cyl_factor, group='cyl'), method='lm', size=1, se=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Issue 4\n", + "Nice to have parameter `ordered`. Owerwise have to use `scale_discrete(breaks=[...])` to order groups by number of cylinders:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", + "+ scale_color_discrete(breaks=[4, 5, 6, 8])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/examples/jupyter-notebooks-dev/factor.ipynb b/docs/examples/jupyter-notebooks-dev/factor.ipynb deleted file mode 100644 index 04308a4fc77..00000000000 --- a/docs/examples/jupyter-notebooks-dev/factor.ipynb +++ /dev/null @@ -1,860 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from lets_plot import *\n", - "import lets_plot.mapping as pm" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - "
Lets-Plot JS is embedded.
\n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "load_lets_plot_js()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "df = {\n", - " 'x': [0, 5, 10, 15],\n", - " 'y': [0, 5, 10, 15],\n", - " 'a': [1, 2, 3, 4],\n", - " 'b': [4, 5, 6, 7]\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# factor, no scale\n", - "p = ggplot(df, aes(x='x', y='y')) + geom_point(aes(color=pm.factor('a')), size=9)\n", - "p" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# factor, scale\n", - "p = ggplot(df, aes(x='x', y='y')) \\\n", - " + geom_point(aes(color='a', fill=pm.factor('b')), shape=21, size=9, stroke=5) \\\n", - " + scale_color_discrete()\n", - "p" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# data in mappings, scale_color_discrete\n", - "p = ggplot() + geom_point(aes(x=[0, 5, 10], y=[0, 5, 10], color=[1, 2, 4]), size=9) + scale_color_discrete()\n", - "p" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Issue 1\n", - "\n", - "`factor` is not working when used in \"ggplot()\" mapping." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "p3 = ggplot(df, aes(x='x', y='y', color=pm.factor('a'))) + geom_point(size=9)\n", - "p3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Issue 2\n", - "`factor` doesn't create groups the way discrete variable does." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "df = {\n", - " 'x': [0, 5, 10, 15],\n", - " 'y': [0, 5, 10, 15],\n", - " 'a': [0, 0, 1, 1],\n", - " 'c': ['a', 'a', 'b', 'b']\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "p4 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color=pm.factor('a')), size=3)\n", - "p4" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# expected: 2 lines ('c' is a discrete variable)\n", - "p5 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color='c'), size=3)\n", - "p5" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# expected: 2 lines (`group` is defined manually)\n", - "p6 = ggplot(df, aes(x='x', y='y')) + geom_line(aes(color='a', group='a'), size=3)\n", - "p6" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Issue 2a\n", - "Also about groups but with `stat` this time" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "mpg_df = pd.read_csv('https://jetbrains.bintray.com/lets-plot/mpg.csv')\n", - "mpg_plot = ggplot(mpg_df, aes(x='displ', y='hwy'))" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cyl_factor = pm.factor('cyl')\n", - "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", - "+ geom_smooth(aes(color=cyl_factor), method='lm', size=1, se=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# expected: seperate regression line for each group\n", - "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", - "+ geom_smooth(aes(color='cyl', group='cyl'), method='lm', size=1, se=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Issue 3\n", - "\"Not an aesthetic 'group'\" error when used with `factor`" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", - "+ geom_smooth(aes(color=cyl_factor, group='cyl'), method='lm', size=1, se=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Issue 4\n", - "Nice to have parameter `ordered`. Owerwise have to use `scale_discrete(breaks=[...])` to order groups by number of cylinders:" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 40, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "mpg_plot + geom_point(aes(color=cyl_factor)) \\\n", - "+ scale_color_discrete(breaks=[4, 5, 6, 8])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataFrame.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataFrame.kt index 41a745e517f..4b1faaa1657 100644 --- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataFrame.kt +++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/DataFrame.kt @@ -146,7 +146,8 @@ class DataFrame private constructor(builder: Builder) { class Variable @JvmOverloads constructor( val name: String, val source: Source = Source.ORIGIN, - val label: String = name) { + val label: String = name + ) { val isOrigin: Boolean get() = source == Source.ORIGIN @@ -205,6 +206,12 @@ class DataFrame private constructor(builder: Builder) { return this } + fun putDiscrete(variable: Variable, v: List<*>): Builder { + putIntern(variable, v) + myIsNumeric[variable] = false + return this + } + fun putIntern(variable: Variable, v: List<*>) { myVectorByVar[variable] = ArrayList(v) } diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/DataFrameUtil.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/DataFrameUtil.kt index 4edc12da8f0..ef237e54528 100644 --- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/DataFrameUtil.kt +++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/data/DataFrameUtil.kt @@ -95,43 +95,31 @@ object DataFrameUtil { return LinkedHashSet(data[variable]) } - fun hasValues(data: DataFrame, `var`: DataFrame.Variable): Boolean { - return data.has(`var`) && !data[`var`].isEmpty() - } - - fun valuesOrNull(data: DataFrame, `var`: DataFrame.Variable): List<*>? { - return if (data.has(`var`)) { - data[`var`] - } else null - } - fun sortedCopy(variables: Iterable): List { val ordering = Ordering.from(Comparator { o1, o2 -> o1.name.compareTo(o2.name) }) return ordering.sortedCopy(variables) } fun variables(df: DataFrame): Map { - val vars = HashMap() - for (`var` in df.variables()) { - vars[`var`.name] = `var` - } - return vars + return df.variables().associateBy { it.name } } fun appendReplace(df0: DataFrame, df1: DataFrame): DataFrame { - val df0Vars = variables(df0) - - val builder = df0.builder() - for (df1Var in df1.variables()) { - var resultVar = df1Var - if (df0Vars.containsKey(df1Var.name)) { - val df0Var = df0Vars[df1Var.name]!! - builder.remove(df0Var) - resultVar = df0Var + fun DataFrame.Builder.put(destVars: Collection, df: DataFrame) = apply { + destVars.forEach { destVar -> + val srcVar = findVariableOrFail(df, destVar.name) + when (df.isNumeric(srcVar)) { + true -> putNumeric(destVar, df.getNumeric(srcVar)) + false -> putDiscrete(destVar, df[srcVar]) + } } - builder.put(resultVar, df1[df1Var]) } - return builder.build() + + return DataFrame.Builder() + .put(df0.variables().filter { it.name !in variables(df1) }, df0) // df0 - df1, keep vars from df0 + .put(df0.variables().filter { it.name in variables(df1) }, df1) // df0 & df1, keep vars from df0 + .put(df1.variables().filter { it.name !in variables(df0) }, df1) // df1 - df0, new vars from df1 + .build() } fun toMap(df: DataFrame): Map> { @@ -155,16 +143,12 @@ object DataFrameUtil { @JvmOverloads fun createVariable(name: String, label: String = name): DataFrame.Variable { - val variable: DataFrame.Variable - when { - TransformVar.isTransformVar(name) -> variable = TransformVar[name] - Stats.isStatVar(name) -> variable = Stats.statVar(name) - Dummies.isDummyVar(name) -> return Dummies.newDummy( - name - ) - else -> variable = DataFrame.Variable(name, DataFrame.Variable.Source.ORIGIN, label) + return when { + TransformVar.isTransformVar(name) -> TransformVar[name] + Stats.isStatVar(name) -> Stats.statVar(name) + Dummies.isDummyVar(name) -> Dummies.newDummy(name) + else -> DataFrame.Variable(name, DataFrame.Variable.Source.ORIGIN, label) } - return variable } fun getSummaryText(df: DataFrame): String { diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ConfigUtil.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ConfigUtil.kt index 446b1a22410..fac9f8fcd87 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ConfigUtil.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ConfigUtil.kt @@ -30,6 +30,7 @@ object ConfigUtil { } } + internal fun createDataFrame(rawData: Any?): DataFrame { val varNameMap = asVarNameMap(rawData) return updateDataFrame(DataFrame.Builder.emptyFrame(), varNameMap) @@ -96,20 +97,18 @@ object ConfigUtil { val varNameMap = HashMap>() if (data is Map<*, *>) { - val mapData = data as Map<*, *>? - for (k in mapData!!.keys) { - val v = mapData[k] + for (k in data.keys) { + val v = data[k] if (v is List<*>) { varNameMap[k.toString()] = v } } } else if (data is List<*>) { - val list = data as List<*>? // check if this is a matrix - all elements are lists of the same size var matrix = true var rowSize = -1 - for (row in list!!) { + for (row in data) { if (row is List<*>) { if (rowSize < 0 || row.size == rowSize) { rowSize = row.size @@ -121,9 +120,9 @@ object ConfigUtil { } if (matrix) { - val dummyNames = Dummies.dummyNames(list.size) - for (i in list.indices) { - varNameMap[dummyNames[i]] = list[i] as List<*> + val dummyNames = Dummies.dummyNames(data.size) + for (i in data.indices) { + varNameMap[dummyNames[i]] = data[i] as List<*> } } else { // simple data vector @@ -140,30 +139,20 @@ object ConfigUtil { private fun updateDataFrame(df: DataFrame, data: Map>): DataFrame { val dfVars = DataFrameUtil.variables(df) val b = df.builder() - for (varName in data.keys) { - val variable: DataFrame.Variable - if (dfVars.containsKey(varName)) { - variable = dfVars[varName]!! - } else { - variable = DataFrameUtil.createVariable(varName) - } - - b.put(variable, toList(data[varName]!!)) + for ((varName, values) in data) { + val variable = dfVars[varName] ?: DataFrameUtil.createVariable(varName) + b.put(variable, values) } return b.build() } private fun toList(o: Any): List<*> { - if (o is List<*>) { - return o - } - if (o is Number) { - return listOf(o.toDouble()) - } - if (o is Iterable<*>) { - throw IllegalArgumentException("Can't cast/transform to list: " + o::class.simpleName) + return when (o) { + is List<*> -> o + is Number -> listOf(o.toDouble()) + is Iterable<*> -> throw IllegalArgumentException("Can't cast/transform to list: " + o::class.simpleName) + else -> listOf(o.toString()) } - return listOf(o.toString()) } internal fun createAesMapping(data: DataFrame, mapping: Map<*, *>?): Map, DataFrame.Variable> { diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/DataMetaUtil.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/DataMetaUtil.kt new file mode 100644 index 00000000000..737fda14983 --- /dev/null +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/DataMetaUtil.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2020. 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.config + +import jetbrains.datalore.plot.base.DataFrame +import jetbrains.datalore.plot.base.data.DataFrameUtil +import jetbrains.datalore.plot.base.data.DataFrameUtil.createVariable +import jetbrains.datalore.plot.base.data.DataFrameUtil.findVariableOrFail +import jetbrains.datalore.plot.config.Option.Meta.MappingAnnotation +import jetbrains.datalore.plot.config.Option.Meta.MappingAnnotation.DISCRETE + +object DataMetaUtil { + private const val prefix = "@as_discrete@" + + private fun isDiscrete(variable: String) = variable.startsWith(prefix) + + public fun toDiscrete(variable: String): String { + require(!isDiscrete(variable)) { "toDiscrete() - variable already encoded: $variable" } + return "$prefix$variable" + } + + private fun fromDiscrete(variable: String): String { + require(isDiscrete(variable)) { "fromDiscrete() - variable is not encoded: $variable" } + return variable.removePrefix(prefix) + } + + /** + @returns Map + */ + private fun getAesMappingAnnotations(options: Map<*, *>): Map { + return options + .getMaps(MappingAnnotation.TAG) + ?.associate { it.read(MappingAnnotation.AES) as String to it.read(MappingAnnotation.ANNOTATION) as String } + ?: emptyMap() + } + + /** + @returns Set of discrete aes + */ + fun getAsDiscreteAesSet(options: Map<*, *>): Set { + return getAesMappingAnnotations(options).filterValues(DISCRETE::equals).keys + } + + fun createScaleSpecs(plotOptions: Map): List> { + val plotDiscreteAes = plotOptions + .getMap(Option.Meta.DATA_META) + ?.let(DataMetaUtil::getAsDiscreteAesSet) + ?: emptySet() + + val layersDiscreteAes = plotOptions + .getMaps(Option.Plot.LAYERS)?.asSequence() + ?.mapNotNull { it.getMap(Option.Meta.DATA_META) } + ?.map(DataMetaUtil::getAsDiscreteAesSet) // diascrete aes from all layers + ?.flatten()?.toSet() // List> -> Set + ?: emptySet() + + + return (plotDiscreteAes + layersDiscreteAes).map { aes -> + mutableMapOf( + Option.Scale.AES to aes, + Option.Scale.DISCRETE_DOMAIN to true + ) + } + } + + /** + * returns mappings and DataFrame extended with auto-generated discrete mappings and variables + */ + fun createDataFrame( + options: OptionsAccessor, + commonData: DataFrame, + commonDiscreteAes: Set, + commonMappings: Map<*, *>, + isClientSide: Boolean + ): Pair, DataFrame> { + val ownData = ConfigUtil.createDataFrame(options.get(Option.PlotBase.DATA)) + val ownMappings = options.getMap(Option.PlotBase.MAPPING) + + if (isClientSide) { + return Pair( + // no new discrete mappings, all job was done on server side + ownMappings, + // re-insert existing variables as discrete + DataFrameUtil.toMap(ownData) + .filter { (varName, _) -> isDiscrete(varName) } + .entries + .fold(DataFrame.Builder(ownData)) { acc, (varName, values) -> + val variable = findVariableOrFail(ownData, varName) + // re-insert as discrete + acc.remove(variable) + acc.putDiscrete(variable, values) + } + .build() + ) + + } + + // server side + + // own names not yet encoded, i.e. 'cyl' + val ownDiscreteMappings = run { + val ownDiscreteAes = getAsDiscreteAesSet(options.getMap(Option.Meta.DATA_META)) + return@run ownMappings.filter { (aes, _) -> aes in ownDiscreteAes } + } + + // Original (not encoded) discrete var names from both common and own mappings. + val combinedDiscreteVars = run { + // common names already encoded by PlotConfig, i.e. '@as_discrete@cyl'. Restore original name. + val commonDiscreteVars = commonMappings.filterKeys { it in commonDiscreteAes }.variables().map(::fromDiscrete) + + val ownSimpleVars = ownMappings.variables() - ownDiscreteMappings.variables() + + // minus own non-discrete mappings (simple layer var overrides discrete plot var) + return@run ownDiscreteMappings.variables() + commonDiscreteVars - ownSimpleVars + } + + val combinedDfVars = DataFrameUtil.toMap(commonData) + DataFrameUtil.toMap(ownData) + + return Pair( + ownMappings + ownDiscreteMappings.mapValues { (_, varName) -> + require(varName is String) + toDiscrete(varName) + }, + combinedDfVars + .filter { (dfVarName, _) -> dfVarName in combinedDiscreteVars } + .mapKeys { (dfVarName, _) -> createVariable(toDiscrete(dfVarName)) } + .entries + .fold(DataFrame.Builder(ownData)) { acc, (dfVar, values) -> acc.putDiscrete(dfVar, values) } + .build() + ) + } + +} + + +private fun Map<*, *>.variables(): Set { + return values.map { it as String }.toSet() +} diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/LayerConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/LayerConfig.kt index 41d9b431e2b..0e4b51a82b3 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/LayerConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/LayerConfig.kt @@ -12,11 +12,13 @@ import jetbrains.datalore.plot.base.DataFrame import jetbrains.datalore.plot.base.Scale import jetbrains.datalore.plot.base.Stat import jetbrains.datalore.plot.base.data.DataFrameUtil +import jetbrains.datalore.plot.base.data.DataFrameUtil.variables import jetbrains.datalore.plot.builder.VarBinding import jetbrains.datalore.plot.builder.assemble.PosProvider import jetbrains.datalore.plot.builder.assemble.TypedScaleProviderMap import jetbrains.datalore.plot.builder.assemble.geom.DefaultAesAutoMapper import jetbrains.datalore.plot.builder.sampling.Sampling +import jetbrains.datalore.plot.config.DataMetaUtil.createDataFrame import jetbrains.datalore.plot.config.Option.Layer.GEOM import jetbrains.datalore.plot.config.Option.Layer.SHOW_LEGEND import jetbrains.datalore.plot.config.Option.Layer.STAT @@ -24,18 +26,16 @@ import jetbrains.datalore.plot.config.Option.Layer.TOOLTIP import jetbrains.datalore.plot.config.Option.PlotBase.DATA import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING -class LayerConfig constructor( +class LayerConfig( layerOptions: Map<*, *>, sharedData: DataFrame, - plotMapping: Map<*, *>, + plotMappings: Map<*, *>, + plotDiscreteAes: Set, val geomProto: GeomProto, statProto: StatProto, scaleProviderByAes: TypedScaleProviderMap, private val myClientSide: Boolean -) : OptionsAccessor( - layerOptions, - initDefaultOptions(layerOptions, geomProto, statProto) -) { +) : OptionsAccessor(layerOptions, initDefaultOptions(layerOptions, geomProto, statProto)) { // val geomProvider: GeomProvider val stat: Stat @@ -70,13 +70,21 @@ class LayerConfig constructor( } init { + val (layerMappings, layerData) = createDataFrame( + options = this, + commonData = sharedData, + commonDiscreteAes = plotDiscreteAes, + commonMappings = plotMappings, + isClientSide = myClientSide + ) + + if (!myClientSide) { + update(MAPPING, layerMappings) + } - // mapping (inherit from plot) - val mappingOptions = HashMap(plotMapping) - // update with 'layer' mapping - mappingOptions.putAll(getMap(MAPPING)) + // mapping (inherit from plot) + 'layer' mapping + val combinedMappings = plotMappings + layerMappings - val layerData = ConfigUtil.createDataFrame(get(DATA)) var combinedData: DataFrame if (!(sharedData.isEmpty || layerData.isEmpty) && sharedData.rowCount() == layerData.rowCount()) { combinedData = DataFrameUtil.appendReplace(sharedData, layerData) @@ -86,30 +94,31 @@ class LayerConfig constructor( combinedData = sharedData } - var aesMapping: Map, DataFrame.Variable>? + + var aesMappings: Map, DataFrame.Variable>? if (GeoPositionsDataUtil.hasGeoPositionsData(this) && myClientSide) { // join dataset and geo-positions data val dataAndMapping = GeoPositionsDataUtil.initDataAndMappingForGeoPositions( geomProto.geomKind, combinedData, GeoPositionsDataUtil.getGeoPositionsData(this), - mappingOptions + combinedMappings ) combinedData = dataAndMapping.first - aesMapping = dataAndMapping.second + aesMappings = dataAndMapping.second } else { - aesMapping = ConfigUtil.createAesMapping(combinedData, mappingOptions) + aesMappings = ConfigUtil.createAesMapping(combinedData, combinedMappings) } // auto-map variables if necessary - if (aesMapping.isEmpty()) { - aesMapping = DefaultAesAutoMapper.forGeom(geomProto.geomKind).createMapping(combinedData) + if (aesMappings.isEmpty()) { + aesMappings = DefaultAesAutoMapper.forGeom(geomProto.geomKind).createMapping(combinedData) if (!myClientSide) { // store used mapping options to pass to client. val autoMappingOptions = HashMap() - for (aes in aesMapping.keys) { + for (aes in aesMappings.keys) { val option = Option.Mapping.toOption(aes) - val variable = aesMapping[aes]!!.name + val variable = aesMappings[aes]!!.name autoMappingOptions[option] = variable } update(MAPPING, autoMappingOptions) @@ -119,14 +128,14 @@ class LayerConfig constructor( // exclude constant aes from mapping val constants = LayerConfigUtil.initConstants(this) if (constants.isNotEmpty()) { - aesMapping = HashMap(aesMapping) + aesMappings = HashMap(aesMappings) for (aes in constants.keys) { - aesMapping.remove(aes) + aesMappings.remove(aes) } } // grouping - explicitGroupingVarName = initGroupingVarName(combinedData, mappingOptions) + explicitGroupingVarName = initGroupingVarName(combinedData, combinedMappings) statKind = StatKind.safeValueOf(getString(STAT)!!) stat = statProto.createStat(statKind, mergedOptions) @@ -142,11 +151,11 @@ class LayerConfig constructor( } // tooltip aes list - this.tooltipAes = getTooltipAesList(aesMapping) + this.tooltipAes = getTooltipAesList(aesMappings) val varBindings = LayerConfigUtil.createBindings( combinedData, - aesMapping, + aesMappings, scaleProviderByAes, consumedAesSet ) @@ -170,7 +179,7 @@ class LayerConfig constructor( if (fieldName == null && GeoPositionsDataUtil.hasGeoPositionsData(this)) { // 'default' group is important for 'geom_map' - val groupVar = DataFrameUtil.variables(data)["group"] + val groupVar = variables(data)["group"] if (groupVar != null) { fieldName = groupVar.name } @@ -211,26 +220,26 @@ class LayerConfig constructor( return varBindings.find { it.aes == aes }?.scale } - private fun getTooltipAesList(aesMapping: Map, DataFrame.Variable>): List>? { + private fun getTooltipAesList(mappings: Map, DataFrame.Variable>): List>? { // tooltip list is not defined - will be used default tooltips - if (!has(TOOLTIP)) + if (!has(TOOLTIP)) { return null + } - val aesStringList = getStringList(TOOLTIP) + val tooltipAesSpec = getStringList(TOOLTIP) // check if all elements of list are aes - (aesStringList - Aes.values().map { it.name }).firstOrNull { - error("${it} is not aes name ") - } + (tooltipAesSpec - Aes.values().map(Aes<*>::name)).firstOrNull { error("${it} is not aes name ") } // detach aes - val aesList = Aes.values().filter { aesStringList.contains(it.name) } + val tooltipAesList = Aes.values().filter { it.name in tooltipAesSpec } // check if aes list matches to mapping - if (!aesMapping.keys.containsAll(aesList)) - error("Aes list does not match to mapping") + if (!mappings.keys.containsAll(tooltipAesList)) { + error("Tooltip aes list does not match mappings: [${(tooltipAesList - mappings.keys).joinToString()}]") + } - return aesList + return tooltipAesList } private companion object { 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 976bfe11e82..4a1635c0547 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 @@ -39,11 +39,11 @@ object Option { const val OSM_ID = "id" } - object SeriesAnnotation { - const val TAG = "series_annotation" - const val VARIABLE = "variable" + object MappingAnnotation { + const val TAG = "mapping_annotation" + const val AES = "aes" const val ANNOTATION = "annotation" - const val DISCRETE = "discrete" + const val DISCRETE = "as_discrete" } } @@ -211,16 +211,13 @@ object Option { object Mapping { const val GROUP = "group" - val MAP_ID = - toOption(Aes.MAP_ID) // map_id is 'aes' but also used as option in geom_map() + val MAP_ID = toOption(Aes.MAP_ID) // map_id is 'aes' but also used as option in geom_map() private val AES_BY_OPTION = HashMap>() val REAL_AES_OPTION_NAMES: Iterable = AES_BY_OPTION.keys init { - for (aes in Aes.values()) { - AES_BY_OPTION[toOption( - aes - )] = aes + Aes.values().forEach { aes -> + AES_BY_OPTION[toOption(aes)] = aes } // aliases AES_BY_OPTION["colour"] = Aes.COLOR diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/OptionsSelector.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/OptionsSelector.kt index 1154eb89ffe..b3e6c82afbb 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/OptionsSelector.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/OptionsSelector.kt @@ -10,7 +10,7 @@ fun Map<*, *>.read(vararg query: String): Any? { } fun Map<*, *>.read(path: List, item: String): Any? { - return section(path)?.get(item) + return getMap(path)?.get(item) } fun Map<*, *>.write(vararg query: String, value: () -> Any) { @@ -18,7 +18,7 @@ fun Map<*, *>.write(vararg query: String, value: () -> Any) { } fun Map<*, *>.write(path: List, item: String, value: Any) { - provideSection(path).asMutable()[item] = value + provideMap(path)[item] = value } fun Map<*, *>.remove(vararg query: String) { @@ -26,7 +26,7 @@ fun Map<*, *>.remove(vararg query: String) { } fun Map<*, *>.remove(path: List, item: String) { - section(path)?.asMutable()?.remove(item) + getMap(path)?.asMutable()?.remove(item) } fun Map<*, *>.has(vararg query: String): Boolean { @@ -34,40 +34,52 @@ fun Map<*, *>.has(vararg query: String): Boolean { } fun Map<*, *>.has(path: List, item: String): Boolean { - return section(path)?.containsKey(item) ?: false + return getMap(path)?.containsKey(item) ?: false } -fun Map<*, *>.section(vararg query: String): Map<*, *>? { - return section(query.toList()) +fun Map<*, *>.getMap(vararg query: String): Map? { + return getMap(query.toList())?.typed() } -fun Map<*, *>.section(path: List): Map<*, *>? { - return path.fold?>(this, { section, next -> section?.read(next)?.let { it as Map<*, *> } ?: return@fold null } ) +fun Map<*, *>.getMap(path: List): Map? { + return path.fold?>(this, { section, next -> section?.read(next)?.let { it as? Map<*, *> } ?: return@fold null } )?.typed() } -fun Map<*, *>.list(vararg query: String): List<*>? { - return list(query.dropLast(1), query.last()) +fun Map<*, *>.getList(vararg query: String): List<*>? { + return getList(query.dropLast(1), query.last()) } -fun Map<*, *>.list(path: List, item: String): List<*>? { - return section(path)?.get(item) as? List<*> +fun Map<*, *>.getList(path: List, item: String): List<*>? { + return getMap(path)?.get(item) as? List<*> } -fun Map<*, *>.sections(vararg query: String): List>? { - return list(*query)?.map { it as Map<*, *> }?.toList() +fun Map<*, *>.getMaps(vararg query: String): List>? { + return getList(*query)?.mapNotNull { it as? Map<*, *> }?.toList() } -fun Map<*, *>.provideSection(path: List): Map<*, *> { - return path.fold(this, { section, next -> section.asMutable().getOrPut(next, { HashMap() }) as Map<*, *> }) +fun Map<*, *>.provideMap(vararg query: String): MutableMap { + return provideMap(query.toList()) } -fun Map<*, *>.provideSections(vararg query: String): List> { - return provideSections(query.dropLast(1), query.last()) +fun Map<*, *>.provideMap(path: List): MutableMap { + return path.fold( + this, + { acc, next -> + acc.asMutable().getOrPut( + key = next, + defaultValue = { HashMap() } + ) as Map<*, *> + } + ).asMutable() } -fun Map<*, *>.provideSections(path: List, item: String): List> { +fun Map<*, *>.provideMaps(vararg query: String): MutableList> { + return provideMaps(query.dropLast(1), query.last()).asMutable() +} + +fun Map<*, *>.provideMaps(path: List, item: String): MutableList> { @Suppress("UNCHECKED_CAST") - return provideSection(path).asMutable().getOrPut(item, { mutableListOf>() }) as List> + return provideMap(path).getOrPut(item, { mutableListOf>() }) as MutableList> } @Suppress("UNCHECKED_CAST") @@ -75,13 +87,18 @@ fun Map<*, *>.asMutable(): MutableMap { return this as MutableMap } +@Suppress("UNCHECKED_CAST") +fun Map<*, *>.typed(): Map { + return this as Map +} + @Suppress("UNCHECKED_CAST") fun List.asMutable(): MutableList { return this as MutableList } @Suppress("UNCHECKED_CAST") -fun List<*>.asSections(): List> { +fun List<*>.asMaps(): List> { return this as List> } diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfig.kt index a352f2697ad..bb2f0068813 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfig.kt @@ -13,6 +13,7 @@ import jetbrains.datalore.plot.base.data.DataFrameUtil import jetbrains.datalore.plot.builder.assemble.PlotFacets import jetbrains.datalore.plot.builder.assemble.TypedScaleProviderMap import jetbrains.datalore.plot.config.Option.Meta +import jetbrains.datalore.plot.config.Option.Meta.DATA_META import jetbrains.datalore.plot.config.Option.Meta.Kind import jetbrains.datalore.plot.config.Option.Plot.COORD import jetbrains.datalore.plot.config.Option.Plot.FACET @@ -23,11 +24,9 @@ import jetbrains.datalore.plot.config.Option.Plot.TITLE_TEXT import jetbrains.datalore.plot.config.Option.PlotBase.DATA import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING -abstract class PlotConfig(opts: Map) : OptionsAccessor( - opts, - DEF_OPTIONS -) { - +abstract class PlotConfig( + opts: Map +) : OptionsAccessor(opts, DEF_OPTIONS) { val layerConfigs: List val facets: PlotFacets val scaleProvidersMap: TypedScaleProviderMap @@ -44,11 +43,23 @@ abstract class PlotConfig(opts: Map) : OptionsAccessor( init { - sharedData = ConfigUtil.createDataFrame(get(DATA)) - checkState(sharedData != null) + val (plotMappings, plotData) = DataMetaUtil.createDataFrame( + options = this, + commonData = DataFrame.Builder.emptyFrame(), + commonDiscreteAes = emptySet(), + commonMappings = emptyMap(), + isClientSide = isClientSide + ) + + sharedData = plotData - scaleConfigs = createScaleConfigs() + if (!isClientSide) { + update(MAPPING, plotMappings) + } + + scaleConfigs = createScaleConfigs(getList(SCALES) + DataMetaUtil.createScaleSpecs(opts)) scaleProvidersMap = PlotConfigUtil.createScaleProviders(scaleConfigs) + layerConfigs = createLayerConfigs(sharedData, scaleProvidersMap) facets = if (has(FACET)) { @@ -64,10 +75,9 @@ abstract class PlotConfig(opts: Map) : OptionsAccessor( } } - fun createScaleConfigs(): List> { + fun createScaleConfigs(scaleOptionsList: List<*>): List> { // merge options by 'aes' val mergedOpts = HashMap, MutableMap>() - val scaleOptionsList = getList(SCALES) for (opts in scaleOptionsList) { val optsMap = opts as Map<*, *> val aes = ScaleConfig.aesOrFail(optsMap) @@ -102,6 +112,7 @@ abstract class PlotConfig(opts: Map) : OptionsAccessor( layerOptions as Map<*, *>, sharedData, getMap(MAPPING), + DataMetaUtil.getAsDiscreteAesSet(getMap(DATA_META)), scaleProviderByAes ) layerConfigs.add(layerConfig) @@ -112,7 +123,8 @@ abstract class PlotConfig(opts: Map) : OptionsAccessor( protected abstract fun createLayerConfig( layerOptions: Map<*, *>, sharedData: DataFrame?, - plotMapping: Map<*, *>, + plotMappings: Map<*, *>, + plotDiscreteAes: Set, scaleProviderByAes: TypedScaleProviderMap ): LayerConfig diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigClientSide.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigClientSide.kt index 623a159d0cb..2bda59be737 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigClientSide.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigClientSide.kt @@ -48,7 +48,8 @@ class PlotConfigClientSide private constructor(opts: Map) : PlotCon override fun createLayerConfig( layerOptions: Map<*, *>, sharedData: DataFrame?, - plotMapping: Map<*, *>, + plotMappings: Map<*, *>, + plotDiscreteAes: Set, scaleProviderByAes: TypedScaleProviderMap ): LayerConfig { @@ -57,7 +58,8 @@ class PlotConfigClientSide private constructor(opts: Map) : PlotCon return LayerConfig( layerOptions, sharedData!!, - plotMapping, + plotMappings, + plotDiscreteAes, GeomProtoClientSide(geomKind), StatProto(), scaleProviderByAes, true diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/PlotConfigServerSide.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/PlotConfigServerSide.kt index f82d64bee33..f787d608279 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/PlotConfigServerSide.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/PlotConfigServerSide.kt @@ -24,7 +24,8 @@ open class PlotConfigServerSide(opts: Map) : PlotConfig(opts) { override fun createLayerConfig( layerOptions: Map<*, *>, sharedData: DataFrame?, - plotMapping: Map<*, *>, + plotMappings: Map<*, *>, + plotDiscreteAes: Set, scaleProviderByAes: TypedScaleProviderMap ): LayerConfig { @@ -33,7 +34,8 @@ open class PlotConfigServerSide(opts: Map) : PlotConfig(opts) { return LayerConfig( layerOptions, sharedData!!, - plotMapping, + plotMappings, + plotDiscreteAes, GeomProto(geomKind), StatProto(), scaleProviderByAes, diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/DiscreteScaleFromAnnotationChange.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/DiscreteScaleFromAnnotationChange.kt deleted file mode 100644 index 097ee8a3ac8..00000000000 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/DiscreteScaleFromAnnotationChange.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2020. 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.server.config.transform - -import jetbrains.datalore.plot.config.* -import jetbrains.datalore.plot.config.Option.Meta.DATA_META -import jetbrains.datalore.plot.config.Option.Meta.SeriesAnnotation -import jetbrains.datalore.plot.config.Option.Meta.SeriesAnnotation.ANNOTATION -import jetbrains.datalore.plot.config.Option.Meta.SeriesAnnotation.VARIABLE -import jetbrains.datalore.plot.config.Option.Plot -import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING -import jetbrains.datalore.plot.config.Option.Scale -import jetbrains.datalore.plot.config.transform.SpecChange -import jetbrains.datalore.plot.config.transform.SpecChangeContext -import jetbrains.datalore.plot.config.transform.SpecSelector - -class DiscreteScaleFromAnnotationChange : SpecChange { - override fun apply(spec: MutableMap, ctx: SpecChangeContext) { - val annotationScales = spec - .sections(Plot.LAYERS)!! - .filter { it.has(DATA_META, SeriesAnnotation.TAG) && it.has(MAPPING) } - .flatMap(::scalesFromAnnotation) - - if (annotationScales.isNotEmpty()) { - spec.provideSections(Plot.SCALES).asMutable().addAll(annotationScales) - } - } - - companion object { - private fun scalesFromAnnotation(layer: Map<*, *>): List> { - val mapping = layer.section(MAPPING)!!.entries.associateBy({ it.value }, { it.key as String }) - - return layer.sections(DATA_META, SeriesAnnotation.TAG)!! - .filter { it.read(ANNOTATION) == SeriesAnnotation.DISCRETE } - .mapNotNull { it.read(VARIABLE).run(mapping::get) } - .map { aes -> - mutableMapOf( - Scale.AES to aes, - Scale.DISCRETE_DOMAIN to true - ) - } - } - - internal fun specSelector(): SpecSelector { - return SpecSelector.of() - } - } -} diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoDataFrameMappingChange.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoDataFrameMappingChange.kt index ecf688e103b..97d1e900d54 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoDataFrameMappingChange.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoDataFrameMappingChange.kt @@ -25,7 +25,7 @@ class GeoDataFrameMappingChange : SpecChange { override fun apply(spec: MutableMap, ctx: SpecChangeContext) { val geometryColumnName = spec.read(DATA_META, GeoDataFrame.TAG, GEOMETRY) as String - val geometries = spec.list(DATA, geometryColumnName)!! + val geometries = spec.getList(DATA, geometryColumnName)!! val keys = geometries.indices.map(Int::toString) spec.remove(DATA, geometryColumnName) diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoPositionMappingChange.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoPositionMappingChange.kt index b90fd884c39..99d8f4866df 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoPositionMappingChange.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/GeoPositionMappingChange.kt @@ -16,9 +16,9 @@ import jetbrains.datalore.plot.config.Option.Meta.GeoDataFrame.GEOMETRY import jetbrains.datalore.plot.config.Option.Meta.GeoReference import jetbrains.datalore.plot.config.Option.Meta.MAP_DATA_META import jetbrains.datalore.plot.config.Option.Plot +import jetbrains.datalore.plot.config.getMap import jetbrains.datalore.plot.config.has import jetbrains.datalore.plot.config.read -import jetbrains.datalore.plot.config.section import jetbrains.datalore.plot.config.transform.SpecChange import jetbrains.datalore.plot.config.transform.SpecChangeContext import jetbrains.datalore.plot.config.transform.SpecSelector @@ -27,7 +27,7 @@ import jetbrains.datalore.plot.config.write class GeoPositionMappingChange : SpecChange { override fun apply(spec: MutableMap, ctx: SpecChangeContext) { - val mapSpec = spec.section(GEO_POSITIONS)!! + val mapSpec = spec.getMap(GEO_POSITIONS)!! spec.read(MAP_DATA_META, GeoDataFrame.TAG, GEOMETRY) ?.let {it as String } ?.let { geometry -> diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/MapJoinChange.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/MapJoinChange.kt index 0b1f0b721eb..8725633a2a4 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/MapJoinChange.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/MapJoinChange.kt @@ -11,7 +11,7 @@ import jetbrains.datalore.plot.config.Option import jetbrains.datalore.plot.config.Option.Mapping.MAP_ID import jetbrains.datalore.plot.config.Option.Meta.MAP_DATA_META import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING -import jetbrains.datalore.plot.config.list +import jetbrains.datalore.plot.config.getList import jetbrains.datalore.plot.config.transform.SpecChange import jetbrains.datalore.plot.config.transform.SpecChangeContext import jetbrains.datalore.plot.config.transform.SpecSelector @@ -19,7 +19,7 @@ import jetbrains.datalore.plot.config.write class MapJoinChange: SpecChange { override fun apply(spec: MutableMap, ctx: SpecChangeContext) { - val (dataJoinColumn, mapJoinColumn) = spec.list(MAP_JOIN)!! + val (dataJoinColumn, mapJoinColumn) = spec.getList(MAP_JOIN)!! dataJoinColumn?.let { spec.write(MAPPING, MAP_ID) { it } } mapJoinColumn?.let { spec.write(MAP_JOIN_COLUMN) { it } } } diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/PlotConfigServerSideTransforms.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/PlotConfigServerSideTransforms.kt index 2ef648cb40c..1ba4ae31cd8 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/PlotConfigServerSideTransforms.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/server/config/transform/PlotConfigServerSideTransforms.kt @@ -41,10 +41,6 @@ object PlotConfigServerSideTransforms { ReplaceDataVectorsInAesMappingChange.specSelector(), ReplaceDataVectorsInAesMappingChange() ) - .change( - DiscreteScaleFromAnnotationChange.specSelector(), - DiscreteScaleFromAnnotationChange() - ) .change( MapJoinChange.specSelector(), MapJoinChange() diff --git a/plot-config-portable/src/jvmTest/kotlin/plot/server/config/transform/DiscreteScaleFromAnnotationChangeTest.kt b/plot-config-portable/src/jvmTest/kotlin/plot/server/config/transform/DiscreteScaleFromAnnotationChangeTest.kt deleted file mode 100644 index 92e6f1eafd1..00000000000 --- a/plot-config-portable/src/jvmTest/kotlin/plot/server/config/transform/DiscreteScaleFromAnnotationChangeTest.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2020. 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.server.config.transform -import jetbrains.datalore.plot.base.Aes -import jetbrains.datalore.plot.config.Option -import jetbrains.datalore.plot.config.Option.Mapping.toOption -import jetbrains.datalore.plot.config.Option.Meta -import jetbrains.datalore.plot.config.Option.Meta.SeriesAnnotation -import jetbrains.datalore.plot.config.Option.Meta.SeriesAnnotation.DISCRETE -import jetbrains.datalore.plot.config.Option.Plot -import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING -import jetbrains.datalore.plot.config.read -import jetbrains.datalore.plot.config.sections -import jetbrains.datalore.plot.config.transform.SpecChangeContext -import jetbrains.datalore.plot.config.write -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class DiscreteScaleFromAnnotationChangeTest { - - @Test - fun withoutMetaShouldNotAddScale() { - val varName = "drv" - val plotSpec = dict { - layers( - dict { - write(MAPPING, toOption(Aes.COLOR)) { varName } - } - ) - } - - DiscreteScaleFromAnnotationChange().apply(plotSpec, dummyCtx) - plotSpec.sections(Plot.SCALES).run { assertNull(this) } - } - - @Test - fun discreteWithoutScale() { - val varName = "drv" - val plotSpec = dict { - layers( - dict { - write(MAPPING, toOption(Aes.COLOR)) { varName } - write(Meta.DATA_META, SeriesAnnotation.TAG) { - list( - dict { - write(SeriesAnnotation.VARIABLE) { varName } - write(SeriesAnnotation.ANNOTATION) { DISCRETE } - } - ) - } - } - ) - } - DiscreteScaleFromAnnotationChange().apply(plotSpec, dummyCtx) - - plotSpec.sections(Plot.SCALES)!![0].run { - read(Option.Scale.AES).run { assertEquals(toOption(Aes.COLOR), this) } - read(Option.Scale.DISCRETE_DOMAIN).run { assertEquals(true, this) } - read(Option.Scale.SCALE_MAPPER_KIND).run { assertNull(this) } - } - } - - @Test - fun discreteWithExistingDiffrentAesScale_ShouldMergeScales() { - val varName = "drv" - val plotSpec = dict { - scales( - dict { - write(Option.Scale.AES) { toOption(Aes.ALPHA) } - write(Option.Scale.DISCRETE_DOMAIN) { true } - } - ) - layers( - dict { - write(MAPPING, toOption(Aes.COLOR)) { varName } - write(Meta.DATA_META, SeriesAnnotation.TAG) { - list( - dict { - write(SeriesAnnotation.VARIABLE) { varName } - write(SeriesAnnotation.ANNOTATION) { DISCRETE } - } - ) - } - } - ) - } - DiscreteScaleFromAnnotationChange().apply(plotSpec, dummyCtx) - - with(plotSpec.sections(Plot.SCALES)!![0]) { - read(Option.Scale.AES).run { assertEquals(toOption(Aes.ALPHA), this) } - read(Option.Scale.DISCRETE_DOMAIN).run { assertEquals(true, this) } - read(Option.Scale.SCALE_MAPPER_KIND).run { assertNull(this) } - } - - with(plotSpec.sections(Plot.SCALES)!![1]) { - read(Option.Scale.AES).run { assertEquals(toOption(Aes.COLOR), this) } - read(Option.Scale.DISCRETE_DOMAIN).run { assertEquals(true, this) } - read(Option.Scale.SCALE_MAPPER_KIND).run { assertNull(this) } - } - } -} - - -private fun dict(block: MutableMap.() -> Unit): MutableMap { - return mutableMapOf().apply(block) -} - -private fun MutableMap.layers(vararg layerBuilder: MutableMap) { - this[Plot.LAYERS] = layerBuilder.toMutableList() -} - -private fun MutableMap.scales(vararg scaleBuilder: MutableMap) { - this[Plot.SCALES] = scaleBuilder.toMutableList() -} - -private fun list(vararg items: Any): MutableList { - return mutableListOf(*items) -} - -private val dummyCtx = object : SpecChangeContext { - override fun getSpecsAbsolute(vararg keys: String): List> = error("Not expected to be invoked") -} - diff --git a/plot-config/src/jvmTest/kotlin/plot/config/AsDiscreteTest.kt b/plot-config/src/jvmTest/kotlin/plot/config/AsDiscreteTest.kt new file mode 100644 index 00000000000..e8540138abb --- /dev/null +++ b/plot-config/src/jvmTest/kotlin/plot/config/AsDiscreteTest.kt @@ -0,0 +1,751 @@ +/* + * Copyright (c) 2020. 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.config + +import jetbrains.datalore.plot.base.Aes +import jetbrains.datalore.plot.base.DataFrame +import jetbrains.datalore.plot.base.data.DataFrameUtil +import jetbrains.datalore.plot.base.data.DataFrameUtil.variables +import jetbrains.datalore.plot.base.stat.Stats +import jetbrains.datalore.plot.config.AsDiscreteTest.Storage.LAYER +import jetbrains.datalore.plot.config.AsDiscreteTest.Storage.PLOT +import jetbrains.datalore.plot.config.DataMetaUtil.toDiscrete +import jetbrains.datalore.plot.config.PlotConfig.Companion.getErrorMessage +import jetbrains.datalore.plot.config.PlotConfig.Companion.isFailure +import jetbrains.datalore.plot.parsePlotSpec +import jetbrains.datalore.plot.server.config.ServerSideTestUtil +import org.junit.Ignore +import org.junit.Test +import kotlin.math.pow +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class AsDiscreteTest { + + private val data = """ + |{ + | "x": [0, 2, 5, 8, 9, 12, 16, 20, 40], + | "y": [3, 1, 2, 7, 8, 9, 10, 10, 10], + | "g": [0, 0, 0, 1, 1, 1, 2, 2, 2], + | "cyl": [0, 0, 0, 1, 1, 1, 2, 2, 2], + | "d": ["0", "0", "0", "1", "1", "1", "2", "2", "2"] + |} + """.trimMargin() + + enum class Storage { + PLOT, + LAYER + } + + private fun makePlotSpec( + geom: String, + dataStorage: Storage, + mappingStorage: Storage + ): String { + val data = "\"data\": ${this.data}," + val mapping = "\"color\": \"g\"" + val annotation = """ + |"data_meta": { + | "mapping_annotation": [ + | { + | "aes": "color", + | "annotation": "as_discrete" + | } + | ] + |}, + """.trimMargin() + + return """ + |{ + | ${data.takeIf { dataStorage == PLOT } ?: ""} + | ${annotation.takeIf { mappingStorage == PLOT } ?: ""} + | "mapping": { + | ${(mapping + ",").takeIf { mappingStorage == PLOT } ?: ""} + | "x": "x", + | "y": "y" + | }, + | "kind": "plot", + | "layers": [ + | { + | ${data.takeIf { dataStorage == LAYER } ?: ""} + | ${annotation.takeIf { mappingStorage == LAYER } ?: ""} + | "geom": "$geom", + | "mapping": { + | ${mapping.takeIf { mappingStorage == LAYER } ?: ""} + | } + | } + | ] + |} + """.trimMargin() + } + + @Test + fun plot_LayerDataMapping_Geom() { + val spec = makePlotSpec( + geom = "point", + dataStorage = LAYER, + mappingStorage = LAYER + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotDataMapping_Layer_Geom() { + val spec = makePlotSpec( + geom = "point", + dataStorage = PLOT, + mappingStorage = PLOT + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotData_LayerMapping_Geom() { + val spec = makePlotSpec( + geom = "point", + dataStorage = PLOT, + mappingStorage = LAYER + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotMapping_LayerData_Geom() { + val spec = makePlotSpec( + geom = "point", + dataStorage = LAYER, + mappingStorage = PLOT + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plot_LayerDataMapping_Stat() { + val spec = makePlotSpec( + geom = "smooth", + dataStorage = LAYER, + mappingStorage = LAYER + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotDataMapping_Layer_Stat() { + val spec = makePlotSpec( + geom = "smooth", + dataStorage = PLOT, + mappingStorage = PLOT + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotData_LayerMapping_Stat() { + val spec = makePlotSpec( + geom = "smooth", + dataStorage = PLOT, + mappingStorage = LAYER + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun plotMapping_LayerData_Stat() { + val spec = makePlotSpec( + geom = "smooth", + dataStorage = LAYER, + mappingStorage = PLOT + ) + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun smoothAsDiscreteWithGroupVar() { + val spec = """ + |{ + | "data": $data, + | "mapping": { + | "x": "x", + | "y": "y" + | }, + | "kind": "plot", + | "layers": [ + | { + | "geom": "smooth", + | "mapping": { + | "color": "g", + | "group": "g" + | }, + | "data_meta": { + | "mapping_annotation": [ + | { + | "aes": "color", + | "annotation": "as_discrete" + | } + | ] + | } + | } + | ] + |}""".trimMargin() + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Ignore + @Test + fun smoothAsDiscreteWithStatVar() { + val spec = """ + |{ + | "data": $data, + | "data_meta": { + | "mapping_annotation": [ + | { + | "aes": "color", + | "annotation": "as_discrete" + | } + | ] + | }, + | "mapping": { + | "x": "x", + | "y": "y", + | "color": "g" + | }, + | "kind": "plot", + | "layers": [ + | { + | "geom": "smooth", + | "mapping": { + | "color": "g" + | } + | } + | ] + |}""".trimMargin() + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .hasVariable(Stats.GROUP) + .assertVariable(varName = "g", isDiscrete = false) + } + + @Test + fun mergingMappingsAndData() { + fun buildSpec( + geom: String, + fooData: Storage, + fooMapping: Storage, + barData: Storage, + barMapping: Storage + ): String { + fun formatSpec( + values: List>, + targetStorage: Storage, + format: (String) -> String + ): String? { + return values + .filter { (thisStorage, _) -> thisStorage == targetStorage } + .takeIf { it.isNotEmpty() } // introduce nullability for easier string interpolation + ?.let { format(it.joinToString { (_, str) -> str }) } + } + + val fooDataSpec = """"foo": [0, 0, 0, 1, 1, 1, 2, 2, 2]""" + val fooMappingSpec = """"color": "foo"""" + val fooAnnotationSpec = """{"aes": "color", "annotation": "as_discrete"}""" + + val barDataSpec = """"bar": [3, 3, 3, 4, 4, 4, 5, 5, 5]""" + val barMappingSpec = """"fill": "bar"""" + val barAnnotationSpec = """{"aes": "fill", "annotation": "as_discrete"}""" + + fun formatDataSpec(values: List>, targetStorage: Storage): String? = + formatSpec(values, targetStorage) { """"data": {${it}}""" } + + fun formatMappingSpec(values: List>, targetStorage: Storage): String? = + formatSpec(values, targetStorage) { """"mapping": {${it}}""" } + + fun formatAnnotationSpec(values: List>, targetStorage: Storage): String? = + formatSpec(values, targetStorage) { """"data_meta": {"mapping_annotation": [$it]}""" } + + val data = listOf(fooData to fooDataSpec, barData to barDataSpec) + val mapping = listOf(fooMapping to fooMappingSpec, barMapping to barMappingSpec) + val annotation = listOf(fooMapping to fooAnnotationSpec, barMapping to barAnnotationSpec) + + return """ + |{ + | "kind": "plot", + | ${formatDataSpec(data, PLOT)?.let { it + "," } ?: ""} + | "splitter_qwe": "qwe", + | ${formatAnnotationSpec(annotation, PLOT)?.let { it + "," } ?: ""} + | "delimiter_asd": "asd", + | ${formatMappingSpec(mapping, PLOT)?.let { it + "," } ?: ""} + | "delimiter_zxc": "zxc", + | "layers": [ + | { + | "geom": "$geom", + | ${formatDataSpec(data, LAYER)?.let { it + "," } ?: ""} + | "splitter_qwe": "qwe", + | ${formatAnnotationSpec(annotation, LAYER)?.let { it + "," } ?: ""} + | "delimiter_asd": "asd", + | ${formatMappingSpec(mapping, LAYER)?.let { it + "," } ?: ""} + | "delimiter_zxc": "zxc" + | } + | ] + |} + """.trimMargin() + } + + data class Case(val fooMapping: Storage, val fooData: Storage, val barData: Storage, val barMapping: Storage) + + fun Int.bit(bitNumber: Int) = this.and(1 shl bitNumber) != 0 + fun Boolean.asStorage() = PLOT.takeIf { this == false } ?: LAYER + fun Int.asCase() = Case(bit(0).asStorage(), bit(1).asStorage(), bit(2).asStorage(), bit(3).asStorage()) + + // generate cases + val cases = (0 until 2.0.pow(4).toInt()).map(Int::asCase).toSet() + + // validate test data - all cases have to be unique to test different combinations. Set will drop same cases. + assertEquals(2.0.pow(4).toInt(), cases.count()) + + cases.forEach { case -> + buildSpec( + geom = "point", + fooData = case.fooData, + fooMapping = case.fooMapping, + barData = case.barData, + barMapping = case.barMapping + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) { case.toString() } + .assertScale(Aes.FILL, isDiscrete = true) { case.toString() } + .assertVariable(toDiscrete("foo"), isDiscrete = true) { case.toString() } + .assertVariable(toDiscrete("bar"), isDiscrete = true) { case.toString() } + + } + } + + @Test + fun groupingWithDiscreteVariable() { + val spec = """ + |{ + | "ggtitle": {"text": "ggplot + geom_line(data, color=d)"}, + | "kind": "plot", + | "layers": [ + | { + | "data": $data, + | "geom": "line", + | "mapping": { + | "x": "x", + | "y": "y", + | "color": "d" + | } + | } + | ] + |}""".trimMargin() + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable("d", isDiscrete = true) + } + + @Test + fun groupingWithAsDiscrete() { + val spec = """ + |{ + | "ggtitle": {"text": "ggplot + geom_line(data, color=as_discrete(g))"}, + | "kind": "plot", + | "layers": [ + | { + | "data": $data, + | "geom": "line", + | "mapping": { + | "x": "x", + | "y": "y", + | "color": "g" + | }, + | "data_meta": { + | "mapping_annotation": [ + | { + | "aes": "color", + | "annotation": "as_discrete" + | } + | ] + | } + | } + | ] + |}""".trimMargin() + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("g"), isDiscrete = true) + } + + @Test + fun `color='cyl', fill=as_discrete('cyl')`() { + val spec = """ + |{ + | "data": $data, + | "mapping": { + | "x": "x", + | "y": "y" + | }, + | "kind": "plot", + | "layers": [ + | { + | "geom": "point", + | "mapping": { + | "color": "cyl", + | "fill": "cyl" + | }, + | "data_meta": { + | "mapping_annotation": [ + | { + | "aes": "fill", + | "annotation": "as_discrete" + | } + | ] + | } + | } + | ] + |}""".trimMargin() + + toClientPlotConfig(spec) + .assertScale(Aes.FILL, isDiscrete = true) + .assertScale(Aes.COLOR, isDiscrete = false) + .assertVariable(toDiscrete("cyl"), isDiscrete = true) + .assertVariable("cyl", isDiscrete = false) + } + + @Test + fun `ggplot(color='cyl') + geom_point(color=as_discrete('cyl'))`() { + val spec = """ + |{ + | "data": $data, + | "mapping": { + | "x": "x", + | "y": "y", + | "color": "cyl" + | }, + | "kind": "plot", + | "layers": [ + | { + | "geom": "point", + | "mapping": { + | "color": "cyl" + | }, + | "data_meta": { + | "mapping_annotation": [ + | { + | "aes": "color", + | "annotation": "as_discrete" + | } + | ] + | } + | } + | ] + |}""".trimMargin() + + toClientPlotConfig(spec) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("cyl"), isDiscrete = true) + } + + val cyl123 = "\"cyl\": [1, 2, 3]" + val cyl456 = "\"cyl\": [4, 5, 6]" + + @Test + fun `mapping overriding ggplot(cyl123, color=as_discrete('cyl')) + geom_point()`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = null, + layerMapping = false, + layerAnnotation = false + ).let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) // as_discrete in plot + .assertVariable(toDiscrete("cyl"), isDiscrete = true) // no overriding in layer + } + + @Test + fun `mapping overriding ggplot(cyl123, color=as_discrete('cyl')) + geom_point(color=as_discrete('cyl'))`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = null, + layerMapping = true, + layerAnnotation = true + ).let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) // as_discrete in plot + .assertVariable(toDiscrete("cyl"), isDiscrete = true) // as_discrete in layer + } + + @Test + fun `mapping overriding ggplot(cyl123) + geom_point(color=as_discrete('cyl'))`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = false, + plotAnnotation = false, + layerData = null, + layerMapping = true, + layerAnnotation = true + ).let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) // as_discrete in layer + .assertVariable(toDiscrete("cyl"), isDiscrete = true) // as_discrete in layer + } + + @Test + fun `mapping overriding ggplot(cyl123, color=as_discrete('cyl')) + geom_point(color='cyl')`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = null, + layerMapping = true, + layerAnnotation = false + ).let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) // as_discrete in plot + .assertVariable("cyl", isDiscrete = false) // as is (numeric, overrided by layer) + } + + @Test + fun `overriding ggplot(cyl123, color=cyl) + geom_point(cyl456, color=cyl)`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = false, + layerData = cyl456, + layerMapping = true, + layerAnnotation = false + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = false) + .assertVariable("cyl", isDiscrete = false) + .assertValue("cyl", listOf(4.0, 5.0, 6.0)) + } + + @Test + fun `overriding ggplot(cyl123, color=as_discrete(cyl)) + geom_point(cyl456)`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = cyl456, + layerMapping = false, + layerAnnotation = false + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("cyl"), isDiscrete = true) + .assertValue(toDiscrete("cyl"), listOf(4.0, 5.0, 6.0)) + } + + @Test + fun `overriding ggplot(cyl123) + geom_point(cyl456, color=as_discrete(cyl))`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = false, + plotAnnotation = false, + layerData = cyl456, + layerMapping = true, + layerAnnotation = true + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable(toDiscrete("cyl"), isDiscrete = true) + .assertValue(toDiscrete("cyl"), listOf(4.0, 5.0, 6.0)) + } + + @Test + fun `overriding ggplot(cyl123, color=as_discrete(cyl)) + geom_point(cyl456, color=cyl)`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = cyl456, + layerMapping = true, + layerAnnotation = false + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable("cyl", isDiscrete = false) + .assertValue("cyl", listOf(4.0, 5.0, 6.0)) + } + + @Test + fun `overriding ggplot(cyl123, color=as_discrete(cyl)) + geom_point(color=cyl)`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = true, + layerData = null, + layerMapping = true, + layerAnnotation = false + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = true) + .assertVariable("cyl", isDiscrete = false) + .assertValue("cyl", listOf(1.0, 2.0, 3.0)) + } + + @Test + fun `overriding ggplot(cyl123, color=cyl) + geom_point(color=cyl)`() { + buildSpecWithOverriding( + geom = "point", + plotData = cyl123, + plotMapping = true, + plotAnnotation = false, + layerData = null, + layerMapping = true, + layerAnnotation = false + ) + .let(::toClientPlotConfig) + .assertScale(Aes.COLOR, isDiscrete = false) + .assertVariable("cyl", isDiscrete = false) + .assertValue("cyl", listOf(1.0, 2.0, 3.0)) + } +} + +private fun PlotConfigClientSide.assertValue(variable: String, values: List<*>): PlotConfigClientSide { + val data = layerConfigs.single().combinedData + assertEquals(values, data.get(variables(data)[variable]!!)) + return this +} + +private fun PlotConfigClientSide.assertVariable( + varName: String, + isDiscrete: Boolean, + msg: () -> String = { "" } +): PlotConfigClientSide { + val layer = layerConfigs.single() + if (!DataFrameUtil.hasVariable(layer.combinedData, varName)) { + fail("Variable $varName is not found in ${layer.combinedData.variables().map(DataFrame.Variable::name)}") + } + val dfVar = DataFrameUtil.findVariableOrFail(layer.combinedData, varName) + assertEquals(!isDiscrete, layer.combinedData.isNumeric(dfVar), msg()) + return this +} + +private fun PlotConfigClientSide.assertScale( + aes: Aes<*>, + isDiscrete: Boolean, + msg: () -> String = { "" } +): PlotConfigClientSide { + val layer = layerConfigs.single() + val binding = layer.varBindings.firstOrNull { it.aes == aes } ?: fail("$aes not found. ${msg()}") + assertEquals(!isDiscrete, binding.scale!!.isContinuous) + return this +} + +private fun PlotConfigClientSide.hasVariable(variable: DataFrame.Variable): PlotConfigClientSide { + val layer = layerConfigs.single() + assertTrue(DataFrameUtil.hasVariable(layer.combinedData, variable.name)) + return this +} + +private fun toClientPlotConfig(spec: String): PlotConfigClientSide { + return parsePlotSpec(spec) + .let(ServerSideTestUtil::serverTransformWithoutEncoding) + .also { require(!isFailure(it)) { getErrorMessage(it) } } + .let(TestUtil::assertClientWontFail) +} + + +private fun buildSpecWithOverriding( + geom: String, + plotData: String?, + plotMapping: Boolean, + plotAnnotation: Boolean, + layerData: String?, + layerMapping: Boolean, + layerAnnotation: Boolean +): String { + fun formatSpec( + values: List>, + targetStorage: AsDiscreteTest.Storage, + format: String + ): String? { + return values + .filter { (thisStorage, _) -> thisStorage == targetStorage } + .takeIf { it.isNotEmpty() } // introduce nullability for easier string interpolation + ?.let { it.joinToString { (_, str) -> str } } + ?.let { format.replace("%s", it) } + } + + val data = listOfNotNull( + plotData?.let { PLOT to it }, + layerData?.let { LAYER to layerData } + ) + + val cylMappingSpec = """"color": "cyl"""" + + val mapping = listOfNotNull( + (PLOT to cylMappingSpec).takeIf { plotMapping }, + (LAYER to cylMappingSpec).takeIf { layerMapping } + ) + + val cylAnnotationSpec = """{"aes": "color", "annotation": "as_discrete"}""" + val annotation = listOfNotNull( + (PLOT to cylAnnotationSpec).takeIf { plotAnnotation }, + (LAYER to cylAnnotationSpec).takeIf { layerAnnotation } + ) + + return """ + |{ + | "kind": "plot", + | ${"\"data\": {%s},".let { formatSpec(data, PLOT, it) } ?: ""} + | "splitter_qwe": "qwe", + | ${"\"data_meta\": {\"mapping_annotation\": [%s]},".let { formatSpec(annotation, PLOT, it) } ?: ""} + | "delimiter_asd": "asd", + | ${"\"mapping\": {%s},".let { formatSpec(mapping, PLOT, it) } ?: ""} + | "delimiter_zxc": "zxc", + | "layers": [ + | { + | "geom": "$geom", + | ${"\"data\": {%s},".let { formatSpec(data, LAYER, it) } ?: ""} + | "splitter_qwe": "qwe", + | ${"\"data_meta\": {\"mapping_annotation\": [%s]},".let { formatSpec(annotation, LAYER, it) } ?: ""} + | "delimiter_asd": "asd", + | ${"\"mapping\": {%s},".let { formatSpec(mapping, LAYER, it) } ?: ""} + | "delimiter_zxc": "zxc" + | } + | ] + |} + """.trimMargin() +} diff --git a/plot-config/src/jvmTest/kotlin/plot/config/PlotConfigTest.kt b/plot-config/src/jvmTest/kotlin/plot/config/PlotConfigTest.kt index 6f55be55036..3fa446ff064 100644 --- a/plot-config/src/jvmTest/kotlin/plot/config/PlotConfigTest.kt +++ b/plot-config/src/jvmTest/kotlin/plot/config/PlotConfigTest.kt @@ -5,6 +5,7 @@ package jetbrains.datalore.plot.config +import jetbrains.datalore.plot.config.Option.Plot.SCALES import jetbrains.datalore.plot.config.Option.Scale.CONTINUOUS_TRANSFORM import jetbrains.datalore.plot.config.Option.Scale.NAME import jetbrains.datalore.plot.parsePlotSpec @@ -32,7 +33,7 @@ class PlotConfigTest { val opts = parsePlotSpec(spec) val plotConfig = PlotConfigClientSide.create(opts) - val scaleConfigs = plotConfig.createScaleConfigs() + val scaleConfigs = plotConfig.createScaleConfigs(plotConfig.getList(SCALES)) assertEquals(1, scaleConfigs.size.toLong()) assertEquals("name_test", scaleConfigs[0].getString(NAME)) assertEquals("log10", scaleConfigs[0].getString(CONTINUOUS_TRANSFORM)) diff --git a/plot-config/src/jvmTest/kotlin/plot/server/config/GGBunchTest.kt b/plot-config/src/jvmTest/kotlin/plot/server/config/GGBunchTest.kt index e70f6d201cb..d87c157f04c 100644 --- a/plot-config/src/jvmTest/kotlin/plot/server/config/GGBunchTest.kt +++ b/plot-config/src/jvmTest/kotlin/plot/server/config/GGBunchTest.kt @@ -61,7 +61,8 @@ class GGBunchTest { val plotSpec = mutableMapOf( Option.Meta.KIND to Option.Meta.Kind.PLOT, - Option.Plot.LAYERS to listOf(geom) + Option.Meta.KIND to Option.Meta.Kind.PLOT, + Option.PlotBase.MAPPING to emptyMap() ) itemsList.add( diff --git a/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/AsDiscrete.kt b/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/AsDiscrete.kt new file mode 100644 index 00000000000..becf1a2c827 --- /dev/null +++ b/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/AsDiscrete.kt @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2020. 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.model.PlotConfigDemoBase + +class AsDiscrete : PlotConfigDemoBase() { + fun plotSpecList(): List> { + return listOf( + plotData_DiscreteGroup(), + fillFactor(), + fillAndColorFactor(), + fillFactorWithScaleColor(), + layerData_DiscreteGroup(), + smoothStatAsDiscrete(), + smoothStatWithGroup() + ) + } + + private val data = """{ + |"x": [0, 5, 10, 15], + |"y": [0, 5, 10, 15], + |"a": [1, 2, 4, 6], + |"b": [10, 11, 12, 13], + |"c": ["a", "a", "b", "b"], + |"g": [0, 0, 1, 1] +|}""".trimMargin() + + private val smoothData = """ + |{ + | "x": [0, 2, 5, 8, 9, 12, 16, 20, 40], + | "y": [3, 1, 2, 7, 8, 9, 10, 10, 10], + | "g": [0, 0, 0, 1, 1, 1, 2, 2, 2], + | "d": ['0', '0', '0', '1', '1', '1', '2', '2', '2'] + |} + """.trimMargin() + + + private fun plotData_DiscreteGroup(): Map { + val spec = """ + { + "kind": "plot", + "data": $data, + "mapping": { + "x": "x", + "y": "y", + "color": "g" + }, + "data_meta": { + "mapping_annotation": [ + { + "aes": "color", + "annotation": "as_discrete" + } + ] + }, + "layers": [ + { + "geom": "line", + "mapping": { + }, + "size": 3 + } + ] + } + """.trimIndent() + return parsePlotSpec(spec) + } + + private fun layerData_DiscreteGroup(): Map { + val spec = """ + { + "kind": "plot", + "layers": [ + { + "data": $data, + "geom": "line", + "mapping": { + "x": "x", + "y": "y", + "color": "g" + }, + "data_meta": { + "mapping_annotation": [ + { + "aes": "color", + "annotation": "as_discrete" + } + ] + }, + "size": 3 + } + ] + } + """.trimIndent() + return parsePlotSpec(spec) + } + + private fun smoothStatAsDiscrete(): Map { + val spec = """ + { + "mapping": { + "x": "x", + "y": "y" + }, + "kind": "plot", + "layers": [ + { + "data": $smoothData, + "geom": "smooth", + "mapping": { + "color": "g" + }, + "data_meta": { + "mapping_annotation": [ + { + "aes": "color", + "annotation": "as_discrete" + } + ] + }, + "se": false + } + ] + } + """.trimIndent() + return parsePlotSpec(spec) + } + private fun smoothStatWithGroup(): Map { + val spec = """ + { + "data": $smoothData, + "mapping": { + "x": "x", + "y": "y" + }, + "kind": "plot", + "layers": [ + { + "geom": "smooth", + "mapping": { + "color": "g", + "group": "g" + }, + "data_meta": { + "mapping_annotation": [ + { + "aes": "color", + "annotation": "as_discrete" + } + ] + }, + "se": false + } + ] + } + """.trimIndent() + return parsePlotSpec(spec) + } + + + private fun fillFactor(): Map { + + val spec = """ +{ + "ggtitle": { "text": "... fill=as_discrete(a) ..."}, + "kind": "plot", + "layers": [{ + "geom": "point", + "data": $data, + "mapping": {"x": "x", "y": "y", "fill": "a", "color": "b"}, + "data_meta": {"mapping_annotation": [{"aes": "fill", "annotation": "as_discrete"}]}, + "shape": 21, + "size": 9 + }] +} """.trimIndent() + return parsePlotSpec(spec) + } + + + private fun fillFactorWithScaleColor(): Map { + + val spec = """ +{ + "ggtitle": { "text": "... fill=as_discrete(a), scale_color_discrete() ..."}, + "data": $data, + "mapping": {"x": "x", "y": "y"}, + "kind": "plot", + "scales": [{"aesthetic": "color", "discrete": true}], + "layers": [{ + "geom": "point", + "mapping": {"fill": "a", "color": "b"}, + "data_meta": {"mapping_annotation": [{"aes": "fill", "annotation": "as_discrete"}]}, + "shape": 21, + "size": 9 + }] +} """.trimIndent() + return parsePlotSpec(spec) + } + + + private fun fillAndColorFactor(): Map { + + val spec = """ +{ + "ggtitle": { "text": "... fill=as_discrete(a), color=as_discrete(b) ..."}, + "data": $data, + "mapping": {"x": "x", "y": "y"}, + "kind": "plot", + "layers": [{ + "geom": "point", + "mapping": {"fill": "a", "color": "b"}, + "data_meta": {"mapping_annotation": [ + {"aes": "fill", "annotation": "as_discrete"}, + {"aes": "color", "annotation": "as_discrete"} + ]}, + "shape": 21, + "size": 9 + }] +} """.trimIndent() + return parsePlotSpec(spec) + } + +} \ No newline at end of file diff --git a/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/BarPlot.kt b/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/BarPlot.kt index 42da8db8c1e..f06e3c01120 100644 --- a/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/BarPlot.kt +++ b/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/BarPlot.kt @@ -11,7 +11,7 @@ import jetbrains.datalore.plotDemo.model.PlotConfigDemoBase open class BarPlot : PlotConfigDemoBase() { fun plotSpecList(): List> { return listOf( - basic(), + //basic(), fancy() ) } diff --git a/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Factor.kt b/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Factor.kt deleted file mode 100644 index 78d4bfb79a9..00000000000 --- a/plot-demo/src/commonMain/kotlin/jetbrains/datalore/plotDemo/model/plotConfig/Factor.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2020. 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.model.PlotConfigDemoBase - -class Factor : PlotConfigDemoBase() { - fun plotSpecList(): List> { - return listOf( - //dataInMapping() - fillFactor() - ,fillAndColorFactor() - ,fillFactorWithScaleColor() - ) - } - - private val data = """{"x": [0, 5, 10], "y": [0, 5, 10], "a": [1, 2, 4], "b": [5, 6, 7]}""" - private fun dataInMapping(): Map { - val spec = """ -{ - "ggtitle": { "text": "geom_point(aes(x=[0, 5, 10], y=[0, 5, 10], color=factor([1, 2, 4]))"}, - "kind": "plot", - "layers": [{ - "geom": "point", - "mapping": {"x": [0, 5, 10], "y": [0, 5, 10], "color": [1, 2, 4]}, - "data_meta": {"series_annotation": [{"variable": [1, 2, 4], "annotation": "discrete"}]}, - "shape": 21, - "size": 9 - }] -} """.trimIndent() - return parsePlotSpec(spec) - } - - private fun fillFactor(): Map { - - val spec = """ -{ - "ggtitle": { "text": "... fill=factor('a') ..."}, - "data": $data, - "mapping": {"x": "x", "y": "y"}, - "kind": "plot", - "layers": [{ - "geom": "point", - "mapping": {"fill": "a", "color": "b"}, - "data_meta": {"series_annotation": [{"variable": "a", "annotation": "discrete"}]}, - "shape": 21, - "size": 9 - }] -} """.trimIndent() - return parsePlotSpec(spec) - } - - - private fun fillFactorWithScaleColor(): Map { - - val spec = """ -{ - "ggtitle": { "text": "... fill=factor('a'), scale_color_discrete() ..."}, - "data": $data, - "mapping": {"x": "x", "y": "y"}, - "kind": "plot", - "scales": [{"aesthetic": "color", "discrete": True}], - "layers": [{ - "geom": "point", - "mapping": {"fill": "a", "color": "b"}, - "data_meta": {"series_annotation": [{"variable": "a", "annotation": "discrete"}]}, - "shape": 21, - "size": 9 - }] -} """.trimIndent() - return parsePlotSpec(spec) - } - - - private fun fillAndColorFactor(): Map { - - val spec = """ -{ - "ggtitle": { "text": "... fill=factor('a'), color=factor('b') ..."}, - "data": $data, - "mapping": {"x": "x", "y": "y"}, - "kind": "plot", - "layers": [{ - "geom": "point", - "mapping": {"fill": "a", "color": "b"}, - "data_meta": {"series_annotation": [ - {"variable": "a", "annotation": "discrete"}, - {"variable": "b", "annotation": "discrete"} - ]}, - "shape": 21, - "size": 9 - }] -} """.trimIndent() - return parsePlotSpec(spec) - } - -} \ No newline at end of file diff --git a/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/FactorJfx.kt b/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/AsDiscreteJfx.kt similarity index 84% rename from plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/FactorJfx.kt rename to plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/AsDiscreteJfx.kt index 6c3eb73cd4b..15e60e4ad0b 100644 --- a/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/FactorJfx.kt +++ b/plot-demo/src/jvmJfxMain/kotlin/plotDemo/plotConfig/AsDiscreteJfx.kt @@ -7,18 +7,18 @@ package jetbrains.datalore.plotDemo.plotConfig import jetbrains.datalore.base.geometry.DoubleVector import jetbrains.datalore.plot.builder.presentation.Style -import jetbrains.datalore.plotDemo.model.plotConfig.Factor +import jetbrains.datalore.plotDemo.model.plotConfig.AsDiscrete import jetbrains.datalore.vis.demoUtils.SceneMapperDemoFactory -object FactorJfx { +object AsDiscreteJfx { @JvmStatic fun main(args: Array) { - with(Factor()) { + with(AsDiscrete()) { @Suppress("UNCHECKED_CAST") val plotSpecList = plotSpecList() as List> @Suppress("SpellCheckingInspection") PlotConfigDemoUtil.show( - "Factor", + "as_discrete", plotSpecList, SceneMapperDemoFactory(Style.JFX_PLOT_STYLESHEET), DoubleVector(600.0, 300.0) diff --git a/python-package/lets_plot/mapping.py b/python-package/lets_plot/mapping.py index 91bc0bc4e13..f8912b97ad2 100644 --- a/python-package/lets_plot/mapping.py +++ b/python-package/lets_plot/mapping.py @@ -2,25 +2,25 @@ # Use of this source code is governed by the MIT license that can be found in the LICENSE file. -class VariableMeta: - def __init__(self, name, kind): - if name is None: - raise ValueError("name can't be none") +class MappingMeta: + def __init__(self, variable, annotation): + if variable is None: + raise ValueError("variable can't be none") - if kind is None: - raise ValueError("kind can't be none") + if annotation is None: + raise ValueError("annotation can't be none") - self.name = name - self.kind = kind + self.variable = variable + self.annotation = annotation -def factor(var_name): +def as_discrete(variable): """ Marks a numeric variable as categorical. Parameters ---------- - var_name : string + variable : string The name of the variable Returns @@ -41,9 +41,9 @@ def factor(var_name): >>> 'y': [0, 5, 10, 15], >>> 'a': [1, 2, 3, 2] >>> } - >>> ggplot(df, aes(x='x', y='y')) + geom_point(aes(color=pm.factor('a')), size=9) + >>> ggplot(df, aes(x='x', y='y')) + geom_point(aes(color=pm.as_discrete('a')), size=9) """ - if isinstance(var_name, str): - return VariableMeta(var_name, 'discrete') - # aes(x=factor([1, 2, 3])) - pass as is - return var_name + if isinstance(variable, str): + return MappingMeta(variable, 'as_discrete') + # aes(x=as_discrete([1, 2, 3])) - pass as is + return variable diff --git a/python-package/lets_plot/plot/util.py b/python-package/lets_plot/plot/util.py index b915eff9549..635f3e97945 100644 --- a/python-package/lets_plot/plot/util.py +++ b/python-package/lets_plot/plot/util.py @@ -5,7 +5,7 @@ from collections import Iterable from typing import Any, Tuple -from lets_plot.mapping import VariableMeta +from lets_plot.mapping import MappingMeta from lets_plot.plot.core import aes @@ -35,21 +35,21 @@ def as_annotated_data(raw_data: Any, raw_mapping: dict) -> Tuple: # mapping mapping = {} - var_meta = [] + mapping_meta = [] if raw_mapping is not None: - for key, variable in raw_mapping.as_dict().items(): - if key == 'name': # ignore FeatureSpec.name property + for aesthetic, variable in raw_mapping.as_dict().items(): + if aesthetic == 'name': # ignore FeatureSpec.name property continue - if isinstance(variable, VariableMeta): - mapping[key] = variable.name - var_meta.append({ 'variable': variable.name, 'annotation': variable.kind }) + if isinstance(variable, MappingMeta): + mapping[aesthetic] = variable.variable + mapping_meta.append({ 'aes': aesthetic, 'annotation': variable.annotation}) else: - mapping[key] = variable + mapping[aesthetic] = variable - if len(var_meta) > 0: - data_meta.update({ 'series_annotation': var_meta }) + if len(mapping_meta) > 0: + data_meta.update({ 'mapping_annotation': mapping_meta }) return data, aes(**mapping), {'data_meta': data_meta }