diff --git a/.circleci/config.yml b/.circleci/config.yml index 78b0681b2..37f3b162b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,9 +25,10 @@ jobs: python -m venv venv || virtualenv venv . venv/bin/activate pip install -r dev-requirements.txt --quiet - git clone --depth 1 git@github.com:plotly/dash.git dash-main - pip install -e ./dash-main[dev,testing] --quiet - cd dash-main/dash-renderer && npm ci && npm run build && pip install -e . && cd ./../.. + git clone --depth 1 -b dcg-template git@github.com:plotly/dash.git main + cd ./main/\@plotly/dash-generate-components; npm ci; cd ../../.. + pip install -e ./main[dev,testing] --quiet + cd main/dash-renderer && npm ci && npm run build && pip install -e . && cd ./../.. - run: name: Build @@ -35,8 +36,9 @@ jobs: . venv/bin/activate npm run private::build:js-test npm run private::build:backends - python setup.py sdist - cd dist + ls -laR .dcg/dist/py + cat .dcg/dist/py/dash_table/metadata.json + cd .dcg/dist/py; python setup.py sdist; cd dist find . -name "*.gz" | xargs pip install --no-cache-dir --ignore-installed && cd .. - run: @@ -107,7 +109,9 @@ jobs: python -m venv venv || virtualenv venv . venv/bin/activate pip install -r dev-requirements.txt --quiet - pip install --progress-bar off -e git+https://github.com/plotly/dash.git@dev#egg=dash[dev,testing] + pip install --progress-bar off -e git+https://github.com/plotly/dash.git@dcg-template#egg=dash[dev,testing] + git clone --depth 1 -b dcg-template git@github.com:plotly/dash.git main + cd ./main/\@plotly/dash-generate-components; npm ci; cd ../../.. - run: name: Run tests @@ -213,15 +217,17 @@ jobs: name: Install dependencies (dash) command: | . venv/bin/activate - git clone --depth 1 git@github.com:plotly/dash.git dash-main - pip install -e ./dash-main[dev,testing] --quiet - cd dash-main/dash-renderer && npm ci && npm run build && pip install -e . && cd ../.. + git clone --depth 1 -b dcg-template git@github.com:plotly/dash.git main + cd ./main/\@plotly/dash-generate-components; npm ci; cd ../../.. + pip install -e ./main[dev,testing] --quiet + cd main/dash-renderer && npm ci && npm run build && pip install -e . && cd ../.. - run: name: Install test requirements command: | . venv/bin/activate npm run build + cd .dcg/dist/py python setup.py sdist cd dist find . -name "*.gz" | xargs pip install --no-cache-dir --ignore-installed && cd .. diff --git a/.dcg/R/dashDataTable.R b/.dcg/R/dashDataTable.R new file mode 100644 index 000000000..4c288f18b --- /dev/null +++ b/.dcg/R/dashDataTable.R @@ -0,0 +1,70 @@ +# For comprehensive documentation of this package's features, +# please consult https://dashr.plot.ly/datatable +# +# A package vignette is currently in development and will +# provide many of the same examples currently available online +# in an offline-friendly format. + +# The following if statement is not required to run this +# example locally, but was added at the request of CRAN +# maintainers. +if (interactive() && require(dash)) { + library(dash) + library(dashTable) + + app <- Dash$new() + + # We can easily restrict the number of rows to display at + # once by using style_table: + app$layout( + dashDataTable( + id = "table", + columns = lapply(colnames(iris), + function(colName){ + list( + id = colName, + name = colName + ) + }), + style_table = list( + maxHeight = "250px", + overflowY = "scroll" + ), + data = df_to_list(iris) + ) + ) + + app$run_server() + + app <- Dash$new() + + # We can also make rows and columns selectable/deletable + # by setting a few additional attributes: + app$layout( + dashDataTable( + id = "table", + columns = lapply(colnames(iris), + function(colName){ + list( + id = colName, + name = colName, + deletable = TRUE + ) + }), + style_table = list( + maxHeight = "250px", + overflowY = "scroll" + ), + data = df_to_list(iris), + editable = TRUE, + filter_action = "native", + sort_action = "native", + sort_mode = "multi", + column_selectable = "single", + row_selectable = "multi", + row_deletable = TRUE + ) + ) + + app$run_server() +} \ No newline at end of file diff --git a/.dcg/R/df_to_list.R b/.dcg/R/df_to_list.R new file mode 100644 index 000000000..ac4de8ae7 --- /dev/null +++ b/.dcg/R/df_to_list.R @@ -0,0 +1,8 @@ +df_to_list <- function(df) { + if(!(is.data.frame(df))) + stop("df_to_list requires a data.frame object; please verify that df is of the correct type.") + stats::setNames(lapply(split(df, seq(nrow(df))), + FUN = function (x) { + as.list(x) + }), NULL) +} diff --git a/.dcg/R/df_to_list.Rd b/.dcg/R/df_to_list.Rd new file mode 100644 index 000000000..782bf6524 --- /dev/null +++ b/.dcg/R/df_to_list.Rd @@ -0,0 +1,43 @@ +% Auto-generated: do not edit by hand +\name{df_to_list} + +\alias{df_to_list} + +\title{Convert data.frame objects to list-of-lists format} + +\description{ +Convert a \code{\link{data.frame}} to a list of lists for compatibility with \code{\link{dashDataTable}}. The function will return a nested list object in which the sublists contain named elements of varying type; the names correspond to the column names in the original \code{\link{data.frame}}. +} + +\usage{ +df_to_list(df) +} + +\arguments{ +\item{df}{A \code{data.frame} object, which will be transformed into a list of lists. Each row will become a single named list, in which the elements are named as the columns from which they were extracted.} +} + +\value{ +a \code{list} object, in which the sublists are named elements of varying type; the names correspond to the column names in the \code{data.frame} specified by \code{df}. +} + +\examples{ +# first, create data frame +df <- read.csv(url( + 'https://raw.githubusercontent.com/plotly/datasets/master/solar.csv' + ), + check.names=FALSE, + stringsAsFactors=FALSE +) + +# then convert to list-of-lists format for use in dashTable +# the following snippet below will print as JSON +# see the help for dashDataTable to see an actual app example +dashDataTable( + id = 'table', + columns = lapply(colnames(df), function(x) { + list(name = x, id = x) + }), + data = df_to_list(df) +) +} diff --git a/.dcg/R/pkg_help_description.txt b/.dcg/R/pkg_help_description.txt new file mode 100644 index 000000000..02bc1198b --- /dev/null +++ b/.dcg/R/pkg_help_description.txt @@ -0,0 +1,7 @@ +An interactive table component designed for editing and exploring +large datasets, 'dashDataTable' is rendered with standard, semantic HTML + markup, which makes it accessible, responsive, and easy +to style. This component was written from scratch in 'React.js' +specifically for the 'Dash' community. Its API was designed to be +ergonomic and its behaviour is completely customizable through its +properties. \ No newline at end of file diff --git a/.dcg/config.yaml b/.dcg/config.yaml new file mode 100644 index 000000000..828ef8e83 --- /dev/null +++ b/.dcg/config.yaml @@ -0,0 +1,68 @@ +componentPaths: + - src/dash-table/dash/DataTable.js + +recipes: + - py + - R + +dist: + - source: dash_table/bundle.js + target: bundle.js + external: true + - source: dash_table/bundle.js.map + target: bundle.js.map + dynamic: true + external: true + - source: dash_table/async-export.js + target: async-export.js + async: true + external: true + - source: dash_table/async-export.js.map + target: async-export.js.map + dynamic: true + external: true + - source: dash_table/async-highlight.js + target: async-highlight.js + async: true + external: true + - source: dash_table/async-highlight.js.map + target: async-highlight.js.map + dynamic: true + external: true + - source: dash_table/async-table.js + target: async-table.js + async: true + external: true + - source: dash_table/async-table.js.map + target: async-table.js.map + dynamic: true + external: true + +vars: + py: + dist: + - source: Format.py + target: Format.py + - source: FormatTemplate.py + target: FormatTemplate.py + + R: + prefix: dash + depends: + imports: + suggests: + - dash + + pkg_authors: c(person("Chris", "Parmer", email = "chris@plotly.com", role = c("aut")), person("Ryan Patrick", "Kyle", email = "ryan@plotly.com", role = c("cre"), comment = c(ORCID = "0000-0002-4958-2844")), person(family = "Plotly Technologies, Inc.", role = "cph")) + pkg_help_description: ${js.core.readConfigFile('pkg_help_description.txt')} + pkg_help_title: Core Interactive Table Component for 'Dash' + pkg_copyright: Plotly Technologies, Inc. + examples: + DataTable: + dontrun: false + code: ${js.core.readConfigFile('dashDataTable.R')} + dist: + - source: df_to_list.Rd + target: man/df_to_list.Rd + - source: df_to_list.R + target: R/df_to_list.R \ No newline at end of file diff --git a/.dcg/py/Format.py b/.dcg/py/Format.py new file mode 100644 index 000000000..8dd12638c --- /dev/null +++ b/.dcg/py/Format.py @@ -0,0 +1,287 @@ +import collections + + +def get_named_tuple(name, dict): + return collections.namedtuple(name, dict.keys())(*dict.values()) + + +Align = get_named_tuple( + "align", + {"default": "", "left": "<", "right": ">", "center": "^", "right_sign": "="}, +) + +Group = get_named_tuple("group", {"no": "", "yes": ","}) + +Padding = get_named_tuple("padding", {"no": "", "yes": "0"}) + +Prefix = get_named_tuple( + "prefix", + { + "yocto": 10 ** -24, + "zepto": 10 ** -21, + "atto": 10 ** -18, + "femto": 10 ** -15, + "pico": 10 ** -12, + "nano": 10 ** -9, + "micro": 10 ** -6, + "milli": 10 ** -3, + "none": None, + "kilo": 10 ** 3, + "mega": 10 ** 6, + "giga": 10 ** 9, + "tera": 10 ** 12, + "peta": 10 ** 15, + "exa": 10 ** 18, + "zetta": 10 ** 21, + "yotta": 10 ** 24, + }, +) + +Scheme = get_named_tuple( + "scheme", + { + "default": "", + "decimal": "r", + "decimal_integer": "d", + "decimal_or_exponent": "g", + "decimal_si_prefix": "s", + "exponent": "e", + "fixed": "f", + "percentage": "%", + "percentage_rounded": "p", + "binary": "b", + "octal": "o", + "lower_case_hex": "x", + "upper_case_hex": "X", + "unicode": "c", + }, +) + +Sign = get_named_tuple( + "sign", + {"default": "", "negative": "-", "positive": "+", "parantheses": "(", "space": " "}, +) + +Symbol = get_named_tuple( + "symbol", {"no": "", "yes": "$", "binary": "#b", "octal": "#o", "hex": "#x"} +) + +Trim = get_named_tuple("trim", {"no": "", "yes": "~"}) + + +class Format: + def __init__(self, **kwargs): + self._locale = {} + self._nully = "" + self._prefix = Prefix.none + self._specifier = { + "align": Align.default, + "fill": "", + "group": Group.no, + "width": "", + "padding": Padding.no, + "precision": "", + "sign": Sign.default, + "symbol": Symbol.no, + "trim": Trim.no, + "type": Scheme.default, + } + + valid_methods = [ + m for m in dir(self.__class__) if m[0] != "_" and m != "to_plotly_json" + ] + + for kw, val in kwargs.items(): + if kw not in valid_methods: + raise TypeError( + "{0} is not a format method. Expected one of".format(kw), + str(list(valid_methods)), + ) + + getattr(self, kw)(val) + + def _validate_char(self, value): + self._validate_string(value) + + if len(value) != 1: + raise ValueError("expected value to a string of length one") + + def _validate_non_negative_integer_or_none(self, value): + if value is None: + return + + if not isinstance(value, int): + raise TypeError("expected value to be an integer") + + if value < 0: + raise ValueError("expected value to be non-negative", str(value)) + + def _validate_named(self, value, named_values): + if value not in named_values: + raise TypeError("expected value to be one of", str(list(named_values))) + + def _validate_string(self, value): + if not isinstance(value, (str, u"".__class__)): + raise TypeError("expected value to be a string") + + # Specifier + def align(self, value): + self._validate_named(value, Align) + + self._specifier["align"] = value + return self + + def fill(self, value): + self._validate_char(value) + + self._specifier["fill"] = value + return self + + def group(self, value): + if isinstance(value, bool): + value = Group.yes if value else Group.no + + self._validate_named(value, Group) + + self._specifier["group"] = value + return self + + def padding(self, value): + if isinstance(value, bool): + value = Padding.yes if value else Padding.no + + self._validate_named(value, Padding) + + self._specifier["padding"] = value + return self + + def padding_width(self, value): + self._validate_non_negative_integer_or_none(value) + + self._specifier["width"] = value if value is not None else "" + return self + + def precision(self, value): + self._validate_non_negative_integer_or_none(value) + + self._specifier["precision"] = ".{0}".format(value) if value is not None else "" + return self + + def scheme(self, value): + self._validate_named(value, Scheme) + + self._specifier["type"] = value + return self + + def sign(self, value): + self._validate_named(value, Sign) + + self._specifier["sign"] = value + return self + + def symbol(self, value): + self._validate_named(value, Symbol) + + self._specifier["symbol"] = value + return self + + def trim(self, value): + if isinstance(value, bool): + value = Trim.yes if value else Trim.no + + self._validate_named(value, Trim) + + self._specifier["trim"] = value + return self + + # Locale + def symbol_prefix(self, value): + self._validate_string(value) + + if "symbol" not in self._locale: + self._locale["symbol"] = [value, ""] + else: + self._locale["symbol"][0] = value + + return self + + def symbol_suffix(self, value): + self._validate_string(value) + + if "symbol" not in self._locale: + self._locale["symbol"] = ["", value] + else: + self._locale["symbol"][1] = value + + return self + + def decimal_delimiter(self, value): + self._validate_char(value) + + self._locale["decimal"] = value + return self + + def group_delimiter(self, value): + self._validate_char(value) + + self._locale["group"] = value + return self + + def groups(self, groups): + groups = ( + groups + if isinstance(groups, list) + else [groups] + if isinstance(groups, int) + else None + ) + + if not isinstance(groups, list): + raise TypeError("expected groups to be an integer or a list of integers") + if len(groups) == 0: + raise ValueError( + "expected groups to be an integer or a list of " "one or more integers" + ) + + for group in groups: + if not isinstance(group, int): + raise TypeError("expected entry to be an integer") + + if group <= 0: + raise ValueError("expected entry to be a non-negative integer") + + self._locale["grouping"] = groups + return self + + # Nully + def nully(self, value): + self._nully = value + return self + + # Prefix + def si_prefix(self, value): + self._validate_named(value, Prefix) + + self._prefix = value + return self + + def to_plotly_json(self): + f = {} + f["locale"] = self._locale.copy() + f["nully"] = self._nully + f["prefix"] = self._prefix + aligned = self._specifier["align"] != Align.default + f["specifier"] = "{}{}{}{}{}{}{}{}{}{}".format( + self._specifier["fill"] if aligned else "", + self._specifier["align"], + self._specifier["sign"], + self._specifier["symbol"], + self._specifier["padding"], + self._specifier["width"], + self._specifier["group"], + self._specifier["precision"], + self._specifier["trim"], + self._specifier["type"], + ) + + return f diff --git a/.dcg/py/FormatTemplate.py b/.dcg/py/FormatTemplate.py new file mode 100644 index 000000000..9c2688ca8 --- /dev/null +++ b/.dcg/py/FormatTemplate.py @@ -0,0 +1,19 @@ +from .Format import Format, Group, Scheme, Sign, Symbol + + +def money(decimals, sign=Sign.default): + return Format( + group=Group.yes, + precision=decimals, + scheme=Scheme.fixed, + sign=sign, + symbol=Symbol.yes, + ) + + +def percentage(decimals, rounded=False): + if not isinstance(rounded, bool): + raise TypeError("expected rounded to be a boolean") + + rounded = Scheme.percentage_rounded if rounded else Scheme.percentage + return Format(scheme=rounded, precision=decimals) diff --git a/package.json b/package.json index bab3cfb20..6475c3afd 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,14 @@ "private::build:js-test": "run-s \"private::build -- --mode development --config webpack.test.config.js\"", "private::build:js-test-standalone": "run-s \"private::build -- --mode development --config webpack.test.standalone.config.js\"", "private::build:js-test-watch": "run-s \"private::build -- --mode development --config webpack.test.config.js --watch\"", - "private::build:backends": "dash-generate-components src/dash-table/dash/DataTable.js dash_table -p package-info.json && cp dash_table_base/** dash_table/ && dash-generate-components src/dash-table/dash/DataTable.js dash_table -p package-info.json --r-prefix 'dash' --r-suggests 'dash' --jl-prefix 'dash'", + "private::build:backends": "node ./main/\\@plotly/dash-generate-components/src/run.js -c .dcg/config.yaml", "private::format.ts": "npm run private::lint.ts -- --fix", "private::format.prettier": "prettier --config .prettierrc --write \"src/**/*.{js,ts,tsx}\"", - "private::format.black": "black --exclude dash_table .", + "private::format.black": "black --exclude dash_table .dcg .", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint.ts": "tslint --project tsconfig.json --config tslint.json", - "private::lint.flake": "flake8 --exclude=dash_table,node_modules,venv", - "private::lint.black": "black --check --exclude dash_table .", + "private::lint.flake": "flake8 --exclude=dash_table,node_modules,venv,.dcg", + "private::lint.black": "black --check --exclude dash_table .dcg .", "private::lint.prettier": "prettier --config .prettierrc \"src/**/*.{js,ts,tsx}\" --list-different", "private::wait_js": "wait-on http://localhost:8080", "private::opentests": "cypress open",