diff --git a/gui/src/components/plotting/PlotAxis.js b/gui/src/components/plotting/PlotAxis.js
index fedb11293b1bef788ca5fe27fe166af3447a90c8..6a925ffcc5712023f0016c3dfbe7ee1a7f127827 100644
--- a/gui/src/components/plotting/PlotAxis.js
+++ b/gui/src/components/plotting/PlotAxis.js
@@ -22,9 +22,6 @@ import { makeStyles } from '@material-ui/core/styles'
 import { isArray, isNil } from 'lodash'
 import { useResizeDetector } from 'react-resize-detector'
 import { getScaler, getTicks } from './common'
-import { Quantity } from '../units/Quantity'
-import { Unit } from '../units/Unit'
-import { useUnitContext } from '../units/UnitContext'
 import { formatNumber, DType } from '../../utils'
 import PlotLabel from './PlotLabel'
 import PlotTick from './PlotTick'
@@ -76,7 +73,6 @@ const usePlotAxisStyles = makeStyles(theme => ({
 const PlotAxis = React.memo(({
   min,
   max,
-  unit,
   dtype,
   mode,
   decimals,
@@ -95,8 +91,6 @@ const PlotAxis = React.memo(({
   classes,
   'data-testid': testID}) => {
   const styles = usePlotAxisStyles(classes)
-  const {units} = useUnitContext()
-  const unitObj = useMemo(() => new Unit(unit), [unit])
   const {height, width, ref} = useResizeDetector()
   const orientation = {
     left: 'vertical',
@@ -111,9 +105,8 @@ const PlotAxis = React.memo(({
 
   // Determine the correct scaler
   const scaler = useMemo(
-    () => getScaler(scale, [min, max], [0, axisSize]),
-    [scale, min, max, axisSize]
-  )
+    () => getScaler(scale, [min, max], [0, axisSize])
+  , [scale, min, max, axisSize])
 
   // Determine styles that depend on overflow values
   overflowBottom = isNil(overflowBottom) ? 8 : overflowBottom
@@ -157,7 +150,7 @@ const PlotAxis = React.memo(({
     const formatTick = (value) => {
       return dtype === DType.Timestamp
         ? format(value, 'MMM d')
-        : formatNumber(new Quantity(value, unitObj).toSystem(units).value(), dtype, mode, decimals)
+        : formatNumber(value, dtype, mode, decimals)
     }
 
     // Manual ticks
@@ -172,11 +165,9 @@ const PlotAxis = React.memo(({
     const nItems = Math.min(labels, Math.max(2, nItemsFit))
 
     // If the scale length is zero, show only one tick
-    const minConverted = new Quantity(min, unitObj).toSystem(units).value()
-    const maxConverted = new Quantity(max, unitObj).toSystem(units).value()
-    if (minConverted === maxConverted) {
+    if (min === max) {
       return [{
-        label: formatTick(minConverted),
+        label: formatTick(min),
         pos: 0
       }]
     }
@@ -184,15 +175,14 @@ const PlotAxis = React.memo(({
     // Get reasonable, human-readable ticks. the .ticks function from d3-scale
     // does not guarantee an upper limit to the number of ticks, so it cannot be
     // directly used.
-    const unitConverted = unitObj.toSystem(units)
-    return getTicks(minConverted, maxConverted, nItems, dtype, mode, decimals)
+    return getTicks(min, max, nItems, dtype, mode, decimals)
       .map(({tick, value}) => {
         return {
           label: tick,
-          pos: scaler(new Quantity(value, unitConverted).toSI().value()) / axisSize
+          pos: scaler(value) / axisSize
         }
       })
-  }, [axisSize, dtype, labelSize, labels, max, min, scaler, unitObj, units, mode, decimals])
+  }, [axisSize, dtype, labelSize, labels, max, min, scaler, mode, decimals])
 
   // Here we estimate the maximum label width. This is a relatively simple
   // approximattion calculated using the font size. A more reliable way would to
@@ -249,7 +239,6 @@ const PlotAxis = React.memo(({
 PlotAxis.propTypes = {
   min: PropTypes.number,
   max: PropTypes.number,
-  unit: PropTypes.any,
   scale: PropTypes.string,
   mode: PropTypes.oneOf(['scientific', 'SI', 'standard']),
   decimals: PropTypes.number,
@@ -276,8 +265,7 @@ PlotAxis.defaultProps = {
   scale: 'linear',
   scientific: true, // Whether to use scientific notation, e.g. 1e+3
   siPostfix: false, // Whether to use SI postfixes, e.g. K, M, B
-  decimals: 3, // How many decimals to show for custom labels
-  unit: 'dimensionless'
+  decimals: 3 // How many decimals to show for custom labels
 }
 
 export default PlotAxis
diff --git a/gui/src/components/plotting/PlotHistogram.js b/gui/src/components/plotting/PlotHistogram.js
index a52074a9a5294ba892477feb663f6805ddfe4e70..245c67e62a86c4ba4b47939a05297bfb190899f1 100644
--- a/gui/src/components/plotting/PlotHistogram.js
+++ b/gui/src/components/plotting/PlotHistogram.js
@@ -19,17 +19,21 @@ import React, { useRef, useMemo, useCallback } from 'react'
 import clsx from 'clsx'
 import { range as rangeLodash, isNil, clamp } from 'lodash'
 import { useRecoilValue } from 'recoil'
-import { Slider } from '@material-ui/core'
+import { Slider, InputAdornment } from '@material-ui/core'
 import { makeStyles } from '@material-ui/core/styles'
-import { pluralize, formatInteger } from '../../utils'
-import { Unit } from '../units/Unit'
+import { KeyboardDateTimePicker } from '@material-ui/pickers'
+import { pluralize, formatInteger, DType } from '../../utils'
+import { Quantity } from '../units/Quantity'
 import InputUnavailable from '../search/input/InputUnavailable'
+import { InputTextField } from '../search/input/InputText'
 import Placeholder from '../visualization/Placeholder'
 import PlotAxis from './PlotAxis'
 import PlotBar from './PlotBar'
+import FilterTitle from '../search/FilterTitle'
 import { guiState } from '../GUIMenu'
 import PropTypes from 'prop-types'
 import { getScaler } from './common'
+import { dateFormat } from '../../config'
 
 /**
  * An interactive histogram for numeric values.
@@ -40,7 +44,20 @@ const useStyles = makeStyles(theme => ({
     height: '100%',
     boxSizing: 'border-box'
   },
+  container: {
+    width: '100%',
+    height: '100%',
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  histogram: {
+    width: '100%',
+    height: '100%'
+  },
   overflow: {
+    flex: '1 1 auto',
     width: '100%',
     height: '100%'
   },
@@ -52,7 +69,7 @@ const useStyles = makeStyles(theme => ({
     gridTemplateColumns: 'auto 1fr',
     gridTemplateRows: '1fr auto',
     paddingRight: theme.spacing(0.75),
-    paddingBottom: theme.spacing(0.5)
+    paddingBottom: theme.spacing(0.25)
   },
   plot: {
     gridColumn: 2,
@@ -85,6 +102,16 @@ const useStyles = makeStyles(theme => ({
     gridColumn: 2,
     gridRow: 2
   },
+  inputField: {
+    marginTop: 0,
+    marginBottom: 0,
+    flexGrow: 1,
+    minWidth: '6.1rem',
+    maxWidth: '8.5rem'
+  },
+  inputFieldDate: {
+    maxWidth: '15rem'
+  },
   thumb: {
     '&:active': {
       boxShadow: '0px 0px 0px 12px rgb(0, 141, 195, 16%)'
@@ -92,19 +119,47 @@ const useStyles = makeStyles(theme => ({
     '&:focusVisible': {
       boxShadow: '0px 0px 0px 6px rgb(0, 141, 195, 16%)'
     }
+  },
+  title: {
+    flexGrow: 1,
+    marginLeft: theme.spacing(0.5),
+    marginRight: theme.spacing(0.5)
+  },
+  // The following two styles are needed in order for the TextInput to
+  // transition from having a label to not having a label. The MUI component
+  // does not otherwise transition correctly.
+  inputMargin: {
+    paddingTop: 10,
+    paddingBottom: 11
+  },
+  adornment: {
+    marginTop: '0px !important'
+  },
+  spacer: {
+    height: '3rem',
+    flex: '1 1 100%',
+    paddingLeft: '18px',
+    paddingRight: '18px',
+    display: 'flex',
+    alignItems: 'center'
+  },
+  row: {
+    marginTop: theme.spacing(0.25),
+    width: '100%',
+    display: 'flex',
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'flex-start'
   }
 }))
 const PlotHistogram = React.memo(({
+  xAxis,
   bins,
   range,
-  minX,
-  maxX,
-  unitX,
   step,
   nBins,
   scale,
   discretization,
-  dtypeX,
   dtypeY,
   disabled,
   tooltipLabel,
@@ -112,11 +167,27 @@ const PlotHistogram = React.memo(({
   disableSlider,
   onRangeChange,
   onRangeCommit,
+  onMinChange,
+  onMaxChange,
+  onMinBlur,
+  onMaxBlur,
+  onMinSubmit,
+  onMaxSubmit,
   onClick,
   minXInclusive,
   maxXInclusive,
   className,
   classes,
+  showinput,
+  minError,
+  maxError,
+  minInput,
+  maxInput,
+  minLocal,
+  maxLocal,
+  stepSlider,
+  disableHistogram,
+  disableXTitle,
   'data-testid': testID
 }) => {
   const styles = useStyles(classes)
@@ -140,8 +211,17 @@ const PlotHistogram = React.memo(({
   const oldRangeRef = useRef()
   const artificialRange = 1
   const isArtificial = !step && bins?.length === 1
+  const isTime = xAxis.dtype === DType.Timestamp
   step = isArtificial ? artificialRange / nBins : step
-  maxX = isArtificial ? minX + artificialRange : maxX
+
+  // Determine the final maximum of x-axis. If the values are discrete, the
+  // maximum x value needs to be increased.
+  let maxX = isArtificial ? xAxis.min + artificialRange : xAxis.max
+  if (discretization) {
+    if (!isNil(maxX)) {
+      maxX += step
+    }
+  }
 
   // Determine the final bin data and the y axis limits.
   const [finalBins, minY, maxY] = useMemo(() => {
@@ -176,15 +256,9 @@ const PlotHistogram = React.memo(({
       : range
   }, [discretization, step])
 
-  // If the values are discrete, the maximum x value needs to be increased.
   const rangeInternal = useMemo(() => {
     return toInternalRange([range?.gte, range?.lte])
   }, [range, toInternalRange])
-  if (discretization) {
-    if (!isNil(maxX)) {
-      maxX += step
-    }
-  }
 
   // Turns the internal range values back to the external counterparts before
   // sending the event.
@@ -240,14 +314,14 @@ const PlotHistogram = React.memo(({
 
   // Create the plot once items are ready
   const plot = useMemo(() => {
-    if (isNil(finalBins) || isNil(minX) || isNil(maxX)) {
+    if (isNil(finalBins) || isNil(xAxis.min) || isNil(maxX)) {
       return null
     }
     return <div className={styles.canvas}>
       {Object.values(finalBins).map((item, i) => {
         return <PlotBar
-          startX={(item.start - minX) / (maxX - minX)}
-          endX={(item.end - minX) / (maxX - minX)}
+          startX={(item.start - xAxis.min) / (maxX - xAxis.min)}
+          endX={(item.end - xAxis.min) / (maxX - xAxis.min)}
           startY={0}
           endY={item.scale}
           key={i}
@@ -259,11 +333,11 @@ const PlotHistogram = React.memo(({
         />
       })}
     </div>
-  }, [finalBins, minX, maxX, styles.canvas, calculateSelection, rangeInternal, handleClick, tooltipLabel])
+  }, [finalBins, xAxis.min, maxX, styles.canvas, calculateSelection, rangeInternal, handleClick, tooltipLabel])
 
   // Create x-axis once axis range is ready
   const xaxis = useMemo(() => {
-    if (isNil(finalBins) || isNil(minX) || isNil(maxX)) {
+    if (isNil(finalBins) || isNil(xAxis.min) || isNil(maxX)) {
       return null
     }
 
@@ -280,7 +354,7 @@ const PlotHistogram = React.memo(({
       }]
     // Discrete values get label at the center of the bin.
     } else {
-      const start = step * Math.ceil(minX / step)
+      const start = step * Math.ceil(xAxis.min / step)
       const end = step * Math.floor(maxX / step)
       labels = rangeLodash(start, end).map(x => ({
         label: x,
@@ -288,19 +362,120 @@ const PlotHistogram = React.memo(({
       }))
     }
 
+    const min = new Quantity(xAxis.min, xAxis.unitStorage).to(xAxis.unit).value()
+    const max = new Quantity(maxX, xAxis.unitStorage).to(xAxis.unit).value()
+
     return <PlotAxis
       placement="bottom"
-      min={minX}
-      max={maxX}
-      unit={unitX}
+      min={min}
+      max={max}
       mode="scientific"
       labels={labels}
       labelWidth={45}
       overflowLeft={25}
-      dtype={dtypeX}
+      dtype={xAxis.dtype}
       className={styles.xaxis}
     />
-  }, [finalBins, minX, maxX, discretization, isArtificial, unitX, dtypeX, styles.xaxis, step])
+  }, [finalBins, xAxis.min, xAxis.unitStorage, maxX, discretization, isArtificial, xAxis.dtype, styles.xaxis, xAxis.unit, step])
+
+  const [minAdornment, maxAdornment] = useMemo(() => {
+    return disableHistogram
+      ? [undefined, undefined]
+      : [
+      {
+        startAdornment: <InputAdornment
+          position="start"
+          classes={{positionStart: styles.adornment}}
+        >min:</InputAdornment>,
+        classes: {inputMarginDense: styles.inputMargin}
+      },
+      {
+        startAdornment: <InputAdornment
+          position="start"
+          classes={{positionStart: styles.adornment}}
+        >max:</InputAdornment>,
+        classes: {inputMarginDense: styles.inputMargin}
+      }
+    ]
+  }, [disableHistogram, styles])
+
+  // Determine the min input component
+  let inputMinField
+  if (xAxis.dtype === DType.Timestamp) {
+    inputMinField = <KeyboardDateTimePicker
+      error={!!minError}
+      disabled={disabled}
+      helperText={minError}
+      ampm={false}
+      className={clsx(styles.inputField, styles.inputFieldDate)}
+      variant="inline"
+      inputVariant="filled"
+      label="Start time"
+      format={`${dateFormat} kk:mm`}
+      value={minInput}
+      invalidDateMessage=""
+      InputAdornmentProps={{ position: 'end' }}
+      onAccept={(date) => {
+        onMinChange(date)
+        onMinSubmit(date)
+      }}
+      onChange={onMinChange}
+      onBlur={onMinBlur}
+      onKeyDown={(event) => { if (event.key === 'Enter') { onMinSubmit(minInput) } }}
+    />
+  } else {
+    inputMinField = <InputTextField
+      error={!!minError}
+      disabled={disabled}
+      helperText={minError}
+      className={styles.inputField}
+      InputProps={minAdornment}
+      label={disableHistogram ? 'min' : ' '}
+      value={minInput}
+      onChange={onMinChange}
+      onBlur={onMinBlur}
+      onKeyDown={(event) => { if (event.key === 'Enter') { onMinSubmit(minInput) } }}
+    />
+  }
+
+  // Determine the max input component
+  let inputMaxField
+  if (xAxis.dtype === DType.Timestamp) {
+    inputMaxField = <KeyboardDateTimePicker
+      error={!!maxError}
+      disabled={disabled}
+      helperText={maxError}
+      ampm={false}
+      className={clsx(styles.inputField, styles.inputFieldDate)}
+      variant="inline"
+      inputVariant="filled"
+      label="End time"
+      format={`${dateFormat} kk:mm`}
+      value={maxInput}
+      invalidDateMessage=""
+      InputAdornmentProps={{ position: 'end' }}
+      onAccept={(date) => {
+        onMaxChange(date)
+        onMaxSubmit(date)
+      }}
+      onChange={onMaxChange}
+      onBlur={onMaxBlur}
+      onKeyDown={(event) => { if (event.key === 'Enter') { onMaxSubmit(maxInput) } }}
+    />
+  } else {
+    inputMaxField = <InputTextField
+      error={!!maxError}
+      disabled={disabled}
+      helperText={maxError}
+      className={styles.inputField}
+      InputProps={maxAdornment}
+      label={disableHistogram ? 'max' : ' '}
+      value={maxInput}
+      onChange={onMaxChange}
+      onBlur={onMaxBlur}
+      onKeyDown={(event) => { if (event.key === 'Enter') { onMaxSubmit(maxInput) } }}
+    />
+  }
 
   // Create y-axis once axis range is ready.
   const yaxis = useMemo(() => {
@@ -311,7 +486,6 @@ const PlotHistogram = React.memo(({
       placement="left"
       min={minY}
       max={maxY}
-      unit={new Unit('dimensionless')}
       mode='SI'
       labels={5}
       scale={scale}
@@ -338,9 +512,9 @@ const PlotHistogram = React.memo(({
           {plot}
           {!disableSlider &&
             <Slider
-              disabled={disabled || isArtificial || (maxX - minX) === discretization}
+              disabled={disabled || isArtificial || (maxX - xAxis.min) === discretization}
               color={highlight ? 'secondary' : 'primary'}
-              min={minX}
+              min={xAxis.min}
               max={maxX}
               step={step}
               value={rangeInternal}
@@ -355,31 +529,67 @@ const PlotHistogram = React.memo(({
         <div className={styles.square} />
         {xaxis}
       </div>
-    </div>
+  </div>
   }
 
+  const titleComp = <div className={styles.title}>
+    <FilterTitle
+      quantity={xAxis.quantity}
+      label={xAxis.title}
+      unit={xAxis.unit}
+      variant="caption"
+      className={styles.titletext}
+      noWrap={false}
+    />
+  </div>
+
   return <div className={clsx(className, styles.root)} data-testid={testID}>
-    {histComp}
+    <div className={styles.container}>
+      {!disableHistogram && <div className={clsx(styles.histogram, classes?.histogram)}>{histComp}</div>}
+      {!disableXTitle && titleComp}
+      <div className={styles.row}>
+        {showinput
+          ? <>
+            {inputMinField}
+            {(disableHistogram && !isTime)
+              ? <div className={styles.spacer}>
+                  <Slider
+                    disabled={disabled || (minLocal === maxLocal)}
+                    color={highlight ? 'secondary' : 'primary'}
+                    min={minLocal}
+                    max={maxLocal}
+                    step={stepSlider}
+                    value={[range.gte, range.lte]}
+                    onChange={onRangeChange}
+                    onChangeCommitted={onRangeCommit}
+                    valueLabelDisplay="off"
+                  />
+                </div>
+              : <div className={styles.title} />
+            }
+            {inputMaxField}
+            </>
+          : null
+        }
+      </div>
+    </div>
   </div>
 })
 
 PlotHistogram.propTypes = {
+  xAxis: PropTypes.object,
   /* The bins data to show. */
   bins: PropTypes.arrayOf(PropTypes.shape({
     value: PropTypes.number,
     count: PropTypes.number
   })),
   range: PropTypes.object,
-  minX: PropTypes.number,
-  maxX: PropTypes.number,
-  unitX: PropTypes.any,
   step: PropTypes.number,
   /* The number of bins: required only when showing a dummy bin for histograms
    * that have no width. */
   nBins: PropTypes.number,
   /* Discretization of the values. */
   discretization: PropTypes.number,
-  dtypeX: PropTypes.string,
   dtypeY: PropTypes.string,
   scale: PropTypes.string,
   disabled: PropTypes.bool,
@@ -395,8 +605,25 @@ PlotHistogram.propTypes = {
   minXInclusive: PropTypes.bool,
   /* Whether the max slider is inclusive (=max value is included) */
   maxXInclusive: PropTypes.bool,
+  showInput: PropTypes.bool,
   onRangeChange: PropTypes.func,
   onRangeCommit: PropTypes.func,
+  onMinChange: PropTypes.func,
+  onMaxChange: PropTypes.func,
+  onMinSubmit: PropTypes.func,
+  onMaxSubmit: PropTypes.func,
+  showinput: PropTypes.bool,
+  maxError: PropTypes.any,
+  minError: PropTypes.any,
+  maxInput: PropTypes.any,
+  minInput: PropTypes.any,
+  maxLocal: PropTypes.any,
+  minLocal: PropTypes.any,
+  stepSlider: PropTypes.any,
+  disableHistogram: PropTypes.bool,
+  disableXTitle: PropTypes.bool,
+  onMinBlur: PropTypes.func,
+  onMaxBlur: PropTypes.func,
   className: PropTypes.string,
   classes: PropTypes.object,
   'data-testid': PropTypes.string
diff --git a/gui/src/components/plotting/common.js b/gui/src/components/plotting/common.js
index 751850cc5ae513e8c12dad9377fc5bc683ea775a..50fb1a8a9476f7f9ddd09dbc8df954fc247233d7 100644
--- a/gui/src/components/plotting/common.js
+++ b/gui/src/components/plotting/common.js
@@ -131,6 +131,13 @@ export function getTicks(min, max, n, dtype, mode = 'scientific', decimals = 3)
 
     // Config for the available intervals
     const intervals = {
+      fiveyears: {
+        difference: (end, start) => differenceInYears(end, start) / 5,
+        split: (interval) => eachYearOfInterval(interval).filter(x => {
+          return !(x.getFullYear() % 5)
+        }),
+        format: 'yyyy'
+      },
       years: {
         difference: differenceInYears,
         split: eachYearOfInterval,
diff --git a/gui/src/components/search/Filter.js b/gui/src/components/search/Filter.js
index fd7f917c5e243ac91c01606a067af7c9574b5a43..6460b1a8aa23521581d9b0a1806ebff72b68d0a7 100644
--- a/gui/src/components/search/Filter.js
+++ b/gui/src/components/search/Filter.js
@@ -165,7 +165,7 @@ export class Filter {
     this.serializerPretty = params?.serializerPretty || getSerializer(this.dtype, true)
     this.deserializer = params?.deserializer || getDeserializer(this.dtype, this.dimension)
     this.aggregatable = params?.aggregatable === undefined ? false : params?.aggregatable
-    this.widget = params?.widget || getWidgetConfig(this.dtype, params?.aggregatable)
+    this.widget = params?.widget || getWidgetConfig(this.quantity, this.dtype, params?.aggregatable, this.scale)
 
     if (this.default && !this.global) {
       throw Error(`Error constructing filter for ${this.name}: only filters that do not correspond to a metainfo value may have default values set.`)
@@ -202,38 +202,36 @@ export function getEnumOptions(quantity, exclude = ['not processed']) {
  * @param {bool} aggregatable Whether the quantity is aggregatable
  * @returns A widget config object.
  */
-export const getWidgetConfig = (dtype, aggregatable) => {
+export const getWidgetConfig = (quantity, dtype, aggregatable, scale) => {
   if (dtype === DType.Float || dtype === DType.Int || dtype === DType.Timestamp) {
-    return histogramWidgetConfig
+    return {
+      x: {quantity},
+      type: 'histogram',
+      scale,
+      showinput: false,
+      autorange: false,
+      nbins: 30,
+      layout: {
+        sm: {w: 8, h: 3, minW: 3, minH: 3},
+        md: {w: 8, h: 3, minW: 3, minH: 3},
+        lg: {w: 8, h: 3, minW: 3, minH: 3},
+        xl: {w: 8, h: 3, minW: 3, minH: 3},
+        xxl: {w: 8, h: 3, minW: 3, minH: 3}
+      }
+    }
   } else if (aggregatable) {
-    return termsWidgetConfig
-  }
-}
-
-export const histogramWidgetConfig = {
-  type: 'histogram',
-  scale: 'linear',
-  showinput: false,
-  autorange: false,
-  nbins: 30,
-  layout: {
-    sm: {w: 8, h: 3, minW: 8, minH: 3},
-    md: {w: 8, h: 3, minW: 8, minH: 3},
-    lg: {w: 8, h: 3, minW: 8, minH: 3},
-    xl: {w: 8, h: 3, minW: 8, minH: 3},
-    xxl: {w: 8, h: 3, minW: 8, minH: 3}
-  }
-}
-
-export const termsWidgetConfig = {
-  type: 'terms',
-  scale: 'linear',
-  showinput: false,
-  layout: {
-    sm: {w: 6, h: 9, minW: 6, minH: 9},
-    md: {w: 6, h: 9, minW: 6, minH: 9},
-    lg: {w: 6, h: 9, minW: 6, minH: 9},
-    xl: {w: 6, h: 9, minW: 6, minH: 9},
-    xxl: {w: 6, h: 9, minW: 6, minH: 9}
+    return {
+      quantity,
+      type: 'terms',
+      scale: scale,
+      showinput: false,
+      layout: {
+        sm: {w: 6, h: 9, minW: 3, minH: 3},
+        md: {w: 6, h: 9, minW: 3, minH: 3},
+        lg: {w: 6, h: 9, minW: 3, minH: 3},
+        xl: {w: 6, h: 9, minW: 3, minH: 3},
+        xxl: {w: 6, h: 9, minW: 3, minH: 3}
+      }
+    }
   }
 }
diff --git a/gui/src/components/search/FilterChip.js b/gui/src/components/search/FilterChip.js
index 2336d81e4c23d4969de2097479e4164110f132f4..7c0382b4f5ae14a74eed9fa4eb7d609e7bec7d47 100644
--- a/gui/src/components/search/FilterChip.js
+++ b/gui/src/components/search/FilterChip.js
@@ -106,7 +106,7 @@ export const FilterChipGroup = React.memo(({
     <FilterTitle
       quantity={quantity}
       variant="caption"
-      className={styles.title}
+      classes={{text: styles.title}}
     />
     <div className={styles.paper}>
       {children}
diff --git a/gui/src/components/search/FilterRegistry.js b/gui/src/components/search/FilterRegistry.js
index efe62049845f8b531f7fac6615c5d593e1e13a51..4b358d9450a9175b792495f0ef7b2eee8d5b01ed 100644
--- a/gui/src/components/search/FilterRegistry.js
+++ b/gui/src/components/search/FilterRegistry.js
@@ -158,18 +158,6 @@ function registerFilterOptions(name, group, target, label, description, options)
   )
 }
 
-const ptWidgetConfig = {
-  type: 'periodictable',
-  scale: '1/2',
-  layout: {
-    sm: {w: 12, h: 8, minW: 12, minH: 8},
-    md: {w: 12, h: 8, minW: 12, minH: 8},
-    lg: {w: 12, h: 8, minW: 12, minH: 8},
-    xl: {w: 12, h: 8, minW: 12, minH: 8},
-    xxl: {w: 12, h: 8, minW: 12, minH: 8}
-  }
-}
-
 // Presets for different kind of quantities
 const termQuantity = {aggs: {terms: {size: 5}}}
 const termQuantityLarge = {aggs: {terms: {size: 10}}}
@@ -642,7 +630,18 @@ registerFilter(
   'results.material.elements',
   idElements,
   {
-    widget: ptWidgetConfig,
+    widget: {
+      quantity: 'results.material.elements',
+      type: 'periodictable',
+      scale: '1/2',
+      layout: {
+        sm: {w: 12, h: 8, minW: 12, minH: 8},
+        md: {w: 12, h: 8, minW: 12, minH: 8},
+        lg: {w: 12, h: 8, minW: 12, minH: 8},
+        xl: {w: 12, h: 8, minW: 12, minH: 8},
+        xxl: {w: 12, h: 8, minW: 12, minH: 8}
+      }
+    },
     aggs: {terms: {size: elementData.elements.length}},
     value: {
       set: (newQuery, oldQuery, value) => {
diff --git a/gui/src/components/search/FilterTitle.js b/gui/src/components/search/FilterTitle.js
index 0ca7ee1a902c19c497f57bbba3d28f2bee991e2a..2fb098425a479aba8af3ef16038a6c43f495cf02 100644
--- a/gui/src/components/search/FilterTitle.js
+++ b/gui/src/components/search/FilterTitle.js
@@ -33,6 +33,8 @@ import { useUnitContext } from '../units/UnitContext'
 const useStaticStyles = makeStyles(theme => ({
   root: {
   },
+  text: {
+  },
   title: {
     fontWeight: 600,
     color: theme.palette.grey[800]
@@ -64,9 +66,10 @@ const FilterTitle = React.memo(({
   className,
   classes,
   rotation,
-  disableUnit
+  disableUnit,
+  noWrap
 }) => {
-  const styles = useStaticStyles({classes: classes})
+  const styles = useStaticStyles({classes})
   const { filterData } = useSearchContext()
   const sectionContext = useContext(inputSectionContext)
   const {units} = useUnitContext()
@@ -93,14 +96,14 @@ const FilterTitle = React.memo(({
   const finalDescription = description || filterData[quantity]?.description || ''
 
   return <Tooltip title={finalDescription} placement="bottom" {...(TooltipProps || {})}>
-    <div className={clsx(
+    <div className={clsx(className, styles.root,
       rotation === 'right' && styles.right,
       rotation === 'down' && styles.down,
       rotation === 'up' && styles.up
     )}>
       <Typography
-        noWrap
-        className={clsx(className, styles.root, (!section) && styles.title)}
+        noWrap={noWrap}
+        className={clsx(styles.text, (!section) && styles.title)}
         variant={variant}
         onMouseDown={onMouseDown}
         onMouseUp={onMouseUp}
@@ -114,7 +117,7 @@ const FilterTitle = React.memo(({
 FilterTitle.propTypes = {
   quantity: PropTypes.string,
   label: PropTypes.string,
-  unit: PropTypes.string,
+  unit: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
   description: PropTypes.string,
   variant: PropTypes.string,
   className: PropTypes.string,
@@ -123,12 +126,14 @@ FilterTitle.propTypes = {
   disableUnit: PropTypes.bool,
   TooltipProps: PropTypes.object, // Properties forwarded to the Tooltip
   onMouseDown: PropTypes.func,
-  onMouseUp: PropTypes.func
+  onMouseUp: PropTypes.func,
+  noWrap: PropTypes.bool
 }
 
 FilterTitle.defaultProps = {
   variant: 'body2',
-  rotation: 'right'
+  rotation: 'right',
+  noWrap: true
 }
 
 export default FilterTitle
diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js
index f00f48615e2af21be9b5eb15eec64589949862dc..6610728cd6af6223e040f029c9f9fb78fd594f0e 100644
--- a/gui/src/components/search/SearchContext.js
+++ b/gui/src/components/search/SearchContext.js
@@ -54,7 +54,6 @@ import {
   formatTimestamp,
   getDeep,
   formatNumber,
-  setToArray,
   parseQuantityName,
   rsplit,
   parseOperator,
@@ -1985,9 +1984,9 @@ function convertQueryGUIToAPI(query, resource, filtersData, queryModes) {
   // Create the API-compatible keys and values.
   const queryNormalized = {}
   for (const [k, v] of Object.entries(queryCustomized)) {
-    const newValue = convertQuerySingleGUIToAPI(v)
     const {quantity: filterName, op: queryMode} = parseOperator(k)
     let finalKey = filtersData[filterName]?.requestQuantity || filterName
+    const newValue = convertQuerySingleGUIToAPI(v, filterName, filtersData)
     finalKey = resource === 'materials' ? getFilterMaterialPath(finalKey) : finalKey
     let finalQueryMode = queryMode || queryModes?.[k]
     if (isNil(finalQueryMode) && isArray(newValue)) {
@@ -2045,54 +2044,44 @@ function convertQueryGUIToAPI(query, resource, filtersData, queryModes) {
 /**
  * Cleans a single filter value into a form that is supported by the API. This includes:
  * - Sets are transformed into Arrays
- * - Quantities are converted to SI values.
+ * - Quantities are converted to storage unit values
  * - Empty containers are set to undefined
  *
- * @param {string} key Filter name
  * @param {any} value Filter value
- * @param {object} filtersData All of the filters that are available
- * @param {string} queryMode Determines the queryMode
+ * @param {Filter} filter Filter object
  *
  * @returns {any} The filter value in a format that is suitable for the API.
  */
-function convertQuerySingleGUIToAPI(value) {
-  // Determine the API-compatible value.
-  let newValue
+function convertQuerySingleGUIToAPI(value, name, filterData) {
+  const unit = filterData[name]?.unit || 'dimensionless'
+  const convertItem = (item) => item instanceof Quantity ? item.to(unit).value() : item
+
   if (value instanceof Set) {
-    newValue = setToArray(value)
-    if (newValue.length === 0) {
-      newValue = undefined
-    } else {
-      newValue = newValue.map((item) => item instanceof Quantity
-        ? item.toSI().value()
-        : item)
-    }
-  } else if (value instanceof Quantity) {
-    newValue = value.toSI().value()
-  } else if (isArray(value)) {
-    if (value.length === 0) {
-      newValue = undefined
-    } else {
-      newValue = value.map((item) => item instanceof Quantity
-        ? item.toSI().value()
-        : item)
-    }
-  } else if (isPlainObject(value)) {
-    newValue = {}
-    for (const [keyInner, valueInner] of Object.entries(value)) {
-      const apiValue = convertQuerySingleGUIToAPI(valueInner)
-      if (!isNil(apiValue)) {
-        newValue[keyInner] = apiValue
-      }
-    }
-    if (isEmpty(newValue)) {
-      newValue = undefined
-    }
-  } else {
-    newValue = value
+    const newValue = Array.from(value).map(convertItem)
+    return newValue.length ? newValue : undefined
   }
 
-  return newValue
+  if (value instanceof Quantity) {
+    return value.to(unit).value()
+  }
+
+  if (isArray(value)) {
+    const newValue = value.map(convertItem)
+    return newValue.length ? newValue : undefined
+  }
+
+  if (isPlainObject(value)) {
+    const newValue = Object.entries(value).reduce((acc, [key, val]) => {
+      const filterName = isPlainObject(val) ? `${name}.${key}` : name
+      const apiValue = convertQuerySingleGUIToAPI(val, filterName, filterData)
+      if (!isNil(apiValue)) acc[key] = apiValue
+      return acc
+    }, {})
+
+    return isEmpty(newValue) ? undefined : newValue
+  }
+
+  return value
 }
 
 /**
diff --git a/gui/src/components/search/conftest.spec.js b/gui/src/components/search/conftest.spec.js
index a1495cc60d951ba5e17ce7afec27ac2eca425bcc..3dba928ee3c5fbc2f4f081ae5eecb07f34e4ef73 100644
--- a/gui/src/components/search/conftest.spec.js
+++ b/gui/src/components/search/conftest.spec.js
@@ -169,14 +169,14 @@ export async function expectWidgetScatterPlot(widget, loaded, colorTitle, legend
 
 /**
  * Tests that an InputRange is rendered with the given contents.
- * @param {string} quantity The quantity name
+ * @param {object} widget The widget config
  * @param {bool} loaded Whether the data is already loaded
  * @param {bool} histogram Whether the histogram is shown
  * @param {bool} placeholder Whether the placeholder should be checked
  */
-export async function expectInputRange(quantity, loaded, histogram, anchored, min, max, root = screen) {
+export async function expectInputRange(widget, loaded, histogram, anchored, min, max, root = screen) {
     // Test header
-    await expectInputHeader(quantity, true)
+    await expectInputHeader(widget.x.quantity, true)
 
     // Check histogram
     if (histogram) {
@@ -188,14 +188,14 @@ export async function expectInputRange(quantity, loaded, histogram, anchored, mi
 
     // Test text elements if the component is not anchored
     if (!anchored) {
-      const data = defaultFilterData[quantity]
+      const data = defaultFilterData[widget.x.quantity]
       const dtype = data.dtype
       if (dtype === DType.Timestamp) {
         expect(root.getByText('Start time')).toBeInTheDocument()
         expect(root.getByText('End time')).toBeInTheDocument()
       } else {
-        expect(root.getByText('min')).toBeInTheDocument()
-        expect(root.getByText('max')).toBeInTheDocument()
+        expect(root.getByText(histogram ? 'min:' : 'min')).toBeInTheDocument()
+        expect(root.getByText(histogram ? 'max:' : 'max')).toBeInTheDocument()
       }
 
       // Get the formatted datetime in current timezone (timezones differ, so the
diff --git a/gui/src/components/search/input/InputMetainfo.js b/gui/src/components/search/input/InputMetainfo.js
index ae9304eacc7096603855f9c18b03e88ec9a6ada2..4263809e382c36a2f98a490e84e712fd23f3c2b1 100644
--- a/gui/src/components/search/input/InputMetainfo.js
+++ b/gui/src/components/search/input/InputMetainfo.js
@@ -82,12 +82,12 @@ export const InputMetainfoControlled = React.memo(({
     if (optional && empty) {
       return {valid: true, error: undefined}
     } else if (empty) {
-      return {valid: false, error: 'Please specify a value'}
+      return {valid: false, error: 'Please specify a value.'}
     }
     if (validate) {
       return validate(value)
     } else if (!(keysSet.has(value))) {
-      return {valid: false, error: 'Invalid value for this field'}
+      return {valid: false, error: `The quantity "${value}" is not available.`}
     }
     return {valid: true, error: undefined}
   }, [validate, keysSet, optional, disableValidation])
@@ -425,9 +425,10 @@ function getMetainfoOptions(filterData, dtypes, dtypesRepeatable, disableNonAggr
       .filter(([key, data]) => {
         if (disableNonAggregatable && !data.aggregatable) return false
         const dtype = data?.dtype
-        return data?.repeats
+        const passed = data?.repeats
           ? dtypesRepeatable?.has(dtype)
           : dtypes?.has(dtype)
+        return passed
       })
       .map(([key, data]) => [key, {
         key: key,
diff --git a/gui/src/components/search/input/InputRange.js b/gui/src/components/search/input/InputRange.js
index 433fca03f3f1f87ca02f095841ecd1c3ee92f4a7..01b6a37c04f868b20a8e028516ffb7399fe8507d 100644
--- a/gui/src/components/search/input/InputRange.js
+++ b/gui/src/components/search/input/InputRange.js
@@ -17,26 +17,19 @@
  */
 import React, { useState, useMemo, useCallback, useEffect, useRef, useContext } from 'react'
 import { makeStyles } from '@material-ui/core/styles'
-import { Slider } from '@material-ui/core'
-import { useRecoilValue } from 'recoil'
 import PropTypes from 'prop-types'
 import clsx from 'clsx'
 import { isNil } from 'lodash'
-import { KeyboardDateTimePicker } from '@material-ui/pickers'
 import InputHeader from './InputHeader'
-import InputTooltip from './InputTooltip'
 import { inputSectionContext } from './InputSection'
-import { InputTextField } from './InputText'
 import { Quantity } from '../../units/Quantity'
 import { Unit } from '../../units/Unit'
 import { useUnitContext } from '../../units/UnitContext'
 import { DType, formatNumber } from '../../../utils'
 import { getInterval } from '../../plotting/common'
-import { dateFormat } from '../../../config'
 import { useSearchContext } from '../SearchContext'
 import PlotHistogram from '../../plotting/PlotHistogram'
 import { isValid, getTime } from 'date-fns'
-import { guiState } from '../../GUIMenu'
 import { ActionCheckbox } from '../../Actions'
 import { autorangeDescription } from '../widgets/WidgetHistogram'
 
@@ -50,69 +43,16 @@ const useStyles = makeStyles(theme => ({
     height: '100%'
   },
   histogram: {
-  },
-  inputFieldText: {
-    marginTop: 0,
-    marginBottom: 0,
-    flexGrow: 0,
-    flexShrink: 0,
-    flexBasis: '6.1rem'
-  },
-  inputFieldDate: {
-    marginTop: 0,
-    marginBottom: 0,
-    flexGrow: 1
-  },
-  textInput: {
-    textOverflow: 'ellipsis'
-  },
-  container: {
-    width: '100%'
-  },
-  spacer: {
-    height: '3rem',
-    flex: '1 1 100%',
-    paddingLeft: '18px',
-    paddingRight: '18px',
-    display: 'flex',
-    alignItems: 'center'
-  },
-  column: {
-    width: '100%',
-    height: '100%',
-    display: 'flex',
-    flexDirection: 'column'
-  },
-  dash: {
-    height: '56px',
-    lineHeight: '56px',
-    textAlign: 'center',
-    paddingLeft: theme.spacing(1),
-    paddingRight: theme.spacing(1)
-  },
-  row: {
-    marginTop: theme.spacing(0.5),
-    width: '100%',
-    display: 'flex',
-    justifyContent: 'space-between',
-    alignItems: 'flex-start'
-  },
-  thumb: {
-    '&:active': {
-      boxShadow: '0px 0px 0px 12px rgb(0, 141, 195, 16%)'
-    },
-    '&:focusVisible': {
-      boxShadow: '0px 0px 0px 6px rgb(0, 141, 195, 16%)'
-    }
   }
 }))
 export const Range = React.memo(({
-  quantity,
+  xAxis,
   nSteps,
   visible,
   scale,
   nBins,
   disableHistogram,
+  disableXTitle,
   autorange,
   showinput,
   aggId,
@@ -120,16 +60,15 @@ export const Range = React.memo(({
   classes,
   'data-testid': testID
 }) => {
-  const {units} = useUnitContext()
   const {filterData, useAgg, useFilterState, useIsStatisticsEnabled} = useSearchContext()
   const sectionContext = useContext(inputSectionContext)
   const repeats = sectionContext?.repeats
   const isStatisticsEnabled = useIsStatisticsEnabled()
   const styles = useStyles({classes})
-  const [filter, setFilter] = useFilterState(quantity)
+  const [filter, setFilter] = useFilterState(xAxis.quantity)
   const [minLocal, setMinLocal] = useState()
   const [maxLocal, setMaxLocal] = useState()
-  const [plotData, setPlotData] = useState()
+  const [plotData, setPlotData] = useState({xAxis})
   const loading = useRef(false)
   const firstRender = useRef(true)
   const validRange = useRef()
@@ -142,20 +81,13 @@ export const Range = React.memo(({
   const [minInclusive, setMinInclusive] = useState(true)
   const [maxInclusive, setMaxInclusive] = useState(true)
   const highlight = Boolean(filter)
-  const inputVariant = useRecoilValue(guiState('inputVariant'))
   disableHistogram = isNil(disableHistogram) ? !isStatisticsEnabled : disableHistogram
 
   // Determine the description and units
-  const def = filterData[quantity]
-  const unitSI = useMemo(() => {
-    return new Unit(def?.unit || 'dimensionless')
-  }, [def])
-  const unitCurrent = useMemo(() => {
-    return unitSI.toSystem(units)
-  }, [unitSI, units])
-  const dtype = filterData[quantity].dtype
-  const discretization = dtype === DType.Int ? 1 : undefined
-  const isTime = dtype === DType.Timestamp
+  const def = filterData[xAxis.quantity]
+  const unitStorage = useMemo(() => { return new Unit(def?.unit || 'dimensionless') }, [def])
+  const discretization = xAxis.dtype === DType.Int ? 1 : undefined
+  const isTime = xAxis.dtype === DType.Timestamp
   const firstLoad = useRef(true)
 
   // We need to set a valid initial input state: otherwise the component thinks
@@ -163,36 +95,40 @@ export const Range = React.memo(({
   const [minInput, setMinInput] = useState(isTime ? new Date() : '')
   const [maxInput, setMaxInput] = useState(isTime ? new Date() : '')
 
-  // Function for converting filter values into SI values used by the API.
-  const fromFilter = useCallback(filter => {
+  // Function for converting filter values into storage units used by the API.
+  const fromDisplayUnit = useCallback(filter => {
     return filter instanceof Quantity
-      ? filter.toSI().value()
+      ? filter.to(unitStorage).value()
       : filter
-  }, [])
+  }, [unitStorage])
 
-  // Function for converting filter values from SI.
-  const fromSI = useCallback(filter => {
+  // Function for converting filter values from storage unit to display unit
+  const fromStorageUnit = useCallback(filter => {
     return (isTime)
       ? filter
-      : filter instanceof Quantity ? filter.toSI() : new Quantity(filter, unitSI)
-  }, [unitSI, isTime])
+      : filter instanceof Quantity
+        ? filter.to(unitStorage)
+        : new Quantity(filter, unitStorage)
+  }, [unitStorage, isTime])
 
-  // Function for converting filter values from custom unit system.
-  const toSI = useCallback(filter => {
+  // Function for converting filter values from display unit to storage unit
+  const toStorageUnit = useCallback(filter => {
     return (isTime)
       ? filter
-      : filter instanceof Quantity ? filter.toSI() : new Quantity(filter, unitCurrent).toSI()
-  }, [isTime, unitCurrent])
+      : filter instanceof Quantity
+        ? filter.to(unitStorage)
+        : new Quantity(filter, xAxis.unit).to(unitStorage)
+  }, [isTime, xAxis.unit, unitStorage])
 
   // Aggregation when the statistics are enabled: a histogram aggregation with
   // extended bounds based on the currently set filter range. Note: the config
   // should be memoed in order to prevent re-renders.
-  const minRef = useRef(fromFilter(isNil(filter?.gte) ? filter?.gt : filter?.gte))
-  const maxRef = useRef(fromFilter(isNil(filter?.lte) ? filter?.lt : filter?.lte))
+  const minRef = useRef(fromDisplayUnit(isNil(filter?.gte) ? filter?.gt : filter?.gte))
+  const maxRef = useRef(fromDisplayUnit(isNil(filter?.lte) ? filter?.lt : filter?.lte))
   const aggHistogramConfig = useMemo(() => {
     const filterBounds = (filter) ? {
-      min: fromFilter(isNil(filter.gte) ? filter.gt : filter.gte),
-      max: fromFilter(isNil(filter.lte) ? filter.lt : filter.lte)
+      min: fromDisplayUnit(isNil(filter.gte) ? filter.gt : filter.gte),
+      max: fromDisplayUnit(isNil(filter.lte) ? filter.lt : filter.lte)
     } : undefined
     let exclude_from_search
     let extended_bounds
@@ -227,8 +163,8 @@ export const Range = React.memo(({
     return (isTime || !discretization)
       ? {type: 'histogram', buckets: nBins, exclude_from_search, extended_bounds}
       : {type: 'histogram', interval: discretization, exclude_from_search, extended_bounds}
-  }, [filter, fromFilter, isTime, discretization, nBins, autorange])
-  const agg = useAgg(quantity, visible && !disableHistogram, `${aggId}_histogram`, aggHistogramConfig)
+  }, [filter, fromDisplayUnit, isTime, discretization, nBins, autorange])
+  const agg = useAgg(xAxis.quantity, visible && !disableHistogram, `${aggId}_histogram`, aggHistogramConfig)
   useEffect(() => {
     if (!isNil(agg)) {
       firstLoad.current = false
@@ -238,45 +174,46 @@ export const Range = React.memo(({
   // Aggregation when the statistics are disabled: a simple min_max aggregation
   // is enough in order to get the slider range.
   const aggSliderConfig = useMemo(() => ({type: 'min_max', exclude_from_search: true}), [])
-  const aggSlider = useAgg(quantity, visible && disableHistogram, `${aggId}_slider`, aggSliderConfig)
+  const aggSlider = useAgg(xAxis.quantity, visible && disableHistogram, `${aggId}_slider`, aggSliderConfig)
 
   // Determine the global minimum and maximum values
-  const [minGlobalSI, maxGlobalSI] = useMemo(() => {
-    let minGlobalSI
-    let maxGlobalSI
+  const [minGlobal, maxGlobal] = useMemo(() => {
+    let minGlobal
+    let maxGlobal
     if (disableHistogram) {
-      minGlobalSI = aggSlider?.data?.[0]
-      maxGlobalSI = aggSlider?.data?.[1]
+      minGlobal = aggSlider?.data?.[0]
+      maxGlobal = aggSlider?.data?.[1]
     } else {
       const nBuckets = agg?.data?.length || 0
       if (nBuckets === 1) {
-        minGlobalSI = agg.data[0].value
-        maxGlobalSI = minGlobalSI
+        minGlobal = agg.data[0].value
+        maxGlobal = minGlobal
       } else if (nBuckets > 1) {
         for (const bucket of agg.data) {
-          if (isNil(minGlobalSI)) {
-            minGlobalSI = bucket.value
+          if (isNil(minGlobal)) {
+            minGlobal = bucket.value
           }
-          maxGlobalSI = bucket.value + (discretization ? 0 : agg.interval)
+          maxGlobal = bucket.value + (discretization ? 0 : agg.interval)
         }
-        if (isNil(minGlobalSI)) {
-          minGlobalSI = agg.data[0].value
+        if (isNil(minGlobal)) {
+          minGlobal = agg.data[0].value
         }
-        if (isNil(maxGlobalSI)) {
-          maxGlobalSI = agg.data[agg.data.length - 1].value + (discretization ? 0 : agg.interval)
+        if (isNil(maxGlobal)) {
+          maxGlobal = agg.data[agg.data.length - 1].value + (discretization ? 0 : agg.interval)
         }
       }
     }
     firstRender.current = false
-    return [minGlobalSI, maxGlobalSI]
+    return [minGlobal, maxGlobal]
   }, [agg, aggSlider, disableHistogram, discretization])
 
   const stepHistogram = agg?.interval
-  const unavailable = isNil(minGlobalSI) || isNil(maxGlobalSI) || isNil(range)
+  const unavailable = isNil(minGlobal) || isNil(maxGlobal) || isNil(range)
   const disabled = unavailable
 
   // Determine the step value for sliders. Notice that this does not have to
-  // match with the histogram binning.
+  // match with the histogram binning, and that we want to do call the
+  // getInterval function on the display unit range.
   const stepSlider = useMemo(() => {
     if (discretization) {
       return discretization
@@ -285,15 +222,15 @@ export const Range = React.memo(({
       return undefined
     }
     const rangeSI = maxLocal - minLocal
-    const range = new Quantity(rangeSI, unitSI).toSystem(units).value()
-    const intervalCustom = getInterval(range, nSteps, dtype)
-    return new Quantity(intervalCustom, unitCurrent).toSI().value()
-  }, [maxLocal, minLocal, discretization, nSteps, unitSI, unitCurrent, units, dtype])
+    const range = new Quantity(rangeSI, unitStorage).to(xAxis.unit).value()
+    const intervalCustom = getInterval(range, nSteps, xAxis.dtype)
+    return new Quantity(intervalCustom, xAxis.unit).to(unitStorage).value()
+  }, [maxLocal, minLocal, discretization, nSteps, xAxis.dtype, xAxis.unit, unitStorage])
 
   // When filter changes, the plot data should not be updated.
   useEffect(() => {
-    loading.current = true
-  }, [filter])
+    if (autorange) loading.current = true
+  }, [filter, autorange])
 
   // Once the aggregation data arrives, the plot data can be updated.
   useEffect(() => {
@@ -309,16 +246,29 @@ export const Range = React.memo(({
     if (loading.current || isNil(agg?.data)) {
       return
     }
-    setPlotData({step: stepHistogram, data: agg.data, minX: minLocal, maxX: maxLocal})
-  }, [loading, nBins, agg, minLocal, maxLocal, stepHistogram])
+
+    setPlotData({
+      xAxis: {
+        quantity: xAxis.quantity,
+        unit: xAxis.unit,
+        unitStorage: unitStorage,
+        dtype: xAxis.dtype,
+        title: xAxis.title,
+        min: minLocal,
+        max: maxLocal
+      },
+      step: stepHistogram,
+      data: agg.data
+    })
+  }, [loading, nBins, agg, minLocal, maxLocal, stepHistogram, unitStorage, xAxis.quantity, xAxis.unit, xAxis.dtype, xAxis.title])
 
   // Function for converting search values into the currently selected unit
   // system.
   const toInternal = useCallback(filter => {
-    return (!isTime && unitSI)
-      ? formatNumber(new Quantity(filter, unitSI).toSystem(units).value())
+    return (!isTime && unitStorage)
+      ? formatNumber(new Quantity(filter, unitStorage).to(xAxis.unit).value())
       : filter
-  }, [unitSI, isTime, units])
+  }, [unitStorage, isTime, xAxis.unit])
 
   // If no filter has been specified by the user, the range is automatically
   // adjusted according to global min/max of the field. If filter is set, the
@@ -343,19 +293,19 @@ export const Range = React.memo(({
     const limitMin = (global, filter) => limit(global, filter, true)
     const limitMax = (global, filter) => limit(global, filter, false)
 
-    if (!isNil(minGlobalSI) && !isNil(maxGlobalSI)) {
+    if (!isNil(minGlobal) && !isNil(maxGlobal)) {
       // When no filter is set, use the whole available range
       if (isNil(filter)) {
-        gte = minGlobalSI
-        lte = maxGlobalSI
-        min = minGlobalSI
-        max = maxGlobalSI
+        gte = minGlobal
+        lte = maxGlobal
+        min = minGlobal
+        max = maxGlobal
       // A single specific value is given
       } else if (filter instanceof Quantity) {
-        gte = filter.toSI().value()
-        lte = filter.toSI().value()
-        min = limitMin(minGlobalSI, gte)
-        max = limitMax(maxGlobalSI, lte)
+        gte = filter.to(unitStorage).value()
+        lte = filter.to(unitStorage).value()
+        min = limitMin(minGlobal, gte)
+        max = limitMax(maxGlobal, lte)
       // A range is given. For visualization purposes open-ended queries are
       // displayed as well, although making such queries is currently not
       // supported.
@@ -364,20 +314,20 @@ export const Range = React.memo(({
         maxInc = isNil(filter.lt)
         gte = filter.gte || filter.gt
         lte = filter.lte || filter.lt
-        gte = gte instanceof Quantity ? gte.toSI().value() : gte
-        lte = lte instanceof Quantity ? lte.toSI().value() : lte
+        gte = gte instanceof Quantity ? gte.to(unitStorage).value() : gte
+        lte = lte instanceof Quantity ? lte.to(unitStorage).value() : lte
 
         if (isNil(gte)) {
-          min = limitMin(minGlobalSI, lte)
+          min = limitMin(minGlobal, lte)
           gte = min
         } else {
-          min = limitMin(minGlobalSI, gte)
+          min = limitMin(minGlobal, gte)
         }
         if (isNil(lte)) {
-          max = limitMax(maxGlobalSI, gte)
+          max = limitMax(maxGlobal, gte)
           lte = max
         } else {
-          max = limitMax(maxGlobalSI, lte)
+          max = limitMax(maxGlobal, lte)
         }
       }
       minRef.current = min
@@ -392,7 +342,7 @@ export const Range = React.memo(({
       setMaxError()
       setMinError()
     }
-  }, [minGlobalSI, maxGlobalSI, filter, unitSI, toInternal, units, def, isTime, autorange])
+  }, [minGlobal, maxGlobal, filter, unitStorage, toInternal, def, isTime, autorange])
 
   // Returns whether the given range is an acceptable value to be queried and
   // displayed.
@@ -405,7 +355,7 @@ export const Range = React.memo(({
 
   // Handles changes in the min input
   const handleMinChange = useCallback((value) => {
-    loading.current = true
+    if (autorange) loading.current = true
     let val
     if (isTime) {
       value.setSeconds(0, 0)
@@ -417,10 +367,11 @@ export const Range = React.memo(({
     setMinInput(val)
     setMaxError()
     setMinError()
-  }, [isTime])
+  }, [isTime, autorange])
 
   // Handles changes in the max input
   const handleMaxChange = useCallback((value) => {
+    if (autorange) loading.current = true
     loading.current = true
     let val
     if (isTime) {
@@ -433,7 +384,7 @@ export const Range = React.memo(({
     setMaxInput(val)
     setMaxError()
     setMinError()
-  }, [isTime])
+  }, [isTime, autorange])
 
   // Called when min values are submitted through the input field
   const handleMinSubmit = useCallback((value) => {
@@ -449,7 +400,7 @@ export const Range = React.memo(({
       if (isNaN(number)) {
         error = 'Invalid minimum value.'
       } else {
-        value = toSI(number)
+        value = toStorageUnit(number)
       }
     }
     if ((isTime ? value : value.value) > range.lte) {
@@ -462,11 +413,11 @@ export const Range = React.memo(({
       setFilter(old => {
         return {
           gte: value,
-          lte: fromSI(isNil(old?.lte) ? maxLocal : old.lte)
+          lte: fromStorageUnit(isNil(old?.lte) ? maxLocal : old.lte)
         }
       })
     }
-  }, [isTime, setFilter, fromSI, toSI, maxLocal, range])
+  }, [isTime, setFilter, fromStorageUnit, toStorageUnit, maxLocal, range])
 
   // Called when max values are submitted through the input field
   const handleMaxSubmit = useCallback((value) => {
@@ -482,7 +433,7 @@ export const Range = React.memo(({
       if (isNaN(number)) {
         error = 'Invalid maximum value.'
       } else {
-        value = toSI(number)
+        value = toStorageUnit(number)
       }
     }
     if ((isTime ? value : value.value) < range.gte) {
@@ -494,12 +445,12 @@ export const Range = React.memo(({
       maxInputChanged.current = false
       setFilter(old => {
         return {
-          gte: fromSI(isNil(old?.gte) ? minLocal : old.gte),
+          gte: fromStorageUnit(isNil(old?.gte) ? minLocal : old.gte),
           lte: value
         }
       })
     }
-  }, [isTime, setFilter, fromSI, toSI, minLocal, range])
+  }, [isTime, setFilter, fromStorageUnit, toStorageUnit, minLocal, range])
 
   // Handle range commit: Set the filter when mouse is released on a slider.
   // Notice that we cannot rely on the value given by the slider event: it may
@@ -509,17 +460,17 @@ export const Range = React.memo(({
     const value = validRange.current
     if (!isNil(value) && rangeChanged.current) {
       setFilter({
-        gte: fromSI(value[0]),
-        lte: fromSI(value[1])
+        gte: fromStorageUnit(value[0]),
+        lte: fromStorageUnit(value[1])
       })
       rangeChanged.current = false
     }
-  }, [setFilter, fromSI])
+  }, [setFilter, fromStorageUnit])
 
   // Handle range change: only change the rendered values, filter is send with
   // handleRangeCommit.
   const handleRangeChange = useCallback((event, value, validate = true) => {
-    loading.current = true
+    if (autorange) loading.current = true
     const valid = !validate || validateRange(value)
     if (valid) {
       rangeChanged.current = true
@@ -532,7 +483,7 @@ export const Range = React.memo(({
       setMaxError()
       setMinError()
     }
-  }, [validateRange, validRange, toInternal])
+  }, [validateRange, validRange, toInternal, autorange])
 
   // Handle min input field blur: only if the input has changed, the filter will
   // be submitted.
@@ -546,143 +497,51 @@ export const Range = React.memo(({
     maxInputChanged.current && handleMaxSubmit(maxInput)
   }, [maxInput, handleMaxSubmit, maxInputChanged])
 
-  // Determine the min input component
-  let inputMinField
-  if (dtype === DType.Timestamp) {
-    inputMinField = <KeyboardDateTimePicker
-      error={!!minError}
-      disabled={disabled}
-      helperText={minError}
-      ampm={false}
-      className={styles.inputFieldDate}
-      variant="inline"
-      inputVariant={inputVariant}
-      label="Start time"
-      format={`${dateFormat} kk:mm`}
-      value={minInput}
-      invalidDateMessage=""
-      InputAdornmentProps={{ position: 'end' }}
-      onAccept={(date) => {
-        handleMinChange(date)
-        handleMinSubmit(date)
-      }}
-      onChange={handleMinChange}
-      onBlur={handleMinBlur}
-      onKeyDown={(event) => { if (event.key === 'Enter') { handleMinSubmit(minInput) } }}
-    />
-  } else {
-    inputMinField = <InputTextField
-      error={!!minError}
-      disabled={disabled}
-      helperText={minError}
-      label="min"
-      className={styles.inputFieldText}
-      inputProps={{className: styles.textInput}}
-      value={minInput}
-      margin="normal"
-      onChange={handleMinChange}
-      onBlur={handleMinBlur}
-      onKeyDown={(event) => { if (event.key === 'Enter') { handleMinSubmit(minInput) } }}
-    />
-  }
-
-  // Determine the max input component
-  let inputMaxField
-  if (dtype === DType.Timestamp) {
-    inputMaxField = <KeyboardDateTimePicker
-      error={!!maxError}
+  return <div className={clsx(className, styles.root)} data-testid={testID}>
+    <PlotHistogram
+      bins={plotData?.data}
+      xAxis={plotData?.xAxis}
+      step={plotData?.step}
+      minXInclusive={minInclusive}
+      maxXInclusive={maxInclusive}
       disabled={disabled}
-      helperText={maxError}
-      ampm={false}
-      className={styles.inputFieldDate}
-      variant="inline"
-      inputVariant={inputVariant}
-      label="End time"
-      format={`${dateFormat} kk:mm`}
-      value={maxInput}
-      invalidDateMessage=""
-      InputAdornmentProps={{ position: 'end' }}
-      onAccept={(date) => {
-        handleMaxChange(date)
-        handleMaxSubmit(date)
+      scale={scale}
+      nBins={nBins}
+      range={range}
+      highlight={highlight}
+      discretization={discretization}
+      tooltipLabel={repeats ? 'value' : 'entry'}
+      dtypeY={DType.Int}
+      onRangeChange={handleRangeChange}
+      onRangeCommit={handleRangeCommit}
+      onMinBlur={handleMinBlur}
+      onMaxBlur={handleMaxBlur}
+      onMinChange={handleMinChange}
+      onMaxChange={handleMaxChange}
+      onMinSubmit={handleMinSubmit}
+      onMaxSubmit={handleMaxSubmit}
+      classes={{histogram: classes?.histogram}}
+      onClick={(event, value) => {
+        handleRangeChange(event, value, false)
+        handleRangeCommit(event, value)
       }}
-      onChange={handleMaxChange}
-      onBlur={handleMaxBlur}
-      onKeyDown={(event) => { if (event.key === 'Enter') { handleMaxSubmit(maxInput) } }}
-    />
-  } else {
-    inputMaxField = <InputTextField
-      error={!!maxError}
-      disabled={disabled}
-      helperText={maxError}
-      label="max"
-      className={styles.inputFieldText}
-      value={maxInput}
-      margin="normal"
-      onChange={handleMaxChange}
-      onBlur={handleMaxBlur}
-      onKeyDown={(event) => { if (event.key === 'Enter') { handleMaxSubmit(maxInput) } }}
+      minError={minError}
+      maxError={maxError}
+      minInput={minInput}
+      maxInput={maxInput}
+      minLocal={minLocal}
+      maxLocal={maxLocal}
+      showinput={showinput}
+      stepSlider={stepSlider}
+      disableHistogram={disableHistogram}
+      disableXTitle={disableXTitle}
+      data-testid={`${testID}-histogram`}
     />
-  }
-
-  return <div className={clsx(className, styles.root)} data-testid={testID}>
-    <InputTooltip unavailable={unavailable}>
-      <div className={styles.column}>
-        {!disableHistogram &&
-          <PlotHistogram
-            bins={plotData?.data}
-            disabled={disabled}
-            scale={scale}
-            minX={plotData?.minX}
-            maxX={plotData?.maxX}
-            unitX={unitSI}
-            step={plotData?.step}
-            nBins={nBins}
-            range={range}
-            highlight={highlight}
-            discretization={discretization}
-            tooltipLabel={repeats ? 'value' : 'entry'}
-            dtypeX={dtype}
-            dtypeY={DType.Int}
-            onRangeChange={handleRangeChange}
-            onRangeCommit={handleRangeCommit}
-            className={clsx(styles.histogram)}
-            onClick={(event, value) => {
-              handleRangeChange(event, value, false)
-              handleRangeCommit(event, value)
-            }}
-            minXInclusive={minInclusive}
-            maxXInclusive={maxInclusive}
-            data-testid={`${testID}-histogram`}
-          />
-        }
-        {showinput && <div className={styles.row}>
-          {inputMinField}
-          {disableHistogram && !isTime
-            ? <div className={styles.spacer}>
-              <Slider
-                disabled={disabled || (minLocal === maxLocal)}
-                color={highlight ? 'primary' : 'secondary'}
-                min={minLocal}
-                max={maxLocal}
-                step={stepSlider}
-                value={[range.gte, range.lte]}
-                onChange={handleRangeChange}
-                onChangeCommitted={handleRangeCommit}
-                valueLabelDisplay="off"
-              />
-            </div>
-            : <div className={styles.dash} />
-          }
-          {inputMaxField}
-        </div>}
-      </div>
-    </InputTooltip>
   </div>
 })
 
 Range.propTypes = {
-  quantity: PropTypes.string.isRequired,
+  xAxis: PropTypes.object,
   /* Target number of steps for the slider that is shown when statistics are
    * disabled. The actual number may vary, as the step is chosen to be a
    * human-readable value that depends on the range and the unit. */
@@ -695,6 +554,8 @@ Range.propTypes = {
   scale: PropTypes.string,
   /* Whether the histogram is disabled */
   disableHistogram: PropTypes.bool,
+  /* Whether the x title is disabled */
+  disableXTitle: PropTypes.bool,
   /* Set the range automatically according to data. */
   autorange: PropTypes.bool,
   /* Show the input fields for min and max value */
@@ -739,11 +600,19 @@ const InputRange = React.memo(({
   'data-testid': testID
 }) => {
   const {filterData} = useSearchContext()
+  const {units} = useUnitContext()
   const styles = useInputRangeStyles()
   const [scale, setScale] = useState(initialScale || filterData[quantity].scale)
   const dtype = filterData[quantity].dtype
   const isTime = dtype === DType.Timestamp
   const [autorange, setAutorange] = useState(isNil(initialAutorange) ? isTime : initialAutorange)
+  const x = useMemo(() => (
+    {
+      quantity,
+      dtype,
+      unit: new Unit(filterData[quantity]?.unit || 'dimensionless').toSystem(units)
+    }
+  ), [quantity, filterData, dtype, units])
 
   // Determine the description and title
   const def = filterData[quantity]
@@ -769,12 +638,13 @@ const InputRange = React.memo(({
       actions={actions}
     />
     <Range
-      quantity={quantity}
+      xAxis={x}
       nSteps={nSteps}
       visible={visible}
       scale={scale}
       nBins={nBins}
       disableHistogram={disableHistogram}
+      disableXTitle
       autorange={autorange}
       showinput
       aggId={aggId}
diff --git a/gui/src/components/search/input/InputRange.spec.js b/gui/src/components/search/input/InputRange.spec.js
index cb32bfaca26a1e80c3ac0c08ba87ffce91b0d293..b5da2751d8b0db79c308ad39740a5fae9ddb8826 100644
--- a/gui/src/components/search/input/InputRange.spec.js
+++ b/gui/src/components/search/input/InputRange.spec.js
@@ -47,7 +47,7 @@ describe('test initial state', () => {
     time_histogram
   ])('quantity: %s, histogram: %s', async (quantity, histogram, min, max) => {
     renderSearchEntry(<InputRange visible quantity={quantity} disableHistogram={!histogram}/>)
-    await expectInputRange(quantity, false, histogram, false, min, max)
+    await expectInputRange({x: {quantity}}, false, histogram, false, min, max)
   })
 })
 
diff --git a/gui/src/components/search/widgets/Dashboard.js b/gui/src/components/search/widgets/Dashboard.js
index a33c766b7041af5fa902f6683c95f29bc0d2c476..105ed46fd557134defaeb74ccf1bb5c6a213a027 100644
--- a/gui/src/components/search/widgets/Dashboard.js
+++ b/gui/src/components/search/widgets/Dashboard.js
@@ -39,7 +39,7 @@ import WidgetGrid from './WidgetGrid'
 import { Actions, Action } from '../../Actions'
 import { useSearchContext } from '../SearchContext'
 import { WidgetScatterPlotEdit, schemaWidgetScatterPlot } from './WidgetScatterPlotEdit'
-import { WidgetHistogramEdit, schemaWidgetHistogram } from './WidgetHistogram'
+import { WidgetHistogramEdit, schemaWidgetHistogram } from './WidgetHistogramEdit'
 import { WidgetTermsEdit, schemaWidgetTerms } from './WidgetTerms'
 import { WidgetPeriodicTableEdit, schemaWidgetPeriodicTable } from './WidgetPeriodicTable'
 import InputConfig from '../input/InputConfig'
@@ -255,7 +255,7 @@ const Dashboard = React.memo(() => {
       const comp = {
         scatterplot: <WidgetScatterPlotEdit key={id} widget={value}/>,
         periodictable: <WidgetPeriodicTableEdit key={id} {...value}/>,
-        histogram: <WidgetHistogramEdit key={id} {...value}/>,
+        histogram: <WidgetHistogramEdit key={id} widget={value}/>,
         terms: <WidgetTermsEdit key={id} {...value}/>
       }[value.type]
       return comp || null
diff --git a/gui/src/components/search/widgets/Dashboard.spec.js b/gui/src/components/search/widgets/Dashboard.spec.js
index a273e725141904b015fc2459de03748419c20f29..efe82a3d82a632d89651534549d1d57f12fe8662 100644
--- a/gui/src/components/search/widgets/Dashboard.spec.js
+++ b/gui/src/components/search/widgets/Dashboard.spec.js
@@ -69,7 +69,8 @@ describe('displaying an initial widget and removing it', () => {
       'histogram',
       {
         type: 'histogram',
-        quantity: 'results.material.n_elements',
+        title: 'Test title',
+        x: {quantity: 'results.material.n_elements'},
         scale: 'linear',
         editing: false,
         visible: true,
@@ -81,7 +82,7 @@ describe('displaying an initial widget and removing it', () => {
           xxl: {x: Infinity, y: 0, w: 12, h: 9}
         }
       },
-      async (widget, loaded) => await expectInputRange(widget.quantity, loaded, true, true)
+      async (widget, loaded) => await expectInputRange(widget, loaded, true, true)
     ],
     [
       'scatterplot',
diff --git a/gui/src/components/search/widgets/WidgetGrid.js b/gui/src/components/search/widgets/WidgetGrid.js
index f3b2dcd5831b3613b64e44d6ddd7d312f5f3f9c1..0966315a39b7e7c9d5c690d396097d194a0c3a25 100644
--- a/gui/src/components/search/widgets/WidgetGrid.js
+++ b/gui/src/components/search/widgets/WidgetGrid.js
@@ -178,13 +178,13 @@ const useStyles = makeStyles(theme => {
   return {
     root: {
       position: 'relative',
-      marginLeft: theme.spacing(-1),
-      marginRight: theme.spacing(-1)
+      marginLeft: theme.spacing(-0.75),
+      marginRight: theme.spacing(-0.75)
     },
     component: {
       position: 'absolute',
-      top: theme.spacing(1),
-      bottom: theme.spacing(1.5),
+      top: theme.spacing(0.5),
+      bottom: theme.spacing(1.25),
       left: theme.spacing(1.5),
       right: theme.spacing(1.5),
       height: 'unset',
@@ -195,10 +195,10 @@ const useStyles = makeStyles(theme => {
     },
     containerInner: {
       position: 'absolute',
-      top: theme.spacing(1),
-      bottom: theme.spacing(1),
-      left: theme.spacing(1),
-      right: theme.spacing(1),
+      top: theme.spacing(0.75),
+      bottom: theme.spacing(0.75),
+      left: theme.spacing(0.75),
+      right: theme.spacing(0.75),
       height: 'unset',
       width: 'unset'
     }
@@ -220,14 +220,44 @@ const WidgetGrid = React.memo(({
   const layout = useMemo(() => {
     if (!nCols) return {}
 
+    // If layouts are not provided for all different breakpoints, duplicate the
+    // closest layout that is found. We need to work on copies as the widgets
+    // information is immutable.
+    const widgetCopies = {}
+    for (const [id, widget] of Object.entries(widgets)) {
+      if (!widget.visible) continue
+      if (!widget.layout) {
+        widgetCopies[id] = widget
+        continue
+      }
+      const widgetCopy = cloneDeep(widget)
+      const breakpointNames = Object.keys(breakpoints)
+      for (const i of range(0, breakpointNames.length)) {
+        const breakpoint = breakpointNames[i]
+        if (!widgetCopy.layout[breakpoint]) {
+          for (const j of range(1, breakpointNames.length - 1)) {
+            const breakpointSmaller = widgetCopy.layout[breakpointNames[i - j]]
+            const breakpointBigger = widgetCopy.layout[breakpointNames[i + j]]
+            if (breakpointSmaller) {
+              widgetCopy.layout[breakpoint] = breakpointSmaller
+              break
+            } else if (breakpointBigger) {
+              widgetCopy.layout[breakpoint] = breakpointBigger
+              break
+            }
+          }
+        }
+      }
+      widgetCopies[id] = widgetCopy
+    }
+
     const layouts = {}
     for (const breakpoint of Object.keys(breakpoints)) {
       // This is the layout in the format as react-grid-layout would read it. x:
       // Infinity means that we want to place the item at the very end.
       // Add widgets
       let i = 0
-      let layout = Object.entries(widgets)
-        .filter(([id, value]) => value?.visible)
+      let layout = Object.entries(widgetCopies)
         .map(([id, value]) => {
           const layout = value.layout?.[breakpoint]
           const config = {
@@ -345,7 +375,9 @@ export default WidgetGrid
 const useHandleStyles = makeStyles(theme => {
   return {
     root: {
-      margin: theme.spacing(1.25)
+      margin: theme.spacing(1),
+      width: '25px',
+      height: '25px'
     }
   }
 })
@@ -354,8 +386,11 @@ const ResizeHandle = React.forwardRef((props, ref) => {
   const styles = useHandleStyles()
   return <div
     ref={ref}
-    className={clsx('react-resizable-handle', `react-resizable-handle-${handleAxis}`, styles.root)}
-    style={{width: '30px', height: '30px'}}
+    className={clsx(
+      'react-resizable-handle',
+      `react-resizable-handle-${handleAxis}`,
+      styles.root
+    )}
     {...restProps}
     >
     </div>
diff --git a/gui/src/components/search/widgets/WidgetHistogram.js b/gui/src/components/search/widgets/WidgetHistogram.js
index 9e76e883972cd12031e83082b26558660db265f7..e0fbdf1f3f1a72285f9deb731cb1e19fb6f46484 100644
--- a/gui/src/components/search/widgets/WidgetHistogram.js
+++ b/gui/src/components/search/widgets/WidgetHistogram.js
@@ -15,27 +15,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import React, { useState, useCallback, useMemo } from 'react'
+import React, { useCallback, useMemo } from 'react'
 import PropTypes from 'prop-types'
-import { string, number, bool } from 'yup'
-import {
-  TextField,
-  MenuItem,
-  Checkbox,
-  FormControlLabel
-} from '@material-ui/core'
 import { useSearchContext } from '../SearchContext'
-import { InputMetainfo } from '../input/InputMetainfo'
-import { InputTextField } from '../input/InputText'
-import { Widget, schemaWidget } from './Widget'
+import { Widget } from './Widget'
 import { ActionCheckbox, ActionSelect } from '../../Actions'
-import { WidgetEditDialog, WidgetEditGroup, WidgetEditOption } from './WidgetEdit'
 import { Range } from '../input/InputRange'
-import { DType } from '../../../utils'
 import { scales } from '../../plotting/common'
-
-// Predefined in order to not break memoization
-const dtypes = new Set([DType.Float, DType.Int, DType.Timestamp])
+import {getDisplayLabel} from '../../../utils'
+import { Unit } from '../../units/Unit'
+import { useUnitContext } from '../../units/UnitContext'
 
 /**
  * Displays a histogram widget.
@@ -46,16 +35,34 @@ export const WidgetHistogram = React.memo((
   id,
   title,
   description,
-  quantity,
+  x,
   nbins,
   scale,
   autorange,
   showinput,
   className
 }) => {
-  const { useSetWidget } = useSearchContext()
+  const { filterData, useSetWidget } = useSearchContext()
+  const {units} = useUnitContext()
   const setWidget = useSetWidget(id)
 
+  // Create final axis config for the plot
+  const xAxis = useMemo(() => {
+    const xFilter = filterData[x.quantity]
+    const xTitle = x.title || getDisplayLabel(xFilter)
+    const xType = xFilter?.dtype
+    const xUnit = x.unit
+      ? new Unit(x.unit)
+      : new Unit(xFilter.unit || 'dimensionless').toSystem(units)
+
+    return {
+      ...x,
+      title: xTitle,
+      unit: xUnit,
+      dtype: xType
+    }
+  }, [filterData, x, units])
+
   const handleEdit = useCallback(() => {
     setWidget(old => { return {...old, editing: true } })
   }, [setWidget])
@@ -66,8 +73,7 @@ export const WidgetHistogram = React.memo((
 
   return <Widget
     id={id}
-    quantity={quantity}
-    title={title}
+    title={title || 'Histogram'}
     description={description}
     onEdit={handleEdit}
     className={className}
@@ -87,14 +93,13 @@ export const WidgetHistogram = React.memo((
     </>}
   >
     <Range
-      quantity={quantity}
+      xAxis={xAxis}
       visible={true}
       nBins={nbins}
       scale={scale}
       anchored={true}
       autorange={autorange}
       showinput={showinput}
-      disableHistogram={false}
       aggId={id}
     />
   </Widget>
@@ -104,152 +109,10 @@ WidgetHistogram.propTypes = {
   id: PropTypes.string.isRequired,
   title: PropTypes.string,
   description: PropTypes.string,
-  quantity: PropTypes.string,
+  x: PropTypes.object,
   nbins: PropTypes.number,
   scale: PropTypes.string,
   autorange: PropTypes.bool,
   showinput: PropTypes.bool,
   className: PropTypes.string
 }
-
-/**
- * A dialog that is used to configure a scatter plot widget.
- */
-export const WidgetHistogramEdit = React.memo((props) => {
-    const {id, editing, visible} = props
-    const { useSetWidget } = useSearchContext()
-    const [settings, setSettings] = useState(props)
-    const [errors, setErrors] = useState({})
-    const setWidget = useSetWidget(id)
-    const hasError = useMemo(() => {
-      return Object.values(errors).some((d) => !!d) || !schemaWidgetHistogram.isValidSync(settings)
-    }, [errors, settings])
-
-    const handleSubmit = useCallback((settings) => {
-      setWidget(old => ({...old, ...settings}))
-    }, [setWidget])
-
-    const handleChange = useCallback((key, value) => {
-      setSettings(old => ({...old, [key]: value}))
-    }, [setSettings])
-
-    const handleError = useCallback((key, value) => {
-      setErrors(old => ({...old, [key]: value}))
-    }, [setErrors])
-
-    const handleAccept = useCallback((key, value) => {
-      try {
-        schemaWidgetHistogram.validateSyncAt(key, {[key]: value})
-      } catch (e) {
-        handleError(key, e.message)
-        return
-      }
-      setErrors(old => ({...old, [key]: undefined}))
-      setSettings(old => ({...old, [key]: value}))
-    }, [handleError, setSettings])
-
-    const handleClose = useCallback(() => {
-      setWidget(old => ({...old, editing: false}))
-    }, [setWidget])
-
-    const handleEditAccept = useCallback(() => {
-      handleSubmit({...settings, editing: false, visible: true})
-    }, [handleSubmit, settings])
-
-    return <WidgetEditDialog
-        id={id}
-        open={editing}
-        visible={visible}
-        title="Edit histogram widget"
-        onClose={handleClose}
-        onAccept={handleEditAccept}
-        error={hasError}
-      >
-      <WidgetEditGroup title="x axis">
-        <WidgetEditOption>
-          <InputMetainfo
-            label="quantity"
-            value={settings.quantity}
-            error={errors.quantity}
-            onChange={(value) => handleChange('quantity', value)}
-            onSelect={(value) => handleAccept('quantity', value)}
-            onError={(value) => handleError('quantity', value)}
-            dtypes={dtypes}
-            dtypesRepeatable={dtypes}
-          />
-        </WidgetEditOption>
-        <WidgetEditOption>
-          <TextField
-            select
-            fullWidth
-            label="Statistics scaling"
-            variant="filled"
-            value={settings.scale}
-            onChange={(event) => { handleChange('scale', event.target.value) }}
-          >
-            {Object.keys(scales).map((key) =>
-              <MenuItem value={key} key={key}>{key}</MenuItem>
-            )}
-          </TextField>
-        </WidgetEditOption>
-        <WidgetEditOption>
-          <TextField
-            select
-            fullWidth
-            label="Maximum number of bins"
-            variant="filled"
-            value={settings.nbins}
-            onChange={(event) => { handleChange('nbins', event.target.value) }}
-          >
-            <MenuItem value={10}>10</MenuItem>
-            <MenuItem value={20}>20</MenuItem>
-            <MenuItem value={30}>30</MenuItem>
-            <MenuItem value={40}>40</MenuItem>
-            <MenuItem value={50}>50</MenuItem>
-          </TextField>
-        </WidgetEditOption>
-      </WidgetEditGroup>
-      <WidgetEditGroup title="general">
-        <WidgetEditOption>
-          <InputTextField
-            label="title"
-            fullWidth
-            value={settings?.title}
-            onChange={(event) => handleChange('title', event.target.value)}
-          />
-        </WidgetEditOption>
-        <WidgetEditOption>
-          <FormControlLabel
-            control={<Checkbox checked={settings.autorange} onChange={(event, value) => handleChange('autorange', value)}/>}
-            label={autorangeDescription}
-          />
-        </WidgetEditOption>
-        <WidgetEditOption>
-          <FormControlLabel
-            control={<Checkbox checked={settings.showinput} onChange={(event, value) => handleChange('showinput', value)}/>}
-            label='Show input fields'
-          />
-        </WidgetEditOption>
-      </WidgetEditGroup>
-    </WidgetEditDialog>
-})
-
-WidgetHistogramEdit.propTypes = {
-  id: PropTypes.string.isRequired,
-  editing: PropTypes.bool,
-  visible: PropTypes.bool,
-  quantity: PropTypes.string,
-  scale: PropTypes.string,
-  nbins: PropTypes.number,
-  autorange: PropTypes.bool,
-  showinput: PropTypes.bool,
-  onClose: PropTypes.func
-}
-
-export const schemaWidgetHistogram = schemaWidget.shape({
-  quantity: string().required('Quantity is required.'),
-  scale: string().required('Scale is required.'),
-  nbins: number().integer().required(),
-  autorange: bool(),
-  showinput: bool()
-})
diff --git a/gui/src/components/search/widgets/WidgetHistogramEdit.js b/gui/src/components/search/widgets/WidgetHistogramEdit.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e557923b9a967d438a72e3fdfa4587e8252bb01
--- /dev/null
+++ b/gui/src/components/search/widgets/WidgetHistogramEdit.js
@@ -0,0 +1,220 @@
+/*
+ * Copyright The NOMAD Authors.
+ *
+ * This file is part of NOMAD. See https://nomad-lab.eu for further info.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React, { useState, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import { string, number, bool, reach } from 'yup'
+import { cloneDeep } from 'lodash'
+import {
+  TextField,
+  MenuItem,
+  Checkbox,
+  FormControlLabel
+} from '@material-ui/core'
+import { useSearchContext } from '../SearchContext'
+import { InputMetainfo } from '../input/InputMetainfo'
+import { InputTextField } from '../input/InputText'
+import UnitInput from '../../units/UnitInput'
+import { schemaWidget, schemaAxis } from './Widget'
+import { WidgetEditDialog, WidgetEditGroup, WidgetEditOption } from './WidgetEdit'
+import { DType, parseJMESPath, setDeep, isEmptyString } from '../../../utils'
+import { scales } from '../../plotting/common'
+import { autorangeDescription } from './WidgetHistogram'
+
+// Predefined in order to not break memoization
+const dtypes = new Set([DType.Float, DType.Int, DType.Timestamp])
+
+/**
+ * A dialog that is used to configure a scatter plot widget.
+ */
+export const WidgetHistogramEdit = React.memo(({widget}) => {
+    const { filterData, useSetWidget } = useSearchContext()
+    const [settings, setSettings] = useState(cloneDeep(widget))
+    const [errors, setErrors] = useState({})
+    const [dimensions, setDimensions] = useState({})
+    const setWidget = useSetWidget(widget.id)
+
+    const handleChange = useCallback((key, value) => {
+      setSettings(old => {
+        const newValue = {...old}
+        setDeep(newValue, key, value)
+        return newValue
+      })
+    }, [setSettings])
+
+    const handleError = useCallback((key, value) => {
+      setErrors(old => ({...old, [key]: value}))
+    }, [setErrors])
+
+    const handleErrorQuantity = useCallback((key, value) => {
+      handleError(key, value)
+      setDimensions((old) => ({...old, [key]: null}))
+    }, [handleError])
+
+    const handleAccept = useCallback((key, value) => {
+      try {
+        reach(schemaWidgetHistogram, key).validateSync(value)
+      } catch (e) {
+        handleError(key, e.message)
+        return
+      }
+      setErrors(old => ({...old, [key]: undefined}))
+      handleChange(key, value)
+    }, [handleError, handleChange])
+
+    const handleAcceptQuantity = useCallback((key, value) => {
+      handleAccept(key, value)
+      const { quantity } = parseJMESPath(value)
+      const dimension = filterData[quantity]?.dimension
+      setDimensions((old) => ({...old, [key]: dimension}))
+    }, [handleAccept, filterData])
+
+    const handleClose = useCallback(() => {
+      setWidget(old => ({...old, editing: false}))
+    }, [setWidget])
+
+    // Upon accepting the entire form, we perform final validation.
+    const handleEditAccept = useCallback(() => {
+      const independentErrors = Object.values(errors).some(x => !!x)
+      if (independentErrors) return
+
+      // Check for missing values. This check is required because there is no
+      // value set when a new widget is created, and pressing the done button
+      // without filling a value should raise an error.
+      const xEmpty = isEmptyString(settings?.x?.quantity)
+      if (xEmpty) {
+        handleErrorQuantity('x.quantity', 'Please specify a value.')
+      }
+
+      if (!independentErrors && !xEmpty) {
+        setWidget(old => ({...old, ...{...settings, editing: false, visible: true}}))
+      }
+    }, [settings, setWidget, errors, handleErrorQuantity])
+
+    return <WidgetEditDialog
+        id={widget.id}
+        open={widget.editing}
+        visible={widget.visible}
+        title="Edit histogram widget"
+        onClose={handleClose}
+        onAccept={handleEditAccept}
+      >
+      <WidgetEditGroup title="x axis">
+        <WidgetEditOption>
+          <InputMetainfo
+            label="quantity"
+            value={settings.x?.quantity}
+            error={errors['x.quantity']}
+            onChange={(value) => handleChange('x.quantity', value)}
+            onAccept={(value) => handleAcceptQuantity('x.quantity', value)}
+            onSelect={(value) => handleAcceptQuantity('x.quantity', value)}
+            onError={(value) => handleErrorQuantity('x.quantity', value)}
+            dtypes={dtypes}
+            dtypesRepeatable={dtypes}
+          />
+        </WidgetEditOption>
+        <WidgetEditOption>
+          <InputTextField
+            label="title"
+            fullWidth
+            value={settings.x?.title}
+            onChange={(event) => handleChange('x.title', event.target.value)}
+          />
+        </WidgetEditOption>
+        <WidgetEditOption>
+          <UnitInput
+            label='unit'
+            value={settings.x?.unit}
+            onChange={(value) => handleChange('x.unit', value)}
+            onSelect={(value) => handleAccept('x.unit', value)}
+            onAccept={(value) => handleAccept('x.unit', value)}
+            error={errors['x.unit']}
+            onError={(value) => handleError('x.unit', value)}
+            dimension={dimensions['x.quantity'] || null}
+            optional
+            disableGroup
+          />
+        </WidgetEditOption>
+      </WidgetEditGroup>
+      <WidgetEditGroup title="general">
+        <WidgetEditOption>
+          <InputTextField
+            label="title"
+            fullWidth
+            value={settings?.title}
+            onChange={(event) => handleChange('title', event.target.value)}
+          />
+        </WidgetEditOption>
+        <WidgetEditOption>
+          <TextField
+            select
+            fullWidth
+            label="Statistics scaling"
+            variant="filled"
+            value={settings.scale}
+            onChange={(event) => { handleChange('scale', event.target.value) }}
+          >
+            {Object.keys(scales).map((key) =>
+              <MenuItem value={key} key={key}>{key}</MenuItem>
+            )}
+          </TextField>
+        </WidgetEditOption>
+        <WidgetEditOption>
+          <TextField
+            select
+            fullWidth
+            label="Maximum number of bins"
+            variant="filled"
+            value={settings.nbins}
+            onChange={(event) => { handleChange('nbins', event.target.value) }}
+          >
+            <MenuItem value={10}>10</MenuItem>
+            <MenuItem value={20}>20</MenuItem>
+            <MenuItem value={30}>30</MenuItem>
+            <MenuItem value={40}>40</MenuItem>
+            <MenuItem value={50}>50</MenuItem>
+          </TextField>
+        </WidgetEditOption>
+
+        <WidgetEditOption>
+          <FormControlLabel
+            control={<Checkbox checked={settings.autorange} onChange={(event, value) => handleChange('autorange', value)}/>}
+            label={autorangeDescription}
+          />
+        </WidgetEditOption>
+        <WidgetEditOption>
+          <FormControlLabel
+            control={<Checkbox checked={settings.showinput} onChange={(event, value) => handleChange('showinput', value)}/>}
+            label='Show input fields'
+          />
+        </WidgetEditOption>
+      </WidgetEditGroup>
+    </WidgetEditDialog>
+})
+
+WidgetHistogramEdit.propTypes = {
+  widget: PropTypes.object,
+  onClose: PropTypes.func
+}
+
+export const schemaWidgetHistogram = schemaWidget.shape({
+  x: schemaAxis.required('Quantity for the x axis is required.'),
+  scale: string().required('Scale is required.'),
+  nbins: number().integer().required(),
+  autorange: bool(),
+  showinput: bool()
+})
diff --git a/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js b/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c449b450099ae759c9062ac1adc5b3a41900a667
--- /dev/null
+++ b/gui/src/components/search/widgets/WidgetHistogramEdit.spec.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright The NOMAD Authors.
+ *
+ * This file is part of NOMAD. See https://nomad-lab.eu for further info.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react'
+import userEvent from '@testing-library/user-event'
+import { screen } from '../../conftest.spec'
+import { renderSearchEntry } from '../conftest.spec'
+import { WidgetHistogramEdit } from './WidgetHistogramEdit'
+
+describe('test edit dialog error messages', () => {
+  test.each([
+    ['missing x', {x: {}}, 'Please specify a value.'],
+    ['unavailable x', {x: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'],
+    ['invalid x unit', {x: {quantity: 'results.material.topology.cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'],
+    ['incompatible x unit', {x: {quantity: 'results.material.topology.cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".']
+  ])('%s', async (name, config, error) => {
+    const finalConfig = {
+      id: '0',
+      editing: true,
+      ...config
+    }
+    renderSearchEntry(<WidgetHistogramEdit widget={finalConfig} />)
+    const button = screen.getByText('Done')
+    await userEvent.click(button)
+    screen.getByText(error)
+  })
+})
diff --git a/gui/src/components/search/widgets/WidgetScatterPlotEdit.js b/gui/src/components/search/widgets/WidgetScatterPlotEdit.js
index 8c43207d265055bdcc0ba3257067bb80122d9ec9..909a3506467b5d28a684e8d9572a7f6eea57ab95 100644
--- a/gui/src/components/search/widgets/WidgetScatterPlotEdit.js
+++ b/gui/src/components/search/widgets/WidgetScatterPlotEdit.js
@@ -28,7 +28,7 @@ import { schemaWidget, schemaAxis, schemaMarkers } from './Widget'
 import { WidgetEditDialog, WidgetEditGroup, WidgetEditOption, WidgetEditSelect } from './WidgetEdit'
 import { useSearchContext } from '../SearchContext'
 import { autorangeDescription } from './WidgetHistogram'
-import { DType, setDeep, parseJMESPath } from '../../../utils'
+import { DType, setDeep, parseJMESPath, isEmptyString } from '../../../utils'
 import { InputTextField } from '../input/InputText'
 import UnitInput from '../../units/UnitInput'
 
@@ -40,9 +40,6 @@ const nPointsOptions = {
   1000: 1000,
   10000: 10000
 }
-function isEmptyString(value) {
-  return value === undefined || value === null || !value?.trim?.()?.length
-}
 /**
  * A dialog that is used to configure a scatter plot widget.
  */
@@ -99,16 +96,16 @@ export const WidgetScatterPlotEdit = React.memo(({widget}) => {
       const independentErrors = Object.values(errors).some(x => !!x)
       if (independentErrors) return
 
-      // Check for missing values: TODO: This kind of check should be replaced
-      // by a dedicated form context. Inputs could automatically register into
-      // this form to enable imperative validation etc.
+      // Check for missing values. This check is required because there is no
+      // value set when a new widget is created, and pressing the done button
+      // without filling a value should raise an error.
       const xEmpty = isEmptyString(settings?.x?.quantity)
       if (xEmpty) {
-        handleErrorQuantity('x.quantity', 'Please specify a value')
+        handleErrorQuantity('x.quantity', 'Please specify a value.')
       }
       const yEmpty = isEmptyString(settings?.y?.quantity)
       if (yEmpty) {
-        handleErrorQuantity('y.quantity', 'Please specify a value')
+        handleErrorQuantity('y.quantity', 'Please specify a value.')
       }
 
       if (!independentErrors && !xEmpty && !yEmpty) {
diff --git a/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js b/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js
index 04382e408e777565031aabbde5fce54232c96a2f..7f8d458f4a66f0c8583d276634dc8e4b1c8a6f82 100644
--- a/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js
+++ b/gui/src/components/search/widgets/WidgetScatterPlotEdit.spec.js
@@ -23,8 +23,8 @@ import { WidgetScatterPlotEdit } from './WidgetScatterPlotEdit'
 
 describe('test edit dialog error messages', () => {
   test.each([
-    ['missing x', {x: {quantity: 'results.material.n_elements'}}, 'Please specify a value'],
-    ['missing y', {y: {quantity: 'results.material.n_elements'}}, 'Please specify a value'],
+    ['missing x', {x: {quantity: 'results.material.n_elements'}}, 'Please specify a value.'],
+    ['missing y', {y: {quantity: 'results.material.n_elements'}}, 'Please specify a value.'],
     ['unavailable x', {x: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'],
     ['unavailable y', {y: {quantity: 'results.material.not_a_quantity'}}, 'The quantity "results.material.not_a_quantity" is not available.'],
     ['unavailable color', {markers: {color: {quantity: 'results.material.not_a_quantity'}}}, 'The quantity "results.material.not_a_quantity" is not available.'],
@@ -37,9 +37,9 @@ describe('test edit dialog error messages', () => {
     ['invalid x unit', {x: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'],
     ['invalid y unit', {y: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}, 'Unit "nounit" not found.'],
     ['invalid color unit', {markers: {color: {quantity: 'results.material.topology[0].cell.a', unit: 'nounit'}}}, 'Unit "nounit" not found.'],
-    ['incompatible x unit', {x: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length"'],
-    ['incompatible y unit', {y: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length"'],
-    ['incompatible color unit', {markers: {color: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}}, 'Unit "joule" is incompatible with dimension "length"']
+    ['incompatible x unit', {x: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'],
+    ['incompatible y unit', {y: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}, 'Unit "joule" is incompatible with dimension "length".'],
+    ['incompatible color unit', {markers: {color: {quantity: 'results.material.topology[0].cell.a', unit: 'joule'}}}, 'Unit "joule" is incompatible with dimension "length".']
   ])('%s', async (name, config, error) => {
     const finalConfig = {
       id: '0',
diff --git a/gui/src/components/search/widgets/WidgetToggle.js b/gui/src/components/search/widgets/WidgetToggle.js
index 8f9666ca88d9ce596e64a2beb96a04c1e472744b..b017ce9465d7783006b1958907d0089c68557347 100644
--- a/gui/src/components/search/widgets/WidgetToggle.js
+++ b/gui/src/components/search/widgets/WidgetToggle.js
@@ -49,9 +49,7 @@ const WidgetToggle = React.memo(({quantity, disabled, 'data-testid': testID}) =>
         id: quantity,
         editing: false,
         visible: true,
-        quantity: quantity,
-        ...cloneDeep(widgetDefault),
-        scale: filterData[quantity].scale
+        ...cloneDeep(widgetDefault)
       }
       if (hasWidget) {
         removeWidget(quantity)
diff --git a/gui/src/components/units/Quantity.js b/gui/src/components/units/Quantity.js
index 5b0e44996f29dbdeb85c0ab5d7b6d4a9777c6766..4926ffac61d2db5651f833bf38f5d3cb7fabb6ed 100644
--- a/gui/src/components/units/Quantity.js
+++ b/gui/src/components/units/Quantity.js
@@ -137,7 +137,7 @@ export function parseQuantity(input, dimension = 'dimensionless', requireValue =
     valueString = undefined
     value = undefined
     if (requireValue) {
-       error = 'Enter a valid numerical value'
+       error = 'Enter a valid numerical value.'
     }
   } else {
     value = Number(valueString)
@@ -146,7 +146,7 @@ export function parseQuantity(input, dimension = 'dimensionless', requireValue =
   // Check unit if required
   if (requireUnit) {
     if (unitString === '') {
-      return {valueString, value, error: 'Unit is required'}
+      return {valueString, value, error: 'Unit is required.'}
     }
   }
 
@@ -180,7 +180,7 @@ export function parseQuantity(input, dimension = 'dimensionless', requireValue =
   // units are compared.
   if (dimension !== null) {
     if (!(unit.dimension(true) === dimension || unit.dimension(false) === dimension)) {
-      error = `Unit "${unit.label(false)}" is incompatible with dimension "${dimension}"`
+      error = `Unit "${unit.label(false)}" is incompatible with dimension "${dimension}".`
     }
   }
 
diff --git a/gui/src/components/units/Quantity.spec.js b/gui/src/components/units/Quantity.spec.js
index 205326066f8ed5d12e9952d05eb89b776b0470ee..8179c40a9f212118887ede8c054a7e6c384b3b1a 100644
--- a/gui/src/components/units/Quantity.spec.js
+++ b/gui/src/components/units/Quantity.spec.js
@@ -128,9 +128,9 @@ test.each([
   ['unit only', 'joule', null, false, true, {valueString: undefined, value: undefined, unit: new Unit('joule')}],
   ['number and unit with dimension', '100 joule', 'energy', true, true, {valueString: '100', value: 100, unit: new Unit('joule')}],
   ['number and unit without dimension', '100 joule', null, true, true, {valueString: '100', value: 100, unit: new Unit('joule')}],
-  ['incorrect dimension', '100 joule', 'length', true, true, {valueString: '100', value: 100, unit: new Unit('joule'), error: 'Unit "joule" is incompatible with dimension "length"'}],
-  ['missing unit', '100', 'length', true, true, {valueString: '100', value: 100, unit: undefined, error: 'Unit is required'}],
-  ['missing value', 'joule', 'energy', true, true, {valueString: undefined, value: undefined, unit: new Unit('joule'), error: 'Enter a valid numerical value'}],
+  ['incorrect dimension', '100 joule', 'length', true, true, {valueString: '100', value: 100, unit: new Unit('joule'), error: 'Unit "joule" is incompatible with dimension "length".'}],
+  ['missing unit', '100', 'length', true, true, {valueString: '100', value: 100, unit: undefined, error: 'Unit is required.'}],
+  ['missing value', 'joule', 'energy', true, true, {valueString: undefined, value: undefined, unit: new Unit('joule'), error: 'Enter a valid numerical value.'}],
   ['mixing number and quantity #1', '1 / joule', 'energy^-1', false, false, {valueString: '1', value: 1, unit: new Unit('1 / joule')}],
   ['mixing number and quantity #2', '100 / joule', 'energy^-1', false, false, {valueString: '100', value: 100, unit: new Unit('1 / joule')}]
 
diff --git a/gui/src/utils.js b/gui/src/utils.js
index c1633cbb6a8e3998adb31d21cdb3fba4f9f0214d..7b9ec7ae672c1715539c81c0966d3be415c815ef 100644
--- a/gui/src/utils.js
+++ b/gui/src/utils.js
@@ -1766,3 +1766,13 @@ export function getDisplayLabel(def, isArchive = false, technicalView = false) {
     : def.name.replace(/_/g, ' ')
   return eln?.[0].label || def?.more?.label || def?.label || (isArchive ? name : capitalize(name))
 }
+
+/**
+ * Checks if the given string is empty: undefined, null or contains only
+ * whitespace.
+ * @param {*} value The value to check
+ * @returns Whether the string is empty
+ */
+export function isEmptyString(value) {
+  return value === undefined || value === null || !value?.trim?.()?.length
+}
diff --git a/gui/tests/env.js b/gui/tests/env.js
index 51abd35805964ab2c973aba1dec8ce0ceca1418f..bb7c2061a3ca797aab88405bd9c5ed32966b7257 100644
--- a/gui/tests/env.js
+++ b/gui/tests/env.js
@@ -2065,7 +2065,9 @@ window.nomadEnv = {
                     "minW": 3
                   }
                 },
-                "quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity",
+                "x": {
+                  "quantity": "results.properties.optoelectronic.solar_cell.illumination_intensity"
+                },
                 "scale": "1/4",
                 "autorange": true,
                 "showinput": true,
@@ -2164,7 +2166,9 @@ window.nomadEnv = {
                     "minW": 8
                   }
                 },
-                "quantity": "results.properties.electronic.band_structure_electronic.band_gap.value",
+                "x": {
+                  "quantity": "results.properties.electronic.band_structure_electronic.band_gap.value"
+                },
                 "scale": "1/4",
                 "autorange": false,
                 "showinput": false,
@@ -2521,7 +2525,9 @@ window.nomadEnv = {
                     "minW": 3
                   }
                 },
-                "quantity": "results.material.topology.pore_limiting_diameter",
+                "x": {
+                  "quantity": "results.material.topology.pore_limiting_diameter"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": true,
@@ -2571,7 +2577,9 @@ window.nomadEnv = {
                     "minW": 3
                   }
                 },
-                "quantity": "results.material.topology.largest_cavity_diameter",
+                "x": {
+                  "quantity": "results.material.topology.largest_cavity_diameter"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": true,
@@ -2621,7 +2629,9 @@ window.nomadEnv = {
                     "minW": 3
                   }
                 },
-                "quantity": "results.material.topology.accessible_surface_area",
+                "x": {
+                  "quantity": "results.material.topology.accessible_surface_area"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": true,
@@ -2671,7 +2681,9 @@ window.nomadEnv = {
                     "minW": 3
                   }
                 },
-                "quantity": "results.material.topology.void_fraction",
+                "x": {
+                  "quantity": "results.material.topology.void_fraction"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": true,
@@ -3200,7 +3212,9 @@ window.nomadEnv = {
                     "minW": 8
                   }
                 },
-                "quantity": "results.properties.catalytic.reaction.weight_hourly_space_velocity",
+                "x": {
+                  "quantity": "results.properties.catalytic.reaction.weight_hourly_space_velocity"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": false,
@@ -3309,7 +3323,9 @@ window.nomadEnv = {
                     "minW": 8
                   }
                 },
-                "quantity": "results.properties.catalytic.reaction.pressure",
+                "x": {
+                  "quantity": "results.properties.catalytic.reaction.pressure"
+                },
                 "scale": "linear",
                 "autorange": false,
                 "showinput": false,
@@ -3597,7 +3613,9 @@ window.nomadEnv = {
                     "minW": 8
                   }
                 },
-                "quantity": "results.properties.catalytic.catalyst_characterization.surface_area",
+                "x": {
+                  "quantity": "results.properties.catalytic.catalyst_characterization.surface_area"
+                },
                 "scale": "1/4",
                 "autorange": false,
                 "showinput": false,
diff --git a/nomad/config/models/ui.py b/nomad/config/models/ui.py
index 0c9d98f86cd90cf6940629c4ba93aaba78a47ab3..7e6bc75ad52e91e0330d1bd4188369a4ede0d76a 100644
--- a/nomad/config/models/ui.py
+++ b/nomad/config/models/ui.py
@@ -471,7 +471,12 @@ class WidgetHistogram(Widget):
     type: Literal['histogram'] = Field(
         'histogram', description='Set as `histogram` to get this widget type.'
     )
-    quantity: str = Field(description='Targeted quantity.')
+    quantity: Optional[str] = Field(
+        description='Targeted quantity. Note that this field is deprecated and `x` should be used instead.'
+    )
+    x: Union[Axis, str] = Field(
+        description='Configures the information source and display options for the x-axis.'
+    )
     scale: ScaleEnum = Field(description='Statistics scaling.')
     autorange: bool = Field(
         True,
@@ -488,6 +493,19 @@ class WidgetHistogram(Widget):
         """
     )
 
+    @root_validator(pre=True)
+    def __validate(cls, values):
+        """Ensures backwards compatibility for quantity."""
+        quantity = values.get('quantity')
+        x = values.get('x')
+        if quantity and not x:
+            values['x'] = {'quantity': quantity}
+            del values['quantity']
+        elif isinstance(x, str):
+            values['x'] = {'quantity': x}
+
+        return values
+
 
 class WidgetPeriodicTable(Widget):
     """Periodic table widget configuration."""