diff --git a/.babelrc b/.babelrc
index c37be69d0..0ebdf4d1e 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,7 @@
{
"presets": ["react", "es2015"],
"plugins": [
+ "react-hot-loader/babel",
"transform-object-rest-spread",
[
"module-resolver",
diff --git a/.gitignore b/.gitignore
index 0482bd203..0f8930425 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,5 @@ lib
!src/lib
accessTokens.js
-
yarn.lock
package-lock.json
diff --git a/dev/App.js b/dev/App.js
index f27a7c941..91e8123b7 100644
--- a/dev/App.js
+++ b/dev/App.js
@@ -83,6 +83,7 @@ class App extends Component {
dataSources={dataSources}
dataSourceOptions={dataSourceOptions}
plotly={plotly}
+ advancedTraceTypeSelector
/>
+
+ this.context.handleClose()}
+ />
+ {children}
+
+
this.context.handleClose()}
+ />
+
+ );
+ }
+}
+
+ModalHeader.propTypes = {
+ title: PropTypes.node,
+ handleClose: PropTypes.func.isRequired,
+};
+
+ModalContent.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+Modal.propTypes = {
+ children: PropTypes.node.isRequired,
+ title: PropTypes.node,
+};
+
+Modal.contextTypes = {
+ handleClose: PropTypes.func,
+ isAnimatingOut: PropTypes.bool,
+};
+
+export default Modal;
+
+export {ModalHeader, ModalContent};
diff --git a/src/components/containers/ModalProvider.js b/src/components/containers/ModalProvider.js
new file mode 100644
index 000000000..919e3bdbd
--- /dev/null
+++ b/src/components/containers/ModalProvider.js
@@ -0,0 +1,95 @@
+import React, {Fragment} from 'react';
+import PropTypes from 'prop-types';
+
+class ModalProvider extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ component: null,
+ componentProps: {},
+ open: false,
+ isAnimatingOut: false,
+ };
+ }
+
+ componentDidUpdate() {
+ const body = document.body;
+ const {open} = this.state;
+
+ // Toggle scroll on document body if modal is open
+ const hasClass = body.classList.contains('no-scroll');
+
+ if (open && !hasClass) {
+ body.classList.add('no-scroll');
+ }
+ if (!open && hasClass) {
+ body.classList.remove('no-scroll');
+ }
+ }
+
+ openModal(component, props) {
+ if (!component) {
+ throw Error('You need to provide a component for the modal to open!');
+ }
+ const {open} = this.state;
+
+ if (!open) {
+ this.setState({
+ component: component,
+ componentProps: props,
+ open: true,
+ });
+ }
+ }
+
+ closeModal() {
+ const {open} = this.state;
+ if (open) {
+ this.setState({
+ open: false,
+ component: null,
+ });
+ }
+ }
+ handleClose() {
+ this.setState({isAnimatingOut: true});
+ const animationDuration = 600;
+ setTimeout(() => {
+ this.setState({isAnimatingOut: false});
+ this.closeModal();
+ }, animationDuration);
+ }
+
+ getChildContext() {
+ return {
+ openModal: (c, p) => this.openModal(c, p),
+ closeModal: () => this.closeModal(),
+ handleClose: () => this.handleClose(),
+ isAnimatingOut: this.state.isAnimatingOut,
+ };
+ }
+
+ render() {
+ const {component: Component, componentProps, isAnimatingOut} = this.state;
+ return (
+
+ {this.props.children}
+ {this.state.open ? (
+
+ ) : null}
+
+ );
+ }
+}
+
+ModalProvider.propTypes = {
+ children: PropTypes.node,
+};
+ModalProvider.childContextTypes = {
+ openModal: PropTypes.func,
+ closeModal: PropTypes.func,
+ handleClose: PropTypes.func,
+ isAnimatingOut: PropTypes.bool,
+};
+
+export default ModalProvider;
diff --git a/src/components/containers/index.js b/src/components/containers/index.js
index 8a7c373ed..fc8cb710a 100644
--- a/src/components/containers/index.js
+++ b/src/components/containers/index.js
@@ -12,6 +12,8 @@ import TraceMarkerSection from './TraceMarkerSection';
import {LayoutPanel, TraceTypeSection} from './derived';
import TraceRequiredPanel from './TraceRequiredPanel';
import SingleSidebarItem from './SingleSidebarItem';
+import ModalProvider from './ModalProvider';
+import Modal from './Modal';
export {
AnnotationAccordion,
@@ -29,4 +31,6 @@ export {
AxesFold,
SingleSidebarItem,
TraceTypeSection,
+ Modal,
+ ModalProvider,
};
diff --git a/src/components/fields/TraceSelector.js b/src/components/fields/TraceSelector.js
index 4f8bd942e..9302224eb 100644
--- a/src/components/fields/TraceSelector.js
+++ b/src/components/fields/TraceSelector.js
@@ -6,72 +6,17 @@ import {
traceTypeToPlotlyInitFigure,
localize,
plotlyTraceToCustomTrace,
+ computeTraceOptionsFromSchema,
} from 'lib';
-
-function computeTraceOptionsFromSchema(schema, _, context) {
- // Filter out Polar "area" type as it is fairly broken and we want to present
- // scatter with fill as an "area" chart type for convenience.
- const traceTypes = Object.keys(schema.traces).filter(
- t => !['area', 'scattermapbox'].includes(t)
- );
-
- // explicit map of all supported trace types (as of plotlyjs 1.32)
- const traceOptions = [
- {value: 'scatter', label: _('Scatter')},
- {value: 'box', label: _('Box')},
- {value: 'bar', label: _('Bar')},
- {value: 'heatmap', label: _('Heatmap')},
- // {value: 'histogram', label: _('Histogram')},
- // {value: 'histogram2d', label: _('2D Histogram')},
- // {value: 'histogram2dcontour', label: _('2D Contour Histogram')},
- {value: 'pie', label: _('Pie')},
- {value: 'contour', label: _('Contour')},
- {value: 'scatterternary', label: _('Ternary Scatter')},
- // {value: 'violin', label: _('Violin')},
- {value: 'scatter3d', label: _('3D Scatter')},
- {value: 'surface', label: _('Surface')},
- {value: 'mesh3d', label: _('3D Mesh')},
- {value: 'scattergeo', label: _('Atlas Map')},
- {value: 'choropleth', label: _('Choropleth')},
- // {value: 'scattergl', label: _('Scatter GL')},
- // {value: 'pointcloud', label: _('Point Cloud')},
- // {value: 'heatmapgl', label: _('Heatmap GL')},
- // {value: 'parcoords', label: _('Parallel Coordinates')},
- // {value: 'sankey', label: _('Sankey')},
- // {value: 'table', label: _('Table')},
- // {value: 'carpet', label: _('Carpet')},
- // {value: 'scattercarpet', label: _('Carpet Scatter')},
- // {value: 'contourcarpet', label: _('Carpet Contour')},
- {value: 'ohlc', label: _('OHLC')},
- {value: 'candlestick', label: _('Candlestick')},
- // {value: 'scatterpolar', label: _('Polar Scatter')},
- ].filter(obj => traceTypes.indexOf(obj.value) !== -1);
-
- const traceIndex = traceType =>
- traceOptions.findIndex(opt => opt.value === traceType);
-
- traceOptions.splice(
- traceIndex('scatter') + 1,
- 0,
- {label: _('Line'), value: 'line'},
- {label: _('Area'), value: 'area'}
- );
-
- traceOptions.splice(traceIndex('scatter3d') + 1, 0, {
- label: _('3D Line'),
- value: 'line3d',
- });
-
- if (context.config && context.config.mapboxAccessToken) {
- traceOptions.push({value: 'scattermapbox', label: _('Satellite Map')});
- }
-
- return traceOptions;
-}
+import TraceTypeSelector, {
+ TraceTypeSelectorButton,
+} from 'components/widgets/TraceTypeSelector';
+import Field from './Field';
class TraceSelector extends Component {
constructor(props, context) {
super(props, context);
+
this.updatePlot = this.updatePlot.bind(this);
let fillMeta;
@@ -102,6 +47,8 @@ class TraceSelector extends Component {
const _ = props.localize;
if (props.traceOptions) {
this.traceOptions = props.traceOptions;
+ } else if (context.traceTypesConfig) {
+ this.traceOptions = context.traceTypesConfig.traces(_);
} else if (context.plotSchema) {
this.traceOptions = computeTraceOptionsFromSchema(
context.plotSchema,
@@ -138,7 +85,6 @@ class TraceSelector extends Component {
updatePlot(value) {
const {updateContainer} = this.props;
-
if (updateContainer) {
updateContainer(traceTypeToPlotlyInitFigure(value));
}
@@ -151,12 +97,28 @@ class TraceSelector extends Component {
options: this.traceOptions,
clearable: false,
});
+ // Check and see if the advanced selector prop is true
+ const {advancedTraceTypeSelector} = this.context;
+ if (advancedTraceTypeSelector) {
+ return (
+
+ this.context.openModal(TraceTypeSelector, props)}
+ />
+
+ );
+ }
return
;
}
}
TraceSelector.contextTypes = {
+ openModal: PropTypes.func,
+ advancedTraceTypeSelector: PropTypes.bool,
+ traceTypesConfig: PropTypes.object,
plotSchema: PropTypes.object,
config: PropTypes.object,
};
diff --git a/src/components/widgets/TraceTypeSelector.js b/src/components/widgets/TraceTypeSelector.js
new file mode 100644
index 000000000..7bbaf7840
--- /dev/null
+++ b/src/components/widgets/TraceTypeSelector.js
@@ -0,0 +1,186 @@
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+import {SearchIcon, ThumnailViewIcon, GraphIcon} from 'plotly-icons';
+import Modal from 'components/containers/Modal';
+import {traceTypeToPlotlyInitFigure, localize, renderTraceIcon} from 'lib';
+
+const actions = ({value}) => [
+ {
+ label: `Charts like this by Plotly users.`,
+ href: `https://plot.ly/feed/?q=plottype:${value}`,
+ icon:
,
+ },
+ {
+ label: `View tutorials on this chart type.`,
+ href: `#`,
+ icon:
,
+ },
+ {
+ label: `See a basic example.`,
+ href: `#`,
+ icon:
,
+ },
+];
+
+const renderActionItems = (actionItems, item) =>
+ actionItems
+ ? actionItems(item).map((action, i) => (
+
+ {action.icon}
+
+ ))
+ : null;
+
+const Item = ({item, active, handleClick, actions, showActions, complex}) => {
+ const {label, value, icon} = item;
+ const SimpleIcon = renderTraceIcon(icon ? icon : value);
+ const ComplexIcon = () => (
+

+ );
+
+ return (
+
handleClick()}
+ >
+
+ {actions && showActions ? renderActionItems(actions, item) : null}
+
+
+ {!complex && (
+
+
+
+ )}
+ {complex && (
+
+
+
+ )}
+
+
{label}
+
+ );
+};
+
+class TraceTypeSelector extends Component {
+ selectAndClose(value) {
+ const computedValue = traceTypeToPlotlyInitFigure(value);
+ this.props.updateContainer(computedValue);
+ this.context.handleClose();
+ }
+
+ renderCategories() {
+ const {fullValue, localize: _} = this.props;
+ const {traces, categories, complex} = this.context.traceTypesConfig;
+
+ return categories(_).map((category, i) => {
+ const items = traces(_).filter(
+ ({category: {value}}) => value === category.value
+ );
+
+ const MAX_ITEMS = 4;
+
+ let columnClasses = 'trace-grid__column';
+
+ if (items.length > MAX_ITEMS) {
+ columnClasses += ' trace-grid__column--double';
+ }
+
+ return (
+
+
{category.label}
+
+ {items.map(item => (
+ - this.selectAndClose(item.value)}
+ />
+ ))}
+
+
+ );
+ });
+ }
+
+ render() {
+ return (
+
+ {this.renderCategories()}
+
+ );
+ }
+}
+
+class TraceTypeButton extends React.Component {
+ render() {
+ const {
+ handleClick,
+ fullValue,
+ localize: _,
+ traceTypesConfig: {traces},
+ } = this.props;
+
+ const {label, icon, value} = traces(_).find(
+ type => type.value === fullValue
+ );
+
+ const Icon = renderTraceIcon(icon ? icon : value);
+
+ return (
+
handleClick() : null}
+ >
+
+
+
+ {label}
+
+ );
+ }
+}
+
+TraceTypeSelector.propTypes = {
+ updateContainer: PropTypes.func,
+ fullValue: PropTypes.string,
+ localize: PropTypes.func,
+};
+TraceTypeButton.propTypes = {
+ handleClick: PropTypes.func.isRequired,
+ fullValue: PropTypes.string.isRequired,
+ localize: PropTypes.func.isRequired,
+ traceTypesConfig: PropTypes.object.isRequired,
+};
+TraceTypeSelector.contextTypes = {
+ traceTypesConfig: PropTypes.object,
+ handleClose: PropTypes.func,
+};
+Item.propTypes = {
+ item: PropTypes.object,
+ active: PropTypes.bool,
+ complex: PropTypes.bool,
+ handleClick: PropTypes.func,
+ actions: PropTypes.array,
+ showActions: PropTypes.bool,
+};
+
+export default localize(TraceTypeSelector);
+
+export const TraceTypeSelectorButton = localize(TraceTypeButton);
diff --git a/src/lib/computeTraceOptionsFromSchema.js b/src/lib/computeTraceOptionsFromSchema.js
new file mode 100644
index 000000000..cb4ac78af
--- /dev/null
+++ b/src/lib/computeTraceOptionsFromSchema.js
@@ -0,0 +1,149 @@
+function computeTraceOptionsFromSchema(schema, _, context) {
+ // Filter out Polar "area" type as it is fairly broken and we want to present
+ // scatter with fill as an "area" chart type for convenience.
+ const traceTypes = Object.keys(schema.traces).filter(
+ t => !['area', 'scattermapbox'].includes(t)
+ );
+
+ const traceOptions = [
+ {
+ value: 'scatter',
+ label: _('Scatter'),
+ },
+ {
+ value: 'box',
+ label: _('Box'),
+ },
+ {
+ value: 'bar',
+ label: _('Bar'),
+ },
+ {
+ value: 'heatmap',
+ label: _('Heatmap'),
+ },
+ {
+ value: 'histogram',
+ label: _('Histogram'),
+ },
+ {
+ value: 'histogram2d',
+ label: _('2D Histogram'),
+ },
+ {
+ value: 'histogram2dcontour',
+ label: _('2D Contour Histogram'),
+ },
+ {
+ value: 'pie',
+ label: _('Pie'),
+ },
+ {
+ value: 'contour',
+ label: _('Contour'),
+ },
+ {
+ value: 'scatterternary',
+ label: _('Ternary Scatter'),
+ },
+ {
+ value: 'violin',
+ label: _('Violin'),
+ },
+ {
+ value: 'scatter3d',
+ label: _('3D Scatter'),
+ },
+ {
+ value: 'surface',
+ label: _('Surface'),
+ },
+ {
+ value: 'mesh3d',
+ label: _('3D Mesh'),
+ },
+ {
+ value: 'scattergeo',
+ label: _('Atlas Map'),
+ },
+ {
+ value: 'choropleth',
+ label: _('Choropleth'),
+ },
+ {
+ value: 'scattergl',
+ label: _('Scatter GL'),
+ },
+ {
+ value: 'pointcloud',
+ label: _('Point Cloud'),
+ },
+ {
+ value: 'heatmapgl',
+ label: _('Heatmap GL'),
+ },
+ {
+ value: 'parcoords',
+ label: _('Parallel Coordinates'),
+ },
+ {
+ value: 'sankey',
+ label: _('Sankey'),
+ },
+ {
+ value: 'table',
+ label: _('Table'),
+ },
+ {
+ value: 'carpet',
+ label: _('Carpet'),
+ },
+ {
+ value: 'scattercarpet',
+ label: _('Carpet Scatter'),
+ },
+ {
+ value: 'contourcarpet',
+ label: _('Carpet Contour'),
+ },
+ {
+ value: 'ohlc',
+ label: _('OHLC'),
+ },
+ {
+ value: 'candlestick',
+ label: _('Candlestick'),
+ },
+ {
+ value: 'scatterpolar',
+ label: _('Polar Scatter'),
+ },
+ ].filter(obj => traceTypes.indexOf(obj.value) !== -1);
+
+ const traceIndex = traceType =>
+ traceOptions.findIndex(opt => opt.value === traceType);
+
+ traceOptions.splice(
+ traceIndex('scatter') + 1,
+ 0,
+ {label: _('Line'), value: 'line'},
+ {label: _('Area'), value: 'area'},
+ {label: _('Timeseries'), value: 'timeseries'}
+ );
+
+ traceOptions.splice(traceIndex('scatter3d') + 1, 0, {
+ label: _('3D Line'),
+ value: 'line3d',
+ });
+
+ if (context.config && context.config.mapboxAccessToken) {
+ traceOptions.push({
+ value: 'scattermapbox',
+ label: _('Satellite Map'),
+ });
+ }
+
+ return traceOptions;
+}
+
+export {computeTraceOptionsFromSchema};
diff --git a/src/lib/index.js b/src/lib/index.js
index ace5747d8..acb0bb8cd 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -52,6 +52,8 @@ function renderTraceIcon(trace) {
: PlotlyIcons.PlotLineIcon;
}
+import {computeTraceOptionsFromSchema} from './computeTraceOptionsFromSchema';
+
export {
axisIdToAxisName,
bem,
@@ -65,6 +67,7 @@ export {
connectToContainer,
connectTraceToPlot,
containerConnectedContextTypes,
+ computeTraceOptionsFromSchema,
traceTypeToPlotlyInitFigure,
dereference,
findFullTraceIndex,
diff --git a/src/lib/traceTypes.js b/src/lib/traceTypes.js
new file mode 100644
index 000000000..2d3a25c8e
--- /dev/null
+++ b/src/lib/traceTypes.js
@@ -0,0 +1,207 @@
+/**
+ * Trace type constants
+ */
+
+export const chartCategory = _ => {
+ return {
+ SIMPLE: {
+ value: 'SIMPLE',
+ label: _('Simple'),
+ },
+ CHARTS_3D: {
+ value: 'CHARTS_3D',
+ label: _('3D charts'),
+ },
+ FINANCIAL: {
+ value: 'FINANCIAL',
+ label: _('Finance'),
+ },
+ DISTRIBUTIONS: {
+ value: 'DISTRIBUTIONS',
+ label: _('Distributions'),
+ },
+ MAPS: {
+ value: 'MAPS',
+ label: _('Maps'),
+ },
+ SPECIALIZED: {
+ value: 'SPECIALIZED',
+ label: _('Specialized'),
+ },
+ WEB_GL: {
+ value: 'WEB_GL',
+ label: _('WebGL'),
+ },
+ };
+};
+
+// Layout specification for TraceTypeSelector.js
+export const categoryLayout = _ => [
+ chartCategory(_).SIMPLE,
+ chartCategory(_).WEB_GL,
+ chartCategory(_).DISTRIBUTIONS,
+ chartCategory(_).FINANCIAL,
+ chartCategory(_).MAPS,
+ chartCategory(_).SPECIALIZED,
+];
+
+export const traceTypes = _ => [
+ {
+ value: 'scatter',
+ label: _('Scatter'),
+ category: chartCategory(_).SIMPLE,
+ },
+ {
+ value: 'bar',
+ label: _('Bar'),
+ category: chartCategory(_).SIMPLE,
+ },
+ {
+ value: 'line',
+ label: _('Line'),
+ category: chartCategory(_).SIMPLE,
+ },
+ // {
+ // value: 'area',
+ // label: _('Area'),
+ // category: chartCategory(_).SIMPLE,
+ // },
+ {
+ value: 'heatmap',
+ label: _('Heatmap'),
+ category: chartCategory(_).SIMPLE,
+ },
+ // {
+ // value: 'table',
+ // label: _('Table'),
+ // category: chartCategory(_).SIMPLE,
+ // },
+ {
+ value: 'contour',
+ label: _('Contour'),
+ category: chartCategory(_).SIMPLE,
+ },
+ {
+ value: 'pie',
+ label: _('Pie'),
+ category: chartCategory(_).SIMPLE,
+ },
+ {
+ value: 'scatter3d',
+ label: _('3D Scatter'),
+ category: chartCategory(_).WEB_GL,
+ },
+ {
+ value: 'line3d',
+ label: _('3D Line'),
+ category: chartCategory(_).WEB_GL,
+ },
+ {
+ value: 'surface',
+ label: _('3D Surface'),
+ category: chartCategory(_).WEB_GL,
+ },
+ {
+ value: 'mesh3d',
+ label: _('3D Mesh'),
+ category: chartCategory(_).WEB_GL,
+ },
+ {
+ value: 'box',
+ label: _('Box'),
+ category: chartCategory(_).DISTRIBUTIONS,
+ },
+ {
+ value: 'histogram',
+ label: _('Histogram'),
+ category: chartCategory(_).DISTRIBUTIONS,
+ },
+ {
+ value: 'histogram2d',
+ label: _('2D Histogram'),
+ category: chartCategory(_).DISTRIBUTIONS,
+ },
+ {
+ value: 'histogram2dcontour',
+ label: _('2D Contour Histogram'),
+ category: chartCategory(_).DISTRIBUTIONS,
+ },
+ // {
+ // value: 'violin',
+ // label: _('Violin'),
+ // category: chartCategory(_).DISTRIBUTIONS,
+ // },
+ {
+ value: 'choropleth',
+ label: _('Choropleth'),
+ category: chartCategory(_).MAPS,
+ },
+ {
+ value: 'scattermapbox',
+ label: _('Satellite Map'),
+ category: chartCategory(_).MAPS,
+ },
+ {
+ value: 'scattergeo',
+ label: _('Atlas Map'),
+ category: chartCategory(_).MAPS,
+ },
+ {
+ value: 'candlestick',
+ label: _('Candlestick'),
+ category: chartCategory(_).FINANCIAL,
+ },
+ {
+ value: 'ohlc',
+ label: _('OHLC'),
+ category: chartCategory(_).FINANCIAL,
+ },
+ // {
+ // value: 'parcoords',
+ // label: _('Parallel Coordinates'),
+ // category: chartCategory(_).SPECIALIZED,
+ // },
+ // {
+ // value: 'sankey',
+ // label: _('Sankey'),
+ // category: chartCategory(_).SPECIALIZED,
+ // },
+ // {
+ // value: 'carpet',
+ // label: _('Carpet'),
+ // category: chartCategory(_).SPECIALIZED,
+ // },
+ // {
+ // value: 'scatterpolar',
+ // label: _('Polar Scatter'),
+ // category: chartCategory(_).SPECIALIZED,
+ // },
+ {
+ value: 'scatterternary',
+ label: _('Ternary Scatter'),
+ category: chartCategory(_).SPECIALIZED,
+ },
+ // {
+ // value: 'pointcloud',
+ // label: _('Point Cloud'),
+ // category: chartCategory(_).WEB_GL,
+ // },
+ {
+ value: 'scattergl',
+ icon: 'scatter',
+ label: _('Scatter GL'),
+ category: chartCategory(_).WEB_GL,
+ },
+ // {
+ // value: 'scatterpolarghl',
+ // icon: 'scatterpolar',
+ // label: _('Scatter Polar GL'),
+ // category: chartCategory(_).WEB_GL,
+ // },
+ // {
+ // value: 'heatmapgl',
+ // icon: 'heatmap',
+ // label: _('Heatmap GL'),
+ // category: chartCategory(_).WEB_GL,
+ // },
+];
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index 5fe267a47..b3a11a800 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -163,7 +163,9 @@
@each $var, $val in $sidebar {
#{--sidebar}-#{$var}: $val;
}
- --editor-width: calc(var(--sidebar-width) + var(--panel-width) + 1px); // +1px for border
+ --editor-width: calc(
+ var(--sidebar-width) + var(--panel-width) + 1px
+ ); // +1px for border
}
@mixin heading($type: 'base') {
@@ -190,3 +192,26 @@
letter-spacing: var(--font-letter-spacing-headings);
}
}
+
+@mixin animate($type: 'fade-in', $time: 1s, $delay: 0s) {
+ @if $type == 'fade-in' {
+ @extend .animate--fade-in;
+ animation-duration: $time;
+ animation-delay: $delay;
+ }
+ @if $type == 'fade-out' {
+ @extend .animate--fade-out;
+ animation-duration: $time;
+ animation-delay: $delay;
+ }
+ @if $type == 'fsb' {
+ @extend .animate--fade-and-slide-in-from-bottom;
+ animation-duration: $time;
+ animation-delay: $delay;
+ }
+ @if $type == 'fsbr' {
+ @extend .animate--fsbr;
+ animation-duration: $time;
+ animation-delay: $delay;
+ }
+}
diff --git a/src/styles/_movement.scss b/src/styles/_movement.scss
new file mode 100644
index 000000000..624f1f6fc
--- /dev/null
+++ b/src/styles/_movement.scss
@@ -0,0 +1,64 @@
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fade-and-slide-in-from-bottom {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes fsbr {
+ from {
+ opacity: 1;
+ transform: none;
+ }
+ to {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+}
+
+@keyframes fade-out {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ }
+}
+
+.animate {
+ &--fade-in {
+ opacity: 0;
+ animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
+ }
+ &--fade-out {
+ opacity: 1;
+ animation: fade-out 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
+ }
+ &--fade-and-slide-in-from-bottom {
+ opacity: 0;
+ transform: translateY(20px);
+ animation: fade-and-slide-in-from-bottom 0.1s forwards
+ cubic-bezier(0.19, 1, 0.22, 1);
+ }
+ &--fsbr {
+ opacity: 1;
+ transform: none;
+ animation: fsbr 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
+ }
+}
diff --git a/src/styles/components/containers/_main.scss b/src/styles/components/containers/_main.scss
index fde545d50..52459fdb7 100644
--- a/src/styles/components/containers/_main.scss
+++ b/src/styles/components/containers/_main.scss
@@ -1,7 +1,8 @@
-@import "panel";
-@import "fold";
-@import "section";
-@import "menupanel";
-@import "info";
-@import "modalbox";
-@import "tabs";
+@import 'panel';
+@import 'fold';
+@import 'section';
+@import 'menupanel';
+@import 'info';
+@import 'modalbox';
+@import 'modal';
+@import 'tabs';
diff --git a/src/styles/components/containers/_modal.scss b/src/styles/components/containers/_modal.scss
new file mode 100644
index 000000000..c132faca7
--- /dev/null
+++ b/src/styles/components/containers/_modal.scss
@@ -0,0 +1,98 @@
+.modal {
+ box-sizing: border-box;
+ * {
+ box-sizing: border-box;
+ }
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ align-items: flex-start;
+ overflow-y: auto;
+ justify-content: center;
+ @include z-index('orbit');
+
+ &__backdrop {
+ height: 100%;
+ width: 100%;
+ left: 0;
+ top: 0;
+ position: fixed;
+ opacity: 0;
+ will-change: opacity;
+ &::before {
+ content: '';
+ height: 100%;
+ width: 100%;
+ left: 0;
+ opacity: 0.5;
+ top: 0;
+ background: var(--color-background-dark);
+ position: fixed;
+ }
+ }
+
+ &__card {
+ background: var(--color-background-top);
+ border-radius: var(--border-radius);
+ position: relative;
+ @include z-index('orbit');
+ max-width: calc(100% - var(--spacing-base-unit));
+ box-shadow: var(--box-shadow-base);
+ display: flex;
+ flex-direction: column;
+ will-change: opacity, transform;
+ flex-grow: 0;
+ margin: 5vh 10vw;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: var(--color-text-base);
+ padding: var(--spacing-half-unit);
+ font-weight: var(--font-weight-semibold);
+
+ &__close {
+ opacity: 0.5;
+ &:hover {
+ cursor: pointer;
+ opacity: 1;
+ }
+ svg {
+ display: block;
+ * {
+ fill: currentColor;
+ }
+ }
+ }
+ }
+ &__content {
+ flex-grow: 1;
+ background-color: var(--color-background-light);
+ border-bottom-left-radius: var(--border-radius);
+ border-bottom-right-radius: var(--border-radius);
+ }
+
+ // ANIMATIONS
+
+ &__backdrop {
+ @include animate('fade-in', 1s);
+ }
+ &__card {
+ @include animate('fsb', 0.85s, 0.1s);
+ }
+
+ &--animate-out {
+ pointer-events: none;
+ .modal__backdrop {
+ @include animate('fade-out', 0.85s);
+ }
+ .modal__card {
+ @include animate('fsbr', 0.85s);
+ }
+ }
+}
diff --git a/src/styles/components/widgets/_main.scss b/src/styles/components/widgets/_main.scss
index 7d8ac46b9..b29cfebfa 100644
--- a/src/styles/components/widgets/_main.scss
+++ b/src/styles/components/widgets/_main.scss
@@ -6,4 +6,5 @@
@import 'numeric-input';
@import 'radio-block';
@import 'text-editor';
-@import "rangeslider";
+@import 'rangeslider';
+@import 'trace-type-selector';
diff --git a/src/styles/components/widgets/_trace-type-selector.scss b/src/styles/components/widgets/_trace-type-selector.scss
new file mode 100644
index 000000000..bb1348a69
--- /dev/null
+++ b/src/styles/components/widgets/_trace-type-selector.scss
@@ -0,0 +1,218 @@
+$item-size: 90px;
+.trace-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ @media (max-width: 1000px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ &__column {
+ text-align: center;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ flex-direction: column;
+ flex-shrink: 0;
+ flex-grow: 0;
+
+ &:not(:first-of-type) {
+ position: relative;
+ &::before {
+ position: absolute;
+ width: 1px;
+ border-left: var(--border-light);
+ height: 100%;
+ top: 0;
+ left: 0;
+ content: '';
+ }
+
+ .trace-grid__column__header {
+ position: relative;
+ z-index: 99;
+ }
+ }
+
+ &--double {
+ grid-column: span 2;
+ flex-grow: 0;
+ .trace-grid__column__items {
+ display: grid;
+ grid-gap: 0px;
+ grid-template-columns: repeat(4, 1fr);
+ }
+ }
+
+ &__items {
+ display: grid;
+ grid-gap: 0px;
+ grid-template-columns: repeat(2, $item-size);
+ flex-grow: 1;
+ width: 100%;
+ padding: 0 var(--spacing-half-unit) var(--spacing-base-unit);
+ }
+
+ &__header {
+ text-transform: capitalize;
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-base);
+ text-align: left;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ border-top: var(--border-light);
+ width: 100%;
+ padding: var(--spacing-base-unit) var(--spacing-base-unit) 0;
+ box-sizing: border-box;
+ }
+ }
+}
+
+.trace-item {
+ width: $item-size;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ flex-grow: 0;
+ margin-top: var(--spacing-base-unit);
+ color: var(--color-text-base);
+ @include trans;
+ position: relative;
+
+ &--active {
+ .trace-item__image {
+ border-color: var(--color-accent);
+ border-width: 2px;
+ }
+ }
+
+ &__actions {
+ position: absolute;
+ width: calc(100%);
+ display: flex;
+ flex-direction: column;
+ top: 0;
+ left: var(--spacing-quarter-unit);
+ justify-content: flex-start;
+ align-items: flex-end;
+ z-index: 99;
+
+ &:hover {
+ .trace-item__actions__item {
+ transform: translateX(-2px);
+ opacity: 1;
+ pointer-events: initial;
+ }
+ }
+ &__item {
+ transform: translateX(-10px);
+ opacity: 0;
+ pointer-events: none;
+ color: var(--color-text-light);
+ &:not(:last-child) {
+ margin-bottom: var(--spacing-quarter-unit);
+ }
+ &:hover {
+ color: var(--color-accent);
+ }
+ @include trans;
+ svg {
+ display: block;
+ width: 16px;
+ height: 16px;
+ * {
+ fill: currentColor;
+ }
+ }
+ }
+ }
+
+ &:hover {
+ cursor: pointer;
+ color: var(--color-accent);
+ .trace-item__label {
+ color: var(--color-accent);
+ }
+ .trace-item__image {
+ border-color: var(--color-accent);
+ }
+ }
+ &__image {
+ position: relative;
+ z-index: 2;
+ $size: 60px;
+ border: 1px solid var(--color-border-default);
+ width: $size;
+ height: $size;
+ border-radius: var(--border-radius);
+ background: var(--color-background-top);
+ box-shadow: 0 2px 9px transparent;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ @include trans;
+
+ svg {
+ display: block;
+ * {
+ fill: currentColor;
+ }
+ }
+
+ img {
+ display: block;
+ font-size: 10px;
+ color: var(--color-text-base);
+ }
+ &__wrapper {
+ width: 100%;
+ }
+ }
+ &__label {
+ font-weight: var(--font-weight-semibold);
+ width: $item-size * 0.8;
+ margin-top: var(--spacing-half-unit);
+ color: var(--color-text-base);
+ text-transform: capitalize;
+ font-size: var(--font-size-small);
+ }
+}
+
+.trace-type-select-dropdown__wrapper {
+ & > * {
+ & > * {
+ pointer-events: none;
+ }
+ &:hover {
+ cursor: pointer;
+ .Select:not(.is-open) .Select-control {
+ border-color: var(--color-border-dark);
+ }
+ }
+ }
+}
+
+.trace-type-select-button {
+ display: flex;
+ align-items: center;
+ border: var(--border-default);
+ width: 100%;
+ height: 36px;
+ border-radius: var(--border-radius);
+ padding: 0 var(--spacing-quarter-unit);
+ &:hover {
+ cursor: pointer;
+ border-color: var(--color-border-dark);
+ }
+ &__icon {
+ max-width: 20px;
+ margin-right: var(--spacing-quarter-unit);
+ svg {
+ max-width: 100%;
+ display: block;
+ * {
+ fill: currentColor;
+ }
+ }
+ }
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
index d219226a5..3d5549a11 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -2,6 +2,8 @@
@import 'variables/main';
@import 'mixins';
@import 'helpers';
+@import 'movement';
+@import '~microtip/microtip';
:root {
--env: $ENV;
diff --git a/webpack.config.js b/webpack.config.js
index f7950dd80..e3f91f174 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -30,7 +30,7 @@ module.exports = {
],
},
},
- exclude: /node_modules/,
+ exclude: [/node_modules/],
},
{
test: /\.(css|scss)?$/,