From 1ea3672c5f98772f47861501bdc83ef3f98d7866 Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Wed, 14 Nov 2018 17:09:52 -0800
Subject: [PATCH] wip, refactored param definitions

---
 src/apps/canvas/component/viewport.tsx        |   2 +-
 src/apps/schema-generator/util/generate.ts    |   2 +-
 src/mol-app/component/parameter/number.tsx    |   2 +-
 src/mol-app/component/parameter/range.tsx     |  44 --------
 src/mol-app/component/parameters.tsx          |   3 -
 .../geometry/direct-volume/direct-volume.ts   |   6 +-
 src/mol-geo/geometry/geometry.ts              |  16 +--
 src/mol-geo/geometry/lines/lines.ts           |   2 +-
 src/mol-geo/geometry/mesh/mesh.ts             |   6 +-
 src/mol-geo/geometry/points/points.ts         |   6 +-
 .../structure/unit/gaussian-density.ts        |  10 +-
 src/mol-plugin/state/actions/basic.ts         |   2 +-
 src/mol-plugin/state/transforms/data.ts       |   6 +-
 src/mol-plugin/state/transforms/model.ts      |   6 +-
 src/mol-plugin/state/transforms/visuals.ts    |   8 +-
 src/mol-plugin/ui/controls/parameters.tsx     |   1 -
 src/mol-repr/structure/complex-visual.ts      |   4 +-
 .../representation/ball-and-stick.ts          |  10 +-
 .../structure/representation/cartoon.ts       |   8 +-
 .../structure/units-representation.ts         |   2 +-
 .../visual/carbohydrate-link-cylinder.ts      |   2 +-
 .../visual/carbohydrate-symbol-mesh.ts        |   2 +-
 .../structure/visual/element-point.ts         |   2 +-
 .../structure/visual/element-sphere.ts        |   6 +-
 .../visual/gaussian-density-point.ts          |   2 +-
 .../visual/gaussian-surface-wireframe.ts      |   2 +-
 .../visual/intra-unit-link-cylinder.ts        |   2 +-
 .../structure/visual/nucleotide-block-mesh.ts |   2 +-
 .../visual/polymer-backbone-cylinder.ts       |   2 +-
 .../visual/polymer-direction-wedge.ts         |   2 +-
 .../structure/visual/polymer-gap-cylinder.ts  |   4 +-
 .../structure/visual/polymer-trace-mesh.ts    |  10 +-
 src/mol-repr/structure/visual/util/link.ts    |   6 +-
 src/mol-repr/volume/isosurface-mesh.ts        |   2 +-
 src/mol-repr/volume/representation.ts         |   2 +-
 src/mol-theme/color/chain-id.ts               |   2 +-
 src/mol-theme/color/cross-link.ts             |   4 +-
 src/mol-theme/color/element-index.ts          |   2 +-
 src/mol-theme/color/polymer-index.ts          |   2 +-
 src/mol-theme/color/sequence-id.ts            |   2 +-
 src/mol-theme/color/uniform.ts                |   2 +-
 src/mol-theme/color/unit-index.ts             |   2 +-
 src/mol-theme/size/uniform.ts                 |   2 +-
 src/mol-util/index.ts                         |   5 -
 src/mol-util/param-definition.ts              | 102 +++++++++++-------
 src/mol-util/string.ts                        |  28 +++++
 46 files changed, 173 insertions(+), 174 deletions(-)
 delete mode 100644 src/mol-app/component/parameter/range.tsx
 create mode 100644 src/mol-util/string.ts

diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx
index c6c6f547b..35c108815 100644
--- a/src/apps/canvas/component/viewport.tsx
+++ b/src/apps/canvas/component/viewport.tsx
@@ -28,7 +28,7 @@ interface ViewportState {
     backgroundColor: Color
 }
 
-const BackgroundColorParam = PD.Color('Background Color', '', Color(0x000000))
+const BackgroundColorParam = PD.Color(Color(0x000000), { label: 'Background Color' })
 
 export class Viewport extends React.Component<ViewportProps, ViewportState> {
     private container: HTMLDivElement | null = null;
diff --git a/src/apps/schema-generator/util/generate.ts b/src/apps/schema-generator/util/generate.ts
index ecf33b495..439867d65 100644
--- a/src/apps/schema-generator/util/generate.ts
+++ b/src/apps/schema-generator/util/generate.ts
@@ -5,7 +5,7 @@
  */
 
 import { Database, Filter, Column } from './schema'
-import { indentString } from 'mol-util';
+import { indentString } from 'mol-util/string';
 
 function header (name: string, info: string, importDatabasePath = 'mol-data/db') {
     return `/**
diff --git a/src/mol-app/component/parameter/number.tsx b/src/mol-app/component/parameter/number.tsx
index fee73d651..3ee613df7 100644
--- a/src/mol-app/component/parameter/number.tsx
+++ b/src/mol-app/component/parameter/number.tsx
@@ -23,7 +23,7 @@ export class NumberParamComponent extends React.Component<NumberParamComponentPr
     }
 
     onChange(valueStr: string) {
-        const value = Number.isInteger(this.props.param.step) ? parseInt(valueStr) : parseFloat(valueStr)
+        const value = this.props.param.step && Number.isInteger(this.props.param.step) ? parseInt(valueStr) : parseFloat(valueStr)
         this.setState({ value })
         this.props.onChange(value)
     }
diff --git a/src/mol-app/component/parameter/range.tsx b/src/mol-app/component/parameter/range.tsx
deleted file mode 100644
index 45b9cf639..000000000
--- a/src/mol-app/component/parameter/range.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-
-export interface RangeParamComponentProps {
-    param: PD.Range
-    value: number
-    onChange(v: number): void
-}
-
-export interface RangeParamComponentState {
-    value: number
-}
-
-export class RangeParamComponent extends React.Component<RangeParamComponentProps, RangeParamComponentState> {
-    state = {
-        value: this.props.value
-    }
-
-    onChange(valueStr: string) {
-        const value = Number.isInteger(this.props.param.step) ? parseInt(valueStr) : parseFloat(valueStr)
-        this.setState({ value })
-        this.props.onChange(value)
-    }
-
-    render() {
-        return <div>
-            <span>{this.props.param.label} </span>
-            <input type='range'
-                value={this.state.value}
-                min={this.props.param.min}
-                max={this.props.param.max}
-                step={this.props.param.step}
-                onChange={e => this.onChange(e.currentTarget.value)}
-            >
-            </input>
-        </div>;
-    }
-}
\ No newline at end of file
diff --git a/src/mol-app/component/parameters.tsx b/src/mol-app/component/parameters.tsx
index c5c0c673c..e493dd358 100644
--- a/src/mol-app/component/parameters.tsx
+++ b/src/mol-app/component/parameters.tsx
@@ -8,7 +8,6 @@ import * as React from 'react'
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { BooleanParamComponent } from './parameter/boolean';
 import { NumberParamComponent } from './parameter/number';
-import { RangeParamComponent } from './parameter/range';
 import { SelectParamComponent } from './parameter/select';
 import { MultiSelectParamComponent } from './parameter/multi-select';
 import { TextParamComponent } from './parameter/text';
@@ -28,8 +27,6 @@ function getParamComponent<P extends PD.Any>(p: PD.Any, value: P['defaultValue']
             return <BooleanParamComponent param={p} value={value} onChange={onChange} />
         case 'number':
             return <NumberParamComponent param={p} value={value} onChange={onChange} />
-        case 'range':
-            return <RangeParamComponent param={p} value={value} onChange={onChange} />
         case 'select':
             return <SelectParamComponent param={p} value={value} onChange={onChange} />
         case 'multi-select':
diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts
index 47527c3d2..af49e2124 100644
--- a/src/mol-geo/geometry/direct-volume/direct-volume.ts
+++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts
@@ -70,9 +70,9 @@ export namespace DirectVolume {
 
     export const Params = {
         ...Geometry.Params,
-        isoValue: PD.Range('Iso Value', '', 0.22, -1, 1, 0.01),
-        renderMode: PD.Select('Render Mode', '', 'isosurface', RenderModeOptions),
-        controlPoints: PD.Text('Control Points', '', '0.19:0.1, 0.2:0.5, 0.21:0.1, 0.4:0.3'),
+        isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
+        renderMode: PD.Select('isosurface', RenderModeOptions),
+        controlPoints: PD.Text('0.19:0.1, 0.2:0.5, 0.21:0.1, 0.4:0.3'),
     }
     export type Params = typeof Params
 
diff --git a/src/mol-geo/geometry/geometry.ts b/src/mol-geo/geometry/geometry.ts
index 79d665d11..598e50977 100644
--- a/src/mol-geo/geometry/geometry.ts
+++ b/src/mol-geo/geometry/geometry.ts
@@ -61,16 +61,16 @@ export namespace Geometry {
     //
 
     export const Params = {
-        alpha: PD.Range('Opacity', '', 1, 0, 1, 0.01),
-        depthMask: PD.Boolean('Depth Mask', '', true),
-        useFog: PD.Boolean('Use Fog', '', false),
-        highlightColor: PD.Color('Highlight Color', '', Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
-        selectColor: PD.Color('Select Color', '', Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
+        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }),
+        depthMask: PD.Boolean(true),
+        useFog: PD.Boolean(false),
+        highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
+        selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
 
-        quality: PD.Select<VisualQuality>('Quality', '', 'auto', VisualQualityOptions),
+        quality: PD.Select<VisualQuality>('auto', VisualQualityOptions),
 
-        colorTheme: PD.Select<BuiltInColorThemeName>('Color Name', '', 'uniform', BuiltInColorThemeOptions),
-        sizeTheme: PD.Select<BuiltInSizeThemeName>('Size Name', '', 'uniform', BuiltInSizeThemeOptions),
+        colorTheme: PD.Select<BuiltInColorThemeName>('uniform', BuiltInColorThemeOptions),
+        sizeTheme: PD.Select<BuiltInSizeThemeName>('uniform', BuiltInSizeThemeOptions),
     }
     export type Params = typeof Params
 
diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts
index 0cf5bfef2..5e2764ce4 100644
--- a/src/mol-geo/geometry/lines/lines.ts
+++ b/src/mol-geo/geometry/lines/lines.ts
@@ -95,7 +95,7 @@ export namespace Lines {
 
     export const Params = {
         ...Geometry.Params,
-        lineSizeAttenuation: PD.Boolean('Line Size Attenuation', '', false),
+        lineSizeAttenuation: PD.Boolean(false),
     }
     export type Params = typeof Params
 
diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts
index 0f7910745..02349efae 100644
--- a/src/mol-geo/geometry/mesh/mesh.ts
+++ b/src/mol-geo/geometry/mesh/mesh.ts
@@ -341,9 +341,9 @@ export namespace Mesh {
 
     export const Params = {
         ...Geometry.Params,
-        doubleSided: PD.Boolean('Double Sided', '', false),
-        flipSided: PD.Boolean('Flip Sided', '', false),
-        flatShaded: PD.Boolean('Flat Shaded', '', false),
+        doubleSided: PD.Boolean(false),
+        flipSided: PD.Boolean(false),
+        flatShaded: PD.Boolean(false),
     }
     export type Params = typeof Params
 
diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts
index 2b245338d..4d443427c 100644
--- a/src/mol-geo/geometry/points/points.ts
+++ b/src/mol-geo/geometry/points/points.ts
@@ -57,9 +57,9 @@ export namespace Points {
 
     export const Params = {
         ...Geometry.Params,
-        pointSizeAttenuation: PD.Boolean('Point Size Attenuation', '', false),
-        pointFilledCircle: PD.Boolean('Point Filled Circle', '', false),
-        pointEdgeBleach: PD.Numeric('Point Edge Bleach', '', 0.2, 0, 1, 0.05),
+        pointSizeAttenuation: PD.Boolean(false),
+        pointFilledCircle: PD.Boolean(false),
+        pointEdgeBleach: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),
     }
     export type Params = typeof Params
 
diff --git a/src/mol-model/structure/structure/unit/gaussian-density.ts b/src/mol-model/structure/structure/unit/gaussian-density.ts
index 0acfa52fd..328bf2c12 100644
--- a/src/mol-model/structure/structure/unit/gaussian-density.ts
+++ b/src/mol-model/structure/structure/unit/gaussian-density.ts
@@ -15,11 +15,11 @@ import { WebGLContext } from 'mol-gl/webgl/context';
 import { PhysicalSizeTheme } from 'mol-theme/size/physical';
 
 export const GaussianDensityParams = {
-    resolution: PD.Numeric('Resolution', '', 1, 0.1, 10, 0.1),
-    radiusOffset: PD.Numeric('Radius Offset', '', 0, 0, 10, 0.1),
-    smoothness: PD.Numeric('Smoothness', '', 1.5, 0.5, 2.5, 0.1),
-    useGpu: PD.Boolean('Use GPU', '', true),
-    ignoreCache: PD.Boolean('Ignore Cache', '', false),
+    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
+    smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
+    useGpu: PD.Boolean(true),
+    ignoreCache: PD.Boolean(false),
 }
 export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams)
 export type GaussianDensityProps = typeof DefaultGaussianDensityProps
diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts
index 85c3814a8..7ccbf3c40 100644
--- a/src/mol-plugin/state/actions/basic.ts
+++ b/src/mol-plugin/state/actions/basic.ts
@@ -20,7 +20,7 @@ export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root
     params: {
         default: () => ({ id: '1grm' }),
         definition: () => ({
-            id: PD.Text('PDB id', '', '1grm'),
+            id: PD.Text('1grm', { label: 'PDB id' }),
         }),
         // validate: p => !p.id || !p.id.trim() ? [['Enter id.', 'id']] : void 0
     },
diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts
index 3c7c043fd..78499dcd9 100644
--- a/src/mol-plugin/state/transforms/data.ts
+++ b/src/mol-plugin/state/transforms/data.ts
@@ -27,9 +27,9 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
             url: 'https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif'
         }),
         definition: () => ({
-            url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''),
-            label: PD.Text('Label', '', ''),
-            isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false)
+            url: PD.Text('', { description: 'Resource URL. Must be the same domain or support CORS.' }),
+            label: PD.Text(''),
+            isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' })
         }),
         // validate: p => !p.url || !p.url.trim() ? [['Enter url.', 'url']] : void 0
     },
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index 2ad0eed9f..220220e34 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -29,7 +29,7 @@ const ParseTrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Mol
             const { blocks } = a.data;
             if (blocks.length === 0) return {};
             return {
-                blockHeader: PD.Select('Header', 'Header of the block to parse', blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]))
+                blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })
             };
         }
     },
@@ -58,7 +58,7 @@ const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajec
     to: [SO.Molecule.Model],
     params: {
         default: () => ({ modelIndex: 0 }),
-        definition: a => ({ modelIndex: PD.Range('Model Index', 'Model Index', 0, 0, Math.max(0, a.data.length - 1), 1) })
+        definition: a => ({ modelIndex: PD.Numeric(0, { min: 0, max: Math.max(0, a.data.length - 1), step: 1 }, { description: 'Model Index' }) })
     },
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
@@ -106,7 +106,7 @@ const CreateStructureAssembly = PluginStateTransform.Create<SO.Molecule.Model, S
         definition(a) {
             const model = a.data;
             const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]);
-            return { id: PD.Select('Asm Id', 'Assembly Id', ids.length ? ids[0][0] : '', ids) };
+            return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) };
         }
     },
     apply({ a, params }) {
diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts
index fb24bc2bc..81760fc7e 100644
--- a/src/mol-plugin/state/transforms/visuals.ts
+++ b/src/mol-plugin/state/transforms/visuals.ts
@@ -33,10 +33,14 @@ const CreateStructureRepresentation = PluginStateTransform.Create<SO.Molecule.St
             }
         }),
         definition: (a, ctx: PluginContext) => ({
-            type: PD.Mapped('Type', '',
+            type: PD.Mapped(
                 ctx.structureReprensentation.registry.default.name,
                 ctx.structureReprensentation.registry.types,
-                name => PD.Group('Params', '', ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data)))
+                name => PD.Group(
+                    ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data),
+                    { label: 'Params' }
+                )
+            )
         })
     },
     apply({ a, params }, plugin: PluginContext) {
diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx
index 28807f96b..2ac820fcc 100644
--- a/src/mol-plugin/ui/controls/parameters.tsx
+++ b/src/mol-plugin/ui/controls/parameters.tsx
@@ -41,7 +41,6 @@ function controlFor(param: PD.Any): ValueControl {
     switch (param.type) {
         case 'boolean': return BoolControl;
         case 'number': return NumberControl;
-        case 'range': return NumberControl;
         case 'multi-select': return MultiSelectControl;
         case 'color': return ColorControl;
         case 'select': return SelectControl;
diff --git a/src/mol-repr/structure/complex-visual.ts b/src/mol-repr/structure/complex-visual.ts
index b24737ef3..25823c82f 100644
--- a/src/mol-repr/structure/complex-visual.ts
+++ b/src/mol-repr/structure/complex-visual.ts
@@ -30,7 +30,7 @@ export interface  ComplexVisual<P extends StructureParams> extends Visual<Struct
 
 const ComplexParams = {
     ...StructureParams,
-    unitKinds: PD.MultiSelect<UnitKind>('Unit Kind', '', ['atomic', 'spheres'], UnitKindOptions),
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
 }
 type ComplexParams = typeof ComplexParams
 
@@ -176,7 +176,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
 
 export const ComplexMeshParams = {
     ...StructureMeshParams,
-    unitKinds: PD.MultiSelect<UnitKind>('Unit Kind', '', [ 'atomic', 'spheres' ], UnitKindOptions),
+    unitKinds: PD.MultiSelect<UnitKind>([ 'atomic', 'spheres' ], UnitKindOptions),
 }
 export type ComplexMeshParams = typeof ComplexMeshParams
 
diff --git a/src/mol-repr/structure/representation/ball-and-stick.ts b/src/mol-repr/structure/representation/ball-and-stick.ts
index d411043d0..a5fe7d277 100644
--- a/src/mol-repr/structure/representation/ball-and-stick.ts
+++ b/src/mol-repr/structure/representation/ball-and-stick.ts
@@ -30,11 +30,11 @@ export const BallAndStickParams = {
     ...ElementSphereParams,
     ...IntraUnitLinkParams,
     ...InterUnitLinkParams,
-    unitKinds: PD.MultiSelect<UnitKind>('Unit Kind', '', ['atomic'], UnitKindOptions),
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0.01, 10, 0.01),
-    sizeTheme: PD.Select<BuiltInSizeThemeName>('Size Theme', '', 'uniform', BuiltInSizeThemeOptions),
-    colorTheme: PD.Select<BuiltInColorThemeName>('Color Theme', '', 'polymer-index', BuiltInColorThemeOptions),
-    visuals: PD.MultiSelect<BallAndStickVisualName>('Visuals', '', ['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions),
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
+    sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 10, step: 0.01 }),
+    sizeTheme: PD.Select<BuiltInSizeThemeName>('uniform', BuiltInSizeThemeOptions),
+    colorTheme: PD.Select<BuiltInColorThemeName>('polymer-index', BuiltInColorThemeOptions),
+    visuals: PD.MultiSelect<BallAndStickVisualName>(['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions),
 }
 export type BallAndStickParams = typeof BallAndStickParams
 export function getBallAndStickParams(ctx: ThemeRegistryContext, structure: Structure) {
diff --git a/src/mol-repr/structure/representation/cartoon.ts b/src/mol-repr/structure/representation/cartoon.ts
index 8e5430564..1bdd3534b 100644
--- a/src/mol-repr/structure/representation/cartoon.ts
+++ b/src/mol-repr/structure/representation/cartoon.ts
@@ -31,10 +31,10 @@ export const CartoonParams = {
     ...PolymerGapParams,
     ...NucleotideBlockParams,
     ...PolymerDirectionParams,
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
-    sizeTheme: PD.Select<BuiltInSizeThemeName>('Size Theme', '', 'uniform', BuiltInSizeThemeOptions),
-    colorTheme: PD.Select<BuiltInColorThemeName>('Color Theme', '', 'polymer-index', BuiltInColorThemeOptions),
-    visuals: PD.MultiSelect<CartoonVisualName>('Visuals', '', ['polymer-trace', 'polymer-gap', 'nucleotide-block'], CartoonVisualOptions),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    sizeTheme: PD.Select<BuiltInSizeThemeName>('uniform', BuiltInSizeThemeOptions),
+    colorTheme: PD.Select<BuiltInColorThemeName>('polymer-index', BuiltInColorThemeOptions),
+    visuals: PD.MultiSelect<CartoonVisualName>(['polymer-trace', 'polymer-gap', 'nucleotide-block'], CartoonVisualOptions),
 }
 export type CartoonParams = typeof CartoonParams
 export function getCartoonParams(ctx: ThemeRegistryContext, structure: Structure) {
diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts
index f0d38b669..b836cc849 100644
--- a/src/mol-repr/structure/units-representation.ts
+++ b/src/mol-repr/structure/units-representation.ts
@@ -21,7 +21,7 @@ import { BehaviorSubject } from 'rxjs';
 
 export const UnitsParams = {
     ...StructureParams,
-    unitKinds: PD.MultiSelect<UnitKind>('Unit Kind', '', ['atomic', 'spheres'], UnitKindOptions),
+    unitKinds: PD.MultiSelect<UnitKind>(['atomic', 'spheres'], UnitKindOptions),
 }
 export type UnitsParams = typeof UnitsParams
 
diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
index bc2b296c4..e58322f57 100644
--- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
@@ -63,7 +63,7 @@ async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure:
 export const CarbohydrateLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    detail: PD.Numeric('Sphere Detail', '', 0, 0, 3, 1),
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }
 export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams
 
diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
index ab85e6f2a..440d6e834 100644
--- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
@@ -147,7 +147,7 @@ async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Struc
 
 export const CarbohydrateSymbolParams = {
     ...ComplexMeshParams,
-    detail: PD.Numeric('Sphere Detail', '', 0, 0, 3, 1),
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }
 export type CarbohydrateSymbolParams = typeof CarbohydrateSymbolParams
 
diff --git a/src/mol-repr/structure/visual/element-point.ts b/src/mol-repr/structure/visual/element-point.ts
index 174080b4e..2b146bde3 100644
--- a/src/mol-repr/structure/visual/element-point.ts
+++ b/src/mol-repr/structure/visual/element-point.ts
@@ -18,7 +18,7 @@ import { Theme } from 'mol-theme/theme';
 
 export const ElementPointParams = {
     ...UnitsPointsParams,
-    pointSizeAttenuation: PD.Boolean('Point Size Attenuation', '', false),
+    pointSizeAttenuation: PD.Boolean(false),
 }
 export type ElementPointParams = typeof ElementPointParams
 
diff --git a/src/mol-repr/structure/visual/element-sphere.ts b/src/mol-repr/structure/visual/element-sphere.ts
index 90a871221..4c1832195 100644
--- a/src/mol-repr/structure/visual/element-sphere.ts
+++ b/src/mol-repr/structure/visual/element-sphere.ts
@@ -14,9 +14,9 @@ import { BuiltInSizeThemeName, BuiltInSizeThemeOptions } from 'mol-theme/size';
 
 export const ElementSphereParams = {
     ...UnitsMeshParams,
-    sizeTheme: PD.Select<BuiltInSizeThemeName>('Size Theme', '', 'physical', BuiltInSizeThemeOptions),
-    sizeFactor: PD.Numeric('Size Factor', '', 1, 0, 10, 0.1),
-    detail: PD.Numeric('Sphere Detail', '', 0, 0, 3, 1),
+    sizeTheme: PD.Select<BuiltInSizeThemeName>('physical', BuiltInSizeThemeOptions),
+    sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }
 export type ElementSphereParams = typeof ElementSphereParams
 
diff --git a/src/mol-repr/structure/visual/gaussian-density-point.ts b/src/mol-repr/structure/visual/gaussian-density-point.ts
index 875705c3c..6a7e20353 100644
--- a/src/mol-repr/structure/visual/gaussian-density-point.ts
+++ b/src/mol-repr/structure/visual/gaussian-density-point.ts
@@ -21,7 +21,7 @@ import { Theme } from 'mol-theme/theme';
 export const GaussianDensityPointParams = {
     ...UnitsPointsParams,
     ...GaussianDensityParams,
-    pointSizeAttenuation: PD.Boolean('Point Size Attenuation', '', false),
+    pointSizeAttenuation: PD.Boolean(false),
 }
 export type GaussianDensityPointParams = typeof GaussianDensityPointParams
 
diff --git a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
index eec416e57..dea76bc8d 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
@@ -35,7 +35,7 @@ async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure
 export const GaussianWireframeParams = {
     ...UnitsLinesParams,
     ...GaussianDensityParams,
-    lineSizeAttenuation: PD.Boolean('Line Size Attenuation', '', false),
+    lineSizeAttenuation: PD.Boolean(false),
 }
 export type GaussianWireframeParams = typeof GaussianWireframeParams
 
diff --git a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
index 0355d66ba..5efdb9cbd 100644
--- a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
@@ -67,7 +67,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, s
 export const IntraUnitLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
 }
 export type IntraUnitLinkParams = typeof IntraUnitLinkParams
 
diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
index 2d9d82716..e208a134b 100644
--- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
@@ -35,7 +35,7 @@ const sVec = Vec3.zero()
 const box = Box()
 
 export const NucleotideBlockMeshParams = {
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
 }
 export const DefaultNucleotideBlockMeshProps = PD.getDefaultValues(NucleotideBlockMeshParams)
 export type NucleotideBlockMeshProps = typeof DefaultNucleotideBlockMeshProps
diff --git a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
index b10d5cee0..62777f8bc 100644
--- a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
@@ -21,7 +21,7 @@ import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
 export const PolymerBackboneCylinderParams = {
-    radialSegments: PD.Numeric('Radial Segments', '', 16, 3, 56, 1),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
 }
 export const DefaultPolymerBackboneCylinderProps = PD.getDefaultValues(PolymerBackboneCylinderParams)
 export type PolymerBackboneCylinderProps = typeof DefaultPolymerBackboneCylinderProps
diff --git a/src/mol-repr/structure/visual/polymer-direction-wedge.ts b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
index 1bca05adc..852fdbda7 100644
--- a/src/mol-repr/structure/visual/polymer-direction-wedge.ts
+++ b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
@@ -30,7 +30,7 @@ const heightFactor = 6
 const wedge = Wedge()
 
 export const PolymerDirectionWedgeParams = {
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
 }
 export const DefaultPolymerDirectionWedgeProps = PD.getDefaultValues(PolymerDirectionWedgeParams)
 export type PolymerDirectionWedgeProps = typeof DefaultPolymerDirectionWedgeProps
diff --git a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
index 0d60c1660..7b74e19ec 100644
--- a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
@@ -23,8 +23,8 @@ import { Theme } from 'mol-theme/theme';
 const segmentCount = 10
 
 export const PolymerGapCylinderParams = {
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
-    radialSegments: PD.Numeric('Radial Segments', '', 16, 3, 56, 1),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
 }
 export const DefaultPolymerGapCylinderProps = PD.getDefaultValues(PolymerGapCylinderParams)
 export type PolymerGapCylinderProps = typeof DefaultPolymerGapCylinderProps
diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
index 698f1f10e..e4eaad776 100644
--- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts
+++ b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
@@ -19,11 +19,11 @@ import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
 export const PolymerTraceMeshParams = {
-    sizeFactor: PD.Numeric('Size Factor', '', 0.2, 0, 10, 0.01),
-    linearSegments: PD.Numeric('Linear Segments', '', 8, 1, 48, 1),
-    radialSegments: PD.Numeric('Radial Segments', '', 16, 3, 56, 1),
-    aspectRatio: PD.Numeric('Aspect Ratio', '', 5, 0.1, 5, 0.1),
-    arrowFactor: PD.Numeric('Arrow Factor', '', 1.5, 0.1, 5, 0.1),
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    aspectRatio: PD.Numeric(5, { min: 0.1, max: 5, step: 0.1 }),
+    arrowFactor: PD.Numeric(1.5, { min: 0.1, max: 5, step: 0.1 }),
 }
 export const DefaultPolymerTraceMeshProps = PD.getDefaultValues(PolymerTraceMeshParams)
 export type PolymerTraceMeshProps = typeof DefaultPolymerTraceMeshProps
diff --git a/src/mol-repr/structure/visual/util/link.ts b/src/mol-repr/structure/visual/util/link.ts
index 179464f52..a080395f4 100644
--- a/src/mol-repr/structure/visual/util/link.ts
+++ b/src/mol-repr/structure/visual/util/link.ts
@@ -16,9 +16,9 @@ import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { VisualContext } from 'mol-repr/representation';
 
 export const LinkCylinderParams = {
-    linkScale: PD.Range('Link Scale', '', 0.4, 0, 1, 0.1),
-    linkSpacing: PD.Range('Link Spacing', '', 1, 0, 2, 0.01),
-    radialSegments: PD.Numeric('Radial Segments', '', 16, 3, 56, 1),
+    linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
+    linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
 }
 export const DefaultLinkCylinderProps = PD.getDefaultValues(LinkCylinderParams)
 export type LinkCylinderProps = typeof DefaultLinkCylinderProps
diff --git a/src/mol-repr/volume/isosurface-mesh.ts b/src/mol-repr/volume/isosurface-mesh.ts
index c7d6e015f..a0387ec44 100644
--- a/src/mol-repr/volume/isosurface-mesh.ts
+++ b/src/mol-repr/volume/isosurface-mesh.ts
@@ -41,7 +41,7 @@ export async function createVolumeIsosurface(ctx: VisualContext, volume: VolumeD
 
 export const IsosurfaceParams = {
     ...Mesh.Params,
-    isoValue: PD.Range('Iso Value', '', 0.22, -1, 1, 0.01),
+    isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
 }
 export type IsosurfaceParams = typeof IsosurfaceParams
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts
index 907d7cfbd..c252534c8 100644
--- a/src/mol-repr/volume/representation.ts
+++ b/src/mol-repr/volume/representation.ts
@@ -135,7 +135,7 @@ export type VolumeRepresentationProvider<P extends VolumeParams> = Representatio
 
 export const VolumeParams = {
     ...Geometry.Params,
-    isoValue: PD.Range('Iso Value', '', 0.22, -1, 1, 0.01),
+    isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
 }
 export type VolumeParams = typeof VolumeParams
 
diff --git a/src/mol-theme/color/chain-id.ts b/src/mol-theme/color/chain-id.ts
index 50018cacd..139da7c8a 100644
--- a/src/mol-theme/color/chain-id.ts
+++ b/src/mol-theme/color/chain-id.ts
@@ -17,7 +17,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives every chain a color based on its `asym_id` value.'
 
 export const ChainIdColorThemeParams = {
-    list: PD.Select<ColorListName>('Color Scale', '', 'RdYlBu', ColorListOptions),
+    list: PD.Select<ColorListName>('RdYlBu', ColorListOptions),
 }
 export function getChainIdColorThemeParams(ctx: ThemeDataContext) {
     return ChainIdColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/cross-link.ts b/src/mol-theme/color/cross-link.ts
index 220455d57..1f6ff23b4 100644
--- a/src/mol-theme/color/cross-link.ts
+++ b/src/mol-theme/color/cross-link.ts
@@ -18,8 +18,8 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).'
 
 export const CrossLinkColorThemeParams = {
-    domain: PD.Interval('Color Domain', '', [-10, 10]),
-    list: PD.Select<ColorListName>('Color Scale', '', 'RdYlBu', ColorListOptions),
+    domain: PD.Interval([-10, 10]),
+    list: PD.Select<ColorListName>('RdYlBu', ColorListOptions),
 }
 export function getCrossLinkColorThemeParams(ctx: ThemeDataContext) {
     return CrossLinkColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/element-index.ts b/src/mol-theme/color/element-index.ts
index c21938103..d0eb06e61 100644
--- a/src/mol-theme/color/element-index.ts
+++ b/src/mol-theme/color/element-index.ts
@@ -17,7 +17,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives every element (atom or coarse sphere/gaussian) a unique color based on the position (index) of the element in the list of elements in the structure.'
 
 export const ElementIndexColorThemeParams = {
-    list: PD.Select<ColorListName>('Color Scale', '', 'RdYlBu', ColorListOptions),
+    list: PD.Select<ColorListName>('RdYlBu', ColorListOptions),
 }
 export function getElementIndexColorThemeParams(ctx: ThemeDataContext) {
     return ElementIndexColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/polymer-index.ts b/src/mol-theme/color/polymer-index.ts
index de03c8700..f34dc35a7 100644
--- a/src/mol-theme/color/polymer-index.ts
+++ b/src/mol-theme/color/polymer-index.ts
@@ -16,7 +16,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives every polymer a unique color based on the position (index) of the polymer in the list of polymers in the structure.'
 
 export const PolymerIndexColorThemeParams = {
-    list: PD.Select<ColorListName>('Color Scale', '', 'RdYlBu', ColorListOptions),
+    list: PD.Select<ColorListName>('RdYlBu', ColorListOptions),
 }
 export function getPolymerIndexColorThemeParams(ctx: ThemeDataContext) {
     return PolymerIndexColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/sequence-id.ts b/src/mol-theme/color/sequence-id.ts
index 881c2057d..43023587e 100644
--- a/src/mol-theme/color/sequence-id.ts
+++ b/src/mol-theme/color/sequence-id.ts
@@ -17,7 +17,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives every polymer residue a color based on its `seq_id` value.'
 
 export const SequenceIdColorThemeParams = {
-    list: PD.Select<ColorListName>('Color Scale', '', 'rainbow', ColorListOptions),
+    list: PD.Select<ColorListName>('rainbow', ColorListOptions),
 }
 export function getSequenceIdColorThemeParams(ctx: ThemeDataContext) {
     return SequenceIdColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/uniform.ts b/src/mol-theme/color/uniform.ts
index 6f445450b..905deb9b3 100644
--- a/src/mol-theme/color/uniform.ts
+++ b/src/mol-theme/color/uniform.ts
@@ -13,7 +13,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives everything the same, uniform color.'
 
 export const UniformColorThemeParams = {
-    value: PD.Color('Color Value', '', DefaultColor),
+    value: PD.Color(DefaultColor),
 }
 export function getUniformColorThemeParams(ctx: ThemeDataContext) {
     return UniformColorThemeParams // TODO return copy
diff --git a/src/mol-theme/color/unit-index.ts b/src/mol-theme/color/unit-index.ts
index d821a61d6..0a5079615 100644
--- a/src/mol-theme/color/unit-index.ts
+++ b/src/mol-theme/color/unit-index.ts
@@ -16,7 +16,7 @@ const DefaultColor = Color(0xCCCCCC)
 const Description = 'Gives every unit (single chain or collection of single elements) a unique color based on the position (index) of the unit in the list of units in the structure.'
 
 export const UnitIndexColorThemeParams = {
-    list: PD.Select<ColorListName>('Color Scale', '', 'RdYlBu', ColorListOptions),
+    list: PD.Select<ColorListName>('RdYlBu', ColorListOptions),
 }
 export function getUnitIndexColorThemeParams(ctx: ThemeDataContext) {
     return UnitIndexColorThemeParams // TODO return copy
diff --git a/src/mol-theme/size/uniform.ts b/src/mol-theme/size/uniform.ts
index dee1b97bf..8d12a1600 100644
--- a/src/mol-theme/size/uniform.ts
+++ b/src/mol-theme/size/uniform.ts
@@ -11,7 +11,7 @@ import { ThemeDataContext } from 'mol-theme/theme';
 const Description = 'Gives everything the same, uniform size.'
 
 export const UniformSizeThemeParams = {
-    value: PD.Numeric('Size Value', '', 1, 0, 20, 0.1),
+    value: PD.Numeric(1, { min: 0, max: 20, step: 0.1 }),
 }
 export function getUniformSizeThemeParams(ctx: ThemeDataContext) {
     return UniformSizeThemeParams // TODO return copy
diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts
index b7c5b083f..9c1aa24bf 100644
--- a/src/mol-util/index.ts
+++ b/src/mol-util/index.ts
@@ -183,9 +183,4 @@ export function formatProgress(p: Progress) {
     if (tp.isIndeterminate) return tp.message;
     const x = (100 * tp.current / tp.max).toFixed(2);
     return `${tp.message} ${x}%`;
-}
-
-const reLine = /^/mg
-export function indentString(str: string, count: number, indent: string) {
-    return count === 0 ? str : str.replace(reLine, indent.repeat(count))
 }
\ No newline at end of file
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index 6e9e58ece..3b36bbd2a 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -7,19 +7,24 @@
 
 import { Color as ColorData } from './color';
 import { shallowClone } from 'mol-util';
+import { Vec2 } from 'mol-math/linear-algebra';
+import { camelCaseToWords } from './string';
 
 export namespace ParamDefinition {
-    export interface Base<T> {
-        label: string
-        description: string
+    export interface Info {
+        label?: string
+        description?: string
+    }
+
+    export interface Base<T> extends Info {
         defaultValue: T
     }
 
     export interface Value<T> extends Base<T> {
         type: 'value'
     }
-    export function Value<T>(label: string, description: string, defaultValue: T): Value<T> {
-        return { type: 'value', label, description, defaultValue }
+    export function Value<T>(defaultValue: T, info: Info = {}): Value<T> {
+        return { type: 'value', defaultValue, ...info }
     }
 
     export interface Select<T extends string> extends Base<T> {
@@ -27,8 +32,8 @@ export namespace ParamDefinition {
         /** array of (value, label) tuples */
         options: [T, string][]
     }
-    export function Select<T extends string>(label: string, description: string, defaultValue: T, options: [T, string][]): Select<T> {
-        return { type: 'select', label, description, defaultValue, options }
+    export function Select<T extends string>(defaultValue: T, options: [T, string][], info: Info = {}): Select<T> {
+        return { type: 'select', defaultValue, options, ...info }
     }
 
     export interface MultiSelect<E extends string, T = E[]> extends Base<T> {
@@ -36,66 +41,67 @@ export namespace ParamDefinition {
         /** array of (value, label) tuples */
         options: [E, string][]
     }
-    export function MultiSelect<E extends string, T = E[]>(label: string, description: string, defaultValue: T, options: [E, string][]): MultiSelect<E, T> {
-        return { type: 'multi-select', label, description, defaultValue, options }
+    export function MultiSelect<E extends string, T = E[]>(defaultValue: T, options: [E, string][], info: Info = {}): MultiSelect<E, T> {
+        return { type: 'multi-select', defaultValue, options, ...info }
     }
 
     export interface Boolean extends Base<boolean> {
         type: 'boolean'
     }
-    export function Boolean(label: string, description: string, defaultValue: boolean): Boolean {
-        return { type: 'boolean', label, description, defaultValue }
-    }
-
-    export interface Range extends Base<number> {
-        type: 'range'
-        min: number
-        max: number
-        /** if an `integer` parse value with parseInt, otherwise use parseFloat */
-        step: number
-    }
-    export function Range(label: string, description: string, defaultValue: number, min: number, max: number, step: number): Range {
-        return { type: 'range', label, description, defaultValue, min, max, step }
+    export function Boolean(defaultValue: boolean, info: Info = {}): Boolean {
+        return { type: 'boolean', defaultValue, ...info }
     }
 
     export interface Text extends Base<string> {
         type: 'text'
     }
-    export function Text(label: string, description: string, defaultValue: string = ''): Text {
-        return { type: 'text', label, description, defaultValue }
+    export function Text(defaultValue: string = '', info: Info = {}): Text {
+        return { type: 'text', defaultValue, ...info }
     }
 
     export interface Color extends Base<ColorData> {
         type: 'color'
     }
-    export function Color(label: string, description: string, defaultValue: ColorData): Color {
-        return { type: 'color', label, description, defaultValue }
+    export function Color(defaultValue: ColorData, info: Info = {}): Color {
+        return { type: 'color', defaultValue, ...info }
     }
 
     export interface Numeric extends Base<number> {
         type: 'number'
-        min: number
-        max: number
-        /** if an `integer` parse value with parseInt, otherwise use parseFloat */
-        step: number
+        /** If given treat as a range. */
+        min?: number
+        /** If given treat as a range. */
+        max?: number
+        /**
+         * If given treat as a range.
+         * If an `integer` parse value with parseInt, otherwise use parseFloat.
+         */
+        step?: number
     }
-    export function Numeric(label: string, description: string, defaultValue: number, min: number, max: number, step: number): Numeric {
-        return { type: 'number', label, description, defaultValue, min, max, step }
+    export function Numeric(defaultValue: number, range: { min?: number, max?: number, step?: number } = {}, info: Info = {}): Numeric {
+        return { type: 'number', defaultValue, ...range, ...info }
     }
 
     export interface Interval extends Base<[number, number]> {
         type: 'interval'
     }
-    export function Interval(label: string, description: string, defaultValue: [number, number]): Interval {
-        return { type: 'interval', label, description, defaultValue }
+    export function Interval(defaultValue: [number, number], info: Info = {}): Interval {
+        return { type: 'interval', defaultValue, ...info }
+    }
+
+    export interface LineGraph extends Base<Vec2[]> {
+        type: 'line-graph'
+    }
+    export function LineGraph(defaultValue: Vec2[], info: Info = {}): LineGraph {
+        return { type: 'line-graph', defaultValue, ...info }
     }
 
     export interface Group<T> extends Base<T> {
         type: 'group',
         params: Params
     }
-    export function Group<P extends Params>(label: string, description: string, params: P): Group<Values<P>> {
-        return { type: 'group', label, description, defaultValue: getDefaultValues(params) as any, params };
+    export function Group<P extends Params>(params: P, info: Info = {}): Group<Values<P>> {
+        return { type: 'group', defaultValue: getDefaultValues(params) as any, params };
     }
 
     export interface Mapped<T> extends Base<{ name: string, params: T }> {
@@ -103,18 +109,23 @@ export namespace ParamDefinition {
         select: Select<string>,
         map(name: string): Any
     }
-    export function Mapped<T>(label: string, description: string, defaultKey: string, names: [string, string][], map: Mapped<T>['map']): Mapped<T> {
+    export function Mapped<T>(defaultKey: string, names: [string, string][], map: Mapped<T>['map'], info: Info = {}): Mapped<T> {
         return {
             type: 'mapped',
-            label,
-            description,
             defaultValue: { name: defaultKey, params: map(defaultKey).defaultValue as any },
-            select: Select<string>(label, description, defaultKey, names),
+            select: Select<string>(defaultKey, names, info),
             map
         };
     }
 
-    export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Range | Text | Color | Numeric | Interval | Group<any> | Mapped<any>
+    export interface Converted<T, C> extends Base<T> {
+        type: 'converted',
+        convertedControl: Base<C>,
+        fromValue(v: T): C,
+        toValue(v: C): T
+    }
+
+    export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Numeric | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any>
 
     export type Params = { [k: string]: Any }
     export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] }
@@ -125,6 +136,15 @@ export namespace ParamDefinition {
         return d as Values<T>
     }
 
+    export function getLabels<T extends Params>(params: T) {
+        const d: { [k: string]: string } = {}
+        Object.keys(params).forEach(k => {
+            const label = params[k].label
+            d[k] = label === undefined ? camelCaseToWords(k) : label
+        })
+        return d as { [k in keyof T]: string }
+    }
+
     export function clone<P extends Params>(params: P): P {
         return shallowClone(params)
     }
diff --git a/src/mol-util/string.ts b/src/mol-util/string.ts
new file mode 100644
index 000000000..a40026c5d
--- /dev/null
+++ b/src/mol-util/string.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+const reLine = /^/mg
+export function indentString(str: string, count: number, indent: string) {
+    return count === 0 ? str : str.replace(reLine, indent.repeat(count))
+}
+
+/** Add space between camelCase text. */
+export function splitCamelCase(str: string) {
+    return str.replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, '$1 $2')
+}
+
+/** Split camelCase text and capitalize. */
+export function camelCaseToWords(str: string) {
+    return capitalize(splitCamelCase(str))
+}
+
+export const lowerCase = (str: string) => str.toLowerCase()
+export const upperCase = (str: string) => str.toUpperCase()
+
+/** Uppercase the first character of each word. */
+export function capitalize(str: string) {
+    return str.toLowerCase().replace(/^\w|\s\w/g, upperCase);
+}
\ No newline at end of file
-- 
GitLab