This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e62978a964 Web console: responding to user feedback about the explore 
view and fixing bugs (#17844)
3e62978a964 is described below

commit 3e62978a964ae956507932b0684e2ebef7bc6ef4
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Sat Mar 29 07:33:02 2025 -0700

    Web console: responding to user feedback about the explore view and fixing 
bugs (#17844)
    
    * better debounce
    
    * better cumpose filter
    
    * hook up preview filters
    
    * better stack handling
    
    * fix some props
    
    * refactor stack to facet
    
    * fix hover part 1
    
    * line hover part 2
    
    * start adding moduleWhere
    
    * info popover
    
    * add filter icon
    
    * toggle button
    
    * module filter bar
---
 web-console/src/utils/general.tsx                  |  17 +++
 .../src/utils/query-manager/query-manager.ts       |  13 +-
 .../column-picker-menu/column-picker-menu.tsx      |  37 ++++--
 .../contains-filter-control.tsx                    |   6 +-
 .../filter-pane/filter-menu/filter-menu.tsx        |  28 ++++-
 .../regexp-filter-control.tsx                      |  11 +-
 .../time-interval-filter-control.tsx               |  50 ++++++--
 .../values-filter-control.tsx                      |  59 +++++----
 .../components/filter-pane/filter-pane.tsx         |   8 +-
 .../src/views/explore-view/components/index.ts     |   2 +-
 .../iso-date-input.tsx}                            |  40 ++++--
 .../components/module-pane/module-pane.scss        |  48 ++++++--
 .../components/module-pane/module-pane.tsx         |  96 +++++++++------
 .../components/preview-pane/preview-pane.scss      |   6 +
 .../components/preview-pane/preview-pane.tsx       |  19 ++-
 .../resource-pane/column-dialog/column-dialog.tsx  |  26 ++--
 .../measure-dialog/measure-dialog.tsx              |  26 ++--
 .../components/resource-pane/resource-pane.tsx     |   6 +-
 .../src/views/explore-view/explore-view.scss       |   1 +
 .../src/views/explore-view/explore-view.tsx        |  22 ++--
 .../src/views/explore-view/models/module-state.ts  |  27 +++--
 .../src/views/explore-view/models/parameter.ts     |   7 +-
 .../src/views/explore-view/models/query-source.ts  |   2 +-
 .../module-repository/module-repository.ts         |   1 +
 .../modules/bar-chart-module/bar-chart-module.tsx  |  25 +++-
 .../grouping-table-module.tsx                      |  15 +--
 .../multi-axis-chart-module.tsx                    |  15 ++-
 .../modules/pie-chart-module/pie-chart-module.tsx  |  30 +++--
 .../record-table-module/record-table-module.tsx    |  15 ++-
 .../time-chart-module/continuous-chart-render.scss |  20 +--
 .../time-chart-module/continuous-chart-render.tsx  | 134 +++++++++++----------
 .../time-chart-module/time-chart-module.tsx        | 118 +++++++++++-------
 32 files changed, 631 insertions(+), 299 deletions(-)

diff --git a/web-console/src/utils/general.tsx 
b/web-console/src/utils/general.tsx
index 8af818664fd..d204c7a89e0 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -458,6 +458,23 @@ export function findMap<T, Q>(
   return filterMap(xs, f)[0];
 }
 
+export function minBy<T>(xs: T[], f: (item: T, index: number) => number): T | 
undefined {
+  if (!xs.length) return undefined;
+
+  let minItem = xs[0];
+  let minValue = f(xs[0], 0);
+
+  for (let i = 1; i < xs.length; i++) {
+    const currentValue = f(xs[i], i);
+    if (currentValue < minValue) {
+      minValue = currentValue;
+      minItem = xs[i];
+    }
+  }
+
+  return minItem;
+}
+
 export function changeByIndex<T>(
   xs: readonly T[],
   i: number,
diff --git a/web-console/src/utils/query-manager/query-manager.ts 
b/web-console/src/utils/query-manager/query-manager.ts
index 0b7fcf5680e..b7cbf3ab1c5 100644
--- a/web-console/src/utils/query-manager/query-manager.ts
+++ b/web-console/src/utils/query-manager/query-manager.ts
@@ -39,6 +39,7 @@ export interface QueryManagerOptions<Q, R, I = never, E 
extends Error = Error> {
     cancelToken: CancelToken,
   ) => Promise<R | IntermediateQueryState<I> | ResultWithAuxiliaryWork<R>>;
   onStateChange?: (queryResolve: QueryState<R, E, I>) => void;
+  debounceInit?: number;
   debounceIdle?: number;
   debounceLoading?: number;
   backgroundStatusCheckInitDelay?: number;
@@ -78,6 +79,7 @@ export class QueryManager<Q, R, I = never, E extends Error = 
Error> {
   private state: QueryState<R, E, I>;
   private currentQueryId = 0;
 
+  private readonly runWhenInit: () => void | Promise<void>;
   private readonly runWhenIdle: () => void | Promise<void>;
   private readonly runWhenLoading: () => void | Promise<void>;
 
@@ -88,6 +90,11 @@ export class QueryManager<Q, R, I = never, E extends Error = 
Error> {
     this.backgroundStatusCheckInitDelay = 
options.backgroundStatusCheckInitDelay || 500;
     this.backgroundStatusCheckDelay = options.backgroundStatusCheckDelay || 
1000;
     this.swallowBackgroundError = options.swallowBackgroundError;
+    if (options.debounceInit !== 0) {
+      this.runWhenInit = debounce(this.run, options.debounceInit || 50);
+    } else {
+      this.runWhenInit = this.run;
+    }
     if (options.debounceIdle !== 0) {
       this.runWhenIdle = debounce(this.run, options.debounceIdle || 100);
     } else {
@@ -257,7 +264,11 @@ export class QueryManager<Q, R, I = never, E extends Error 
= Error> {
         }),
       );
 
-      void this.runWhenIdle();
+      if (this.lastQuery) {
+        void this.runWhenIdle();
+      } else {
+        void this.runWhenInit();
+      }
     }
   }
 
diff --git 
a/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
 
b/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
index 7634cbae4cc..597df5c4d81 100644
--- 
a/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
+++ 
b/web-console/src/views/explore-view/components/column-picker-menu/column-picker-menu.tsx
@@ -17,13 +17,14 @@
  */
 
 import type { IconName } from '@blueprintjs/core';
-import { Icon, InputGroup, Menu, MenuItem } from '@blueprintjs/core';
+import { ContextMenu, Icon, InputGroup, Menu, MenuItem } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import type { Column } from 'druid-query-toolkit';
+import { C } from 'druid-query-toolkit';
 import { useState } from 'react';
 
-import { caseInsensitiveContains, columnToIcon, filterMap } from 
'../../../../utils';
+import { caseInsensitiveContains, columnToIcon, copyAndAlert, filterMap } from 
'../../../../utils';
 
 import './column-picker-menu.scss';
 
@@ -66,17 +67,33 @@ export const ColumnPickerMenu = function 
ColumnPickerMenu(props: ColumnPickerMen
           />
         )}
         {filterMap(columns, (c, i) => {
-          if (!caseInsensitiveContains(c.name, columnSearch)) return;
+          const columnName = c.name;
+          if (!caseInsensitiveContains(columnName, columnSearch)) return;
           const iconName = rightIconForColumn?.(c);
           return (
-            <MenuItem
+            <ContextMenu
               key={i}
-              icon={columnToIcon(c) || IconNames.BLANK}
-              text={c.name}
-              labelElement={iconName && <Icon icon={iconName} />}
-              onClick={() => onSelectColumn(c)}
-              shouldDismissPopover={shouldDismissPopover}
-            />
+              content={
+                <Menu>
+                  <MenuItem
+                    text="Copy"
+                    onClick={() => copyAndAlert(String(columnName), `Copied to 
clipboard`)}
+                  />
+                  <MenuItem
+                    text="Copy as SQL column"
+                    onClick={() => copyAndAlert(String(C(columnName)), `Copied 
to clipboard`)}
+                  />
+                </Menu>
+              }
+            >
+              <MenuItem
+                icon={columnToIcon(c) || IconNames.BLANK}
+                text={columnName}
+                labelElement={iconName && <Icon icon={iconName} />}
+                onClick={() => onSelectColumn(c)}
+                shouldDismissPopover={shouldDismissPopover}
+              />
+            </ContextMenu>
           );
         })}
       </Menu>
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
index b3b6892bb4b..15b9a75cadc 100644
--- 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
+++ 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/contains-filter-control/contains-filter-control.tsx
@@ -30,6 +30,7 @@ import './contains-filter-control.scss';
 
 export interface ContainsFilterControlProps {
   querySource: QuerySource;
+  extraFilter: SqlExpression;
   filter: SqlExpression;
   filterPattern: ContainsFilterPattern;
   setFilterPattern(filterPattern: ContainsFilterPattern): void;
@@ -39,7 +40,7 @@ export interface ContainsFilterControlProps {
 export const ContainsFilterControl = React.memo(function ContainsFilterControl(
   props: ContainsFilterControlProps,
 ) {
-  const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery } 
= props;
+  const { querySource, extraFilter, filter, filterPattern, setFilterPattern, 
runSqlQuery } = props;
   const { column, negated, contains } = filterPattern;
 
   const previewQuery = useMemo(
@@ -47,6 +48,7 @@ export const ContainsFilterControl = React.memo(function 
ContainsFilterControl(
       querySource
         .getInitQuery(
           SqlExpression.and(
+            extraFilter,
             filter,
             contains ? filterPatternToExpression(filterPattern) : undefined,
           ),
@@ -56,7 +58,7 @@ export const ContainsFilterControl = React.memo(function 
ContainsFilterControl(
         .changeLimitValue(101)
         .toString(),
     // eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 
'makePattern' from deps
-    [querySource.query, filter, column, contains, negated],
+    [querySource.query, extraFilter, filter, column, contains, negated],
   );
 
   const [previewState] = useQueryManager<string, string[]>({
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
index b5f5ee2fc9b..39cff40d2a9 100644
--- 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
+++ 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/filter-menu.tsx
@@ -108,6 +108,7 @@ type FilterMenuTab = 'compose' | 'sql';
 
 export interface FilterMenuProps {
   querySource: QuerySource;
+  extraFilter: SqlExpression;
   filter: SqlExpression;
   initPattern?: FilterPattern;
   onPatternChange(newPattern: FilterPattern): void;
@@ -121,6 +122,7 @@ export interface FilterMenuProps {
 export const FilterMenu = React.memo(function FilterMenu(props: 
FilterMenuProps) {
   const {
     querySource,
+    extraFilter,
     filter,
     initPattern,
     onPatternChange,
@@ -136,8 +138,18 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
     initPattern?.type === 'custom' ? 
filterPatternToExpression(initPattern).toString() : '',
   );
   const [pattern, setPattern] = useState<FilterPattern | 
undefined>(initPattern);
+  const [issue, setIssue] = useState<string | undefined>();
   const { columns } = querySource;
 
+  function setFilterPatternOrIssue(pattern: FilterPattern | undefined, issue: 
string | undefined) {
+    if (pattern) {
+      setPattern(pattern);
+      setIssue(undefined);
+    } else {
+      setIssue(issue || 'Issue');
+    }
+  }
+
   function onAcceptPattern(pattern: FilterPattern) {
     onPatternChange(pattern);
     onClose();
@@ -151,6 +163,7 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
       cont = (
         <ValuesFilterControl
           querySource={querySource}
+          extraFilter={extraFilter}
           filter={filter}
           filterPattern={pattern}
           setFilterPattern={setPattern}
@@ -163,6 +176,7 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
       cont = (
         <ContainsFilterControl
           querySource={querySource}
+          extraFilter={extraFilter}
           filter={filter}
           filterPattern={pattern}
           setFilterPattern={setPattern}
@@ -175,6 +189,7 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
       cont = (
         <RegexpFilterControl
           querySource={querySource}
+          extraFilter={extraFilter}
           filter={filter}
           filterPattern={pattern}
           setFilterPattern={setPattern}
@@ -198,7 +213,8 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
         <TimeIntervalFilterControl
           querySource={querySource}
           filterPattern={pattern}
-          setFilterPattern={setPattern}
+          setFilterPatternOrIssue={setFilterPatternOrIssue}
+          onIssue={setIssue}
         />
       );
       break;
@@ -281,6 +297,7 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
             active={tab === 'sql'}
             onClick={() => {
               setFormula(pattern ? 
filterPatternToExpression(pattern).toString() : '');
+              setIssue(undefined);
               setTab('sql');
             }}
           />
@@ -416,8 +433,17 @@ export const FilterMenu = React.memo(function 
FilterMenu(props: FilterMenuProps)
             intent={Intent.PRIMARY}
             text="Apply"
             disabled={tab === 'sql' && formula === ''}
+            data-tooltip={issue ? `Issue: ${issue}` : undefined}
             onClick={() => {
               if (tab === 'compose') {
+                if (issue) {
+                  AppToaster.show({
+                    message: issue,
+                    intent: Intent.DANGER,
+                  });
+                  return;
+                }
+
                 if (pattern) {
                   onAcceptPattern(pattern);
                 }
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
index 23d67ff2ec5..fa0820667e2 100644
--- 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
+++ 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/regexp-filter-control/regexp-filter-control.tsx
@@ -39,6 +39,7 @@ function regexpIssue(possibleRegexp: string): string | 
undefined {
 
 export interface RegexpFilterControlProps {
   querySource: QuerySource;
+  extraFilter: SqlExpression;
   filter: SqlExpression;
   filterPattern: RegexpFilterPattern;
   setFilterPattern(filterPattern: RegexpFilterPattern): void;
@@ -48,21 +49,25 @@ export interface RegexpFilterControlProps {
 export const RegexpFilterControl = React.memo(function RegexpFilterControl(
   props: RegexpFilterControlProps,
 ) {
-  const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery } 
= props;
+  const { querySource, extraFilter, filter, filterPattern, setFilterPattern, 
runSqlQuery } = props;
   const { column, negated, regexp } = filterPattern;
 
   const previewQuery = useMemo(
     () =>
       querySource
         .getInitQuery(
-          SqlExpression.and(filter, regexp ? 
filterPatternToExpression(filterPattern) : undefined),
+          SqlExpression.and(
+            extraFilter,
+            filter,
+            regexp ? filterPatternToExpression(filterPattern) : undefined,
+          ),
         )
         .addSelect(F.cast(C(column), 'VARCHAR').as('c'), { addToGroupBy: 'end' 
})
         .changeOrderByExpression(F.count().toOrderByExpression('DESC'))
         .changeLimitValue(101)
         .toString(),
     // eslint-disable-next-line react-hooks/exhaustive-deps -- exclude 
'makePattern' from deps
-    [querySource.query, filter, column, regexp, negated],
+    [querySource.query, extraFilter, filter, column, regexp, negated],
   );
 
   const [previewState] = useQueryManager<string, string[]>({
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
index 9200ee9e07e..36bee870283 100644
--- 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
+++ 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/time-interval-filter-control/time-interval-filter-control.tsx
@@ -18,35 +18,69 @@
 
 import { FormGroup } from '@blueprintjs/core';
 import type { TimeIntervalFilterPattern } from 'druid-query-toolkit';
-import React from 'react';
+import React, { useState } from 'react';
 
 import type { QuerySource } from '../../../../models';
-import { UtcDateInput } from '../../../utc-date-input/utc-date-input';
+import { IsoDateInput } from '../../../iso-date-input/iso-date-input';
 
 import './time-interval-filter-control.scss';
 
+function isSwappedFilterPattern(pattern: TimeIntervalFilterPattern) {
+  return pattern.end <= pattern.start;
+}
+
 export interface TimeIntervalFilterControlProps {
   querySource: QuerySource;
   filterPattern: TimeIntervalFilterPattern;
-  setFilterPattern(filterPattern: TimeIntervalFilterPattern): void;
+  setFilterPatternOrIssue(
+    filterPattern: TimeIntervalFilterPattern | undefined,
+    issue: string | undefined,
+  ): void;
+  onIssue(issue: string): void;
 }
 
 export const TimeIntervalFilterControl = React.memo(function 
TimeIntervalFilterControl(
   props: TimeIntervalFilterControlProps,
 ) {
-  const { filterPattern, setFilterPattern } = props;
-  const { start, end } = filterPattern;
+  const { filterPattern, setFilterPatternOrIssue, onIssue } = props;
+  const [swappedFilterPattern, setSwappedFilterPattern] = useState<
+    TimeIntervalFilterPattern | undefined
+  >();
+  const { start, end } = swappedFilterPattern || filterPattern;
 
   return (
     <div className="time-interval-filter-control">
       <FormGroup label="Start">
-        <UtcDateInput
+        <IsoDateInput
           date={start}
-          onChange={start => setFilterPattern({ ...filterPattern, start })}
+          onChange={start => {
+            const newPattern = { ...filterPattern, start };
+            if (isSwappedFilterPattern(newPattern)) {
+              setSwappedFilterPattern(newPattern);
+              setFilterPatternOrIssue(undefined, 'Start date must be before 
end date');
+            } else {
+              setSwappedFilterPattern(undefined);
+              setFilterPatternOrIssue(newPattern, undefined);
+            }
+          }}
+          onIssue={() => onIssue('Bad start date')}
         />
       </FormGroup>
       <FormGroup label="End">
-        <UtcDateInput date={end} onChange={end => setFilterPattern({ 
...filterPattern, end })} />
+        <IsoDateInput
+          date={end}
+          onChange={end => {
+            const newPattern = { ...filterPattern, end };
+            if (isSwappedFilterPattern(newPattern)) {
+              setSwappedFilterPattern(newPattern);
+              setFilterPatternOrIssue(undefined, 'End date must be after start 
date');
+            } else {
+              setSwappedFilterPattern(undefined);
+              setFilterPatternOrIssue(newPattern, undefined);
+            }
+          }}
+          onIssue={() => onIssue('Bad end date')}
+        />
       </FormGroup>
     </div>
   );
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
index f14851f081f..cf4701fe456 100644
--- 
a/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
+++ 
b/web-console/src/views/explore-view/components/filter-pane/filter-menu/values-filter-control/values-filter-control.tsx
@@ -16,16 +16,16 @@
  * limitations under the License.
  */
 
-import { FormGroup, Menu, MenuItem } from '@blueprintjs/core';
+import { ContextMenu, FormGroup, Menu, MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import type { CancelToken } from 'axios';
 import type { QueryResult, SqlQuery, ValuesFilterPattern } from 
'druid-query-toolkit';
-import { C, F, SqlExpression } from 'druid-query-toolkit';
+import { C, F, L, SqlExpression } from 'druid-query-toolkit';
 import React, { useMemo, useState } from 'react';
 
 import { ClearableInput } from '../../../../../../components';
 import { useQueryManager } from '../../../../../../hooks';
-import { caseInsensitiveContains, filterMap, toggle } from 
'../../../../../../utils';
+import { caseInsensitiveContains, copyAndAlert, filterMap, toggle } from 
'../../../../../../utils';
 import type { QuerySource } from '../../../../models';
 import { ColumnValue } from '../../../column-value/column-value';
 
@@ -33,6 +33,7 @@ import './values-filter-control.scss';
 
 export interface ValuesFilterControlProps {
   querySource: QuerySource;
+  extraFilter: SqlExpression;
   filter: SqlExpression;
   filterPattern: ValuesFilterPattern;
   setFilterPattern(filterPattern: ValuesFilterPattern): void;
@@ -42,7 +43,7 @@ export interface ValuesFilterControlProps {
 export const ValuesFilterControl = React.memo(function ValuesFilterControl(
   props: ValuesFilterControlProps,
 ) {
-  const { querySource, filter, filterPattern, setFilterPattern, runSqlQuery } 
= props;
+  const { querySource, extraFilter, filter, filterPattern, setFilterPattern, 
runSqlQuery } = props;
   const { column, negated, values: selectedValues } = filterPattern;
   const [initValues] = useState(selectedValues);
   const [searchString, setSearchString] = useState('');
@@ -52,6 +53,7 @@ export const ValuesFilterControl = React.memo(function 
ValuesFilterControl(
       querySource
         .getInitQuery(
           SqlExpression.and(
+            extraFilter,
             filter,
             searchString ? F('ICONTAINS_STRING', C(column), searchString) : 
undefined,
           ),
@@ -61,7 +63,7 @@ export const ValuesFilterControl = React.memo(function 
ValuesFilterControl(
         .changeLimitValue(101)
         .toString(),
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [querySource.query, filter, column, searchString],
+    [querySource.query, extraFilter, filter, column, searchString],
   );
 
   const [valuesState] = useQueryManager<string, any[]>({
@@ -90,24 +92,39 @@ export const ValuesFilterControl = React.memo(function 
ValuesFilterControl(
         {filterMap(valuesToShow, (v, i) => {
           if (!caseInsensitiveContains(v, searchString)) return;
           return (
-            <MenuItem
+            <ContextMenu
               key={i}
-              icon={
-                selectedValues.includes(v)
-                  ? negated
-                    ? IconNames.DELETE
-                    : IconNames.TICK_CIRCLE
-                  : IconNames.CIRCLE
+              content={
+                <Menu>
+                  <MenuItem
+                    text="Copy"
+                    onClick={() => copyAndAlert(String(v), `Copied to 
clipboard`)}
+                  />
+                  <MenuItem
+                    text="Copy as SQL value"
+                    onClick={() => copyAndAlert(String(L(v)), `Copied to 
clipboard`)}
+                  />
+                </Menu>
               }
-              text={<ColumnValue value={v} />}
-              shouldDismissPopover={false}
-              onClick={e => {
-                setFilterPattern({
-                  ...filterPattern,
-                  values: e.altKey ? [v] : toggle(selectedValues, v),
-                });
-              }}
-            />
+            >
+              <MenuItem
+                icon={
+                  selectedValues.includes(v)
+                    ? negated
+                      ? IconNames.DELETE
+                      : IconNames.TICK_CIRCLE
+                    : IconNames.CIRCLE
+                }
+                text={<ColumnValue value={v} />}
+                shouldDismissPopover={false}
+                onClick={e => {
+                  setFilterPattern({
+                    ...filterPattern,
+                    values: e.altKey ? [v] : toggle(selectedValues, v),
+                  });
+                }}
+              />
+            </ContextMenu>
           );
         })}
         {valuesState.loading && <MenuItem icon={IconNames.BLANK} 
text="Loading..." disabled />}
diff --git 
a/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx 
b/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
index cab39295b9f..fa62e0d65cd 100644
--- a/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
+++ b/web-console/src/views/explore-view/components/filter-pane/filter-pane.tsx
@@ -48,6 +48,7 @@ import './filter-pane.scss';
 
 export interface FilterPaneProps {
   querySource: QuerySource | undefined;
+  extraFilter: SqlExpression;
   timezone: Timezone;
   filter: SqlExpression;
   onFilterChange(filter: SqlExpression): void;
@@ -59,6 +60,7 @@ export interface FilterPaneProps {
 export const FilterPane = forwardRef(function FilterPane(props: 
FilterPaneProps, ref) {
   const {
     querySource,
+    extraFilter,
     timezone,
     filter,
     onFilterChange,
@@ -139,6 +141,7 @@ export const FilterPane = forwardRef(function 
FilterPane(props: FilterPaneProps,
                 content={
                   <FilterMenu
                     querySource={querySource}
+                    extraFilter={extraFilter}
                     filter={filterPatternsToExpression(without(patterns, 
pattern))}
                     initPattern={pattern}
                     onPatternChange={newPattern => {
@@ -201,6 +204,7 @@ export const FilterPane = forwardRef(function 
FilterPane(props: FilterPaneProps,
           content={
             <FilterMenu
               querySource={querySource}
+              extraFilter={extraFilter}
               filter={filter}
               initPattern={menuNew?.column ? 
initPatternForColumn(menuNew?.column) : undefined}
               onPatternChange={newPattern => {
@@ -220,7 +224,7 @@ export const FilterPane = forwardRef(function 
FilterPane(props: FilterPaneProps,
             text={patterns.length ? undefined : 'Add filter'}
             onClick={() => setMenuNew({})}
             minimal
-            data-tooltip="Add filter"
+            data-tooltip={patterns.length ? 'Add filter' : undefined}
           />
         </Popover>
       ) : (
@@ -229,7 +233,7 @@ export const FilterPane = forwardRef(function 
FilterPane(props: FilterPaneProps,
           text={patterns.length ? undefined : 'Add filter'}
           disabled
           minimal
-          data-tooltip="Add filter"
+          data-tooltip="No query source, unable to query"
         />
       )}
     </DroppableContainer>
diff --git a/web-console/src/views/explore-view/components/index.ts 
b/web-console/src/views/explore-view/components/index.ts
index 085bbff3c3e..58a8fb6fa72 100644
--- a/web-console/src/views/explore-view/components/index.ts
+++ b/web-console/src/views/explore-view/components/index.ts
@@ -21,6 +21,7 @@ export * from './droppable-container/droppable-container';
 export * from './filter-pane/filter-pane';
 export * from './generic-output-table/generic-output-table';
 export * from './helper-table/helper-table';
+export * from './iso-date-input/iso-date-input';
 export * from './issue/issue';
 export * from './module-pane/module-pane';
 export * from './module-picker/module-picker';
@@ -28,4 +29,3 @@ export * from './resource-pane/resource-pane';
 export * from './source-pane/source-pane';
 export * from './source-query-pane/source-query-pane';
 export * from './sql-input/sql-input';
-export * from './utc-date-input/utc-date-input';
diff --git 
a/web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
 
b/web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
similarity index 62%
rename from 
web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
rename to 
web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
index b1de4ce7c13..b145e31a221 100644
--- 
a/web-console/src/views/explore-view/components/utc-date-input/utc-date-input.tsx
+++ 
b/web-console/src/views/explore-view/components/iso-date-input/iso-date-input.tsx
@@ -16,10 +16,10 @@
  * limitations under the License.
  */
 
-import { InputGroup } from '@blueprintjs/core';
+import { InputGroup, Intent } from '@blueprintjs/core';
 import { useState } from 'react';
 
-function utcParseDate(dateString: string): Date | undefined {
+function isoParseDate(dateString: string): Date | undefined {
   const dateParts = dateString.split(/[-T:. ]/g);
 
   // Extract the individual date and time components
@@ -44,7 +44,7 @@ function utcParseDate(dateString: string): Date | undefined {
   const millisecond = parseInt(dateParts[6], 10);
   if (millisecond >= 1000) return;
 
-  const value = Date.UTC(year, month - 1, day, hour, minute, second); // Month 
is zero-based
+  const value = Date.UTC(year, month - 1, day, hour, minute, second, 
millisecond); // Month is zero-based
   if (isNaN(value)) return;
 
   return new Date(value);
@@ -61,26 +61,42 @@ function formatDate(date: Date): string {
 export interface UtcDateInputProps {
   date: Date;
   onChange(newDate: Date): void;
+  onIssue(): void;
 }
 
-export function UtcDateInput(props: UtcDateInputProps) {
-  const { date, onChange } = props;
-  const [dateString, setDateString] = useState<string | undefined>();
+export function IsoDateInput(props: UtcDateInputProps) {
+  const { date, onChange, onIssue } = props;
+  const [invalidDateString, setInvalidDateString] = useState<string | 
undefined>();
+  const [customDateString, setCustomDateString] = useState<string | 
undefined>();
+  const [focused, setFocused] = useState<boolean>(false);
 
   return (
     <InputGroup
+      className="iso-date-input"
       placeholder="yyyy-MM-dd HH:mm:ss"
-      value={dateString ?? formatDate(date)}
+      intent={!focused && invalidDateString ? Intent.DANGER : undefined}
+      value={
+        invalidDateString ??
+        (customDateString && isoParseDate(customDateString)?.valueOf() === 
date.valueOf()
+          ? customDateString
+          : undefined) ??
+        formatDate(date)
+      }
       onChange={e => {
-        const v = normalizeDateString(e.target.value);
-        const parsedDate = utcParseDate(v);
-        if (parsedDate && formatDate(parsedDate) === v) {
+        const normalizedDateString = normalizeDateString(e.target.value);
+        const parsedDate = isoParseDate(normalizedDateString);
+        if (parsedDate) {
           onChange(parsedDate);
-          setDateString(undefined);
+          setInvalidDateString(undefined);
+          setCustomDateString(normalizedDateString);
         } else {
-          setDateString(v);
+          onIssue();
+          setInvalidDateString(normalizedDateString);
+          setCustomDateString(undefined);
         }
       }}
+      onFocus={() => setFocused(true)}
+      onBlur={() => setFocused(false)}
     />
   );
 }
diff --git 
a/web-console/src/views/explore-view/components/module-pane/module-pane.scss 
b/web-console/src/views/explore-view/components/module-pane/module-pane.scss
index e74e3fe4bae..0fa698cfc9b 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.scss
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.scss
@@ -18,47 +18,71 @@
 
 @import '../../../../variables';
 
-$tob-bar-height: 34px;
+$filter-bar-height: 34px;
+$control-bar-height: 34px;
 $control-pane-width: 240px;
+$small-border: 1px;
 
 .module-pane {
   position: relative;
   @include card-like;
   overflow: hidden;
 
-  .module-top-bar {
+  .filter-pane {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 90px;
+    height: $filter-bar-height;
+    border-bottom: $small-border solid $dark-gray2;
+    padding: 2px 0;
+  }
+
+  .module-control-bar {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
-    height: $tob-bar-height;
-    border-bottom: 1px solid $dark-gray2;
+    height: $control-bar-height;
+    border-bottom: $small-border solid $dark-gray2;
     padding: 2px;
     display: flex;
-
-    .bar-expander {
-      flex: 1;
-    }
   }
 
   .control-pane-container {
     position: absolute;
-    top: $tob-bar-height + 1;
+    top: $control-bar-height + 1;
     bottom: 0;
     width: $control-pane-width;
     right: 0;
-    border-left: 1px solid $dark-gray2;
+    border-left: $small-border solid $dark-gray2;
     padding: 8px;
     overflow: auto;
   }
 
   &.show-controls .module-inner-container {
-    right: $control-pane-width + 1;
+    right: $control-pane-width + $small-border;
+  }
+
+  &.show-filter {
+    .module-control-bar {
+      top: $filter-bar-height + $small-border;
+    }
+
+    .module-inner-container {
+      top: $filter-bar-height + $small-border + $control-bar-height + 
$small-border;
+    }
+  }
+
+  .corner-buttons {
+    position: absolute;
+    top: 2px;
+    right: 2px;
   }
 
   .module-inner-container {
     position: absolute;
-    top: $tob-bar-height + 1;
+    top: $control-bar-height + $small-border;
     bottom: 0;
     left: 0;
     right: 0;
diff --git 
a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx 
b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
index b06e7b1c346..96ed6e8fe90 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
@@ -30,6 +30,7 @@ import { IconNames } from '@blueprintjs/icons';
 import type { Timezone } from 'chronoshift';
 import classNames from 'classnames';
 import type { Column, QueryResult, SqlExpression, SqlQuery } from 
'druid-query-toolkit';
+import { SqlLiteral } from 'druid-query-toolkit';
 import React, { useState } from 'react';
 
 import { useMemoWithPrevious } from '../../../../hooks';
@@ -53,6 +54,7 @@ import { ModuleRepository } from 
'../../module-repository/module-repository';
 import { adjustTransferValue, normalizeType } from '../../utils';
 import { ControlPane } from '../control-pane/control-pane';
 import { DroppableContainer } from 
'../droppable-container/droppable-container';
+import { FilterPane } from '../filter-pane/filter-pane';
 import { Issue } from '../issue/issue';
 import { ModulePicker } from '../module-picker/module-picker';
 
@@ -115,7 +117,7 @@ export const ModulePane = function ModulePane(props: 
ModulePaneProps) {
     onAddToSourceQueryAsColumn,
     onAddToSourceQueryAsMeasure,
   } = props;
-  const { moduleId, parameterValues, showControls } = moduleState;
+  const { moduleId, moduleWhere, parameterValues, showModuleWhere, 
showControls } = moduleState;
   const [stage, setStage] = useState<Stage | undefined>();
 
   const module = ModuleRepository.getModule(moduleId);
@@ -174,6 +176,7 @@ export const ModulePane = function ModulePane(props: 
ModulePaneProps) {
         timezone,
         where,
         setWhere,
+        moduleWhere,
         parameterValues: parameterValuesWithDefaults,
         setParameterValues: updateParameterValues,
         runSqlQuery,
@@ -191,15 +194,27 @@ export const ModulePane = function ModulePane(props: 
ModulePaneProps) {
     setModuleState(moduleState.applyShowMeasure(measure));
   }
 
+  const moduleHasFilter = !SqlLiteral.isTrue(moduleWhere);
   return (
     <div
       className={classNames(
         'module-pane',
         className,
         showControls ? 'show-controls' : 'no-controls',
+        showModuleWhere ? 'show-filter' : 'no-filter',
       )}
     >
-      <div className="module-top-bar">
+      {showModuleWhere && (
+        <FilterPane
+          querySource={querySource}
+          extraFilter={where}
+          timezone={timezone}
+          filter={moduleWhere}
+          onFilterChange={newFilter => 
setModuleState(moduleState.changeModuleWhere(newFilter))}
+          runSqlQuery={runSqlQuery}
+        />
+      )}
+      <div className="module-control-bar">
         <ModulePicker
           selectedModuleId={moduleId}
           onSelectedModuleIdChange={newModuleId => {
@@ -254,42 +269,49 @@ export const ModulePane = function ModulePane(props: 
ModulePaneProps) {
             onAddToSourceQueryAsMeasure={onAddToSourceQueryAsMeasure}
           />
         )}
-        <div className="bar-expander" />
-        <ButtonGroup>
-          <Popover
-            position={Position.BOTTOM_RIGHT}
-            content={
-              <Menu>
-                <MenuItem
-                  icon={IconNames.RESET}
-                  text="Reset visualization parameters"
-                  onClick={() => {
-                    setModuleState(
-                      moduleState.changeParameterValues(
-                        getStickyParameterValuesForModule(moduleId),
-                      ),
-                    );
-                  }}
-                />
-                <MenuItem
-                  icon={IconNames.TRASH}
-                  text="Delete module"
-                  intent={Intent.DANGER}
-                  onClick={onDelete}
-                />
-              </Menu>
-            }
-          >
-            <Button icon={IconNames.MORE} data-tooltip="More module options" 
minimal />
-          </Popover>
-          <Button
-            icon={IconNames.PROPERTIES}
-            data-tooltip={showControls ? 'Hide module controls' : 'Show module 
controls'}
-            minimal
-            onClick={() => setModuleState(moduleState.change({ showControls: 
!showControls }))}
-          />
-        </ButtonGroup>
       </div>
+      <ButtonGroup className="corner-buttons">
+        <Popover
+          position={Position.BOTTOM_RIGHT}
+          content={
+            <Menu>
+              <MenuItem
+                icon={IconNames.RESET}
+                text="Reset visualization parameters"
+                onClick={() => {
+                  setModuleState(
+                    
moduleState.changeParameterValues(getStickyParameterValuesForModule(moduleId)),
+                  );
+                }}
+              />
+              <MenuItem
+                icon={IconNames.TRASH}
+                text="Delete module"
+                intent={Intent.DANGER}
+                onClick={onDelete}
+              />
+            </Menu>
+          }
+        >
+          <Button icon={IconNames.MORE} data-tooltip="More module options" 
minimal />
+        </Popover>
+        <Button
+          icon={moduleHasFilter ? IconNames.FILTER_KEEP : IconNames.FILTER}
+          data-tooltip={`${showModuleWhere ? 'Hide' : 'Show'} module filter 
bar${
+            moduleHasFilter ? `\nCurrent filter: ${moduleWhere}` : ''
+          }`}
+          minimal
+          active={showModuleWhere}
+          onClick={() => setModuleState(moduleState.change({ showModuleWhere: 
!showModuleWhere }))}
+        />
+        <Button
+          icon={IconNames.PROPERTIES}
+          data-tooltip={`${showControls ? 'Hide' : 'Show'} module controls`}
+          minimal
+          active={showControls}
+          onClick={() => setModuleState(moduleState.change({ showControls: 
!showControls }))}
+        />
+      </ButtonGroup>
       {showControls && module && (
         <div className="control-pane-container">
           <ControlPane
diff --git 
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss 
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
index eff7363ad64..8749d7419a1 100644
--- 
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
+++ 
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.scss
@@ -21,6 +21,12 @@
   display: flex;
   flex-direction: column;
 
+  .info-popover {
+    position: absolute;
+    top: 6px;
+    right: 6px;
+  }
+
   .preview-values-wrapper {
     flex: 1;
     position: relative;
diff --git 
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx 
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
index 41bfd4bd14a..bb70bda5544 100644
--- 
a/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
+++ 
b/web-console/src/views/explore-view/components/preview-pane/preview-pane.tsx
@@ -16,11 +16,14 @@
  * limitations under the License.
  */
 
-import { Callout } from '@blueprintjs/core';
+import { Button, Callout, Popover } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import type { QueryResult, SqlQuery } from 'druid-query-toolkit';
+import { dedupe } from 'druid-query-toolkit';
 import React from 'react';
 
+import { PopoverText } from '../../../../components';
 import { useQueryManager } from '../../../../hooks';
 import { formatEmpty } from '../../../../utils';
 
@@ -38,27 +41,35 @@ function getPreviewValues(queryResult: QueryResult): any[] {
 export interface PreviewPaneProps {
   previewQuery: string | undefined;
   runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
+  deduplicate?: boolean;
+  info?: string;
 }
 
 export const PreviewPane = React.memo(function PreviewPane(props: 
PreviewPaneProps) {
-  const { previewQuery, runSqlQuery } = props;
+  const { previewQuery, runSqlQuery, deduplicate, info } = props;
 
   const [previewState] = useQueryManager({
     query: previewQuery,
     processQuery: runSqlQuery,
-    debounceIdle: 1000,
+    debounceIdle: 2000,
+    debounceLoading: 3000,
   });
 
   const previewValues = previewState.data ? 
getPreviewValues(previewState.data) : undefined;
   return (
     <Callout className="preview-pane" title="Preview">
+      {info && (
+        <Popover className="info-popover" 
content={<PopoverText>{info}</PopoverText>}>
+          <Button icon={IconNames.INFO_SIGN} minimal />
+        </Popover>
+      )}
       {previewState.loading && 'Loading...'}
       {previewState.error && <div 
className="preview-error">{previewState.getErrorMessage()}</div>}
       {previewValues &&
         (previewValues.length ? (
           <div className="preview-values-wrapper">
             <div className="preview-values">
-              {previewValues.map((v, i) => (
+              {(deduplicate ? dedupe(previewValues) : previewValues).map((v, 
i) => (
                 <div
                   className={classNames('preview-value', { special: v == null 
|| v === '' })}
                   key={i}
diff --git 
a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
 
b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
index d220106ec76..2ec95a4b989 100644
--- 
a/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
+++ 
b/web-console/src/views/explore-view/components/resource-pane/column-dialog/column-dialog.tsx
@@ -18,7 +18,7 @@
 
 import { Button, Classes, Dialog, FormGroup, InputGroup, Intent, Tag } from 
'@blueprintjs/core';
 import type { QueryResult, SqlQuery } from 'druid-query-toolkit';
-import { F, sql, SqlExpression } from 'druid-query-toolkit';
+import { F, SqlExpression } from 'druid-query-toolkit';
 import React, { useMemo, useState } from 'react';
 
 import { AppToaster } from '../../../../../singletons';
@@ -35,12 +35,14 @@ export interface ColumnDialogProps {
   columnToDuplicate?: string;
   onApply(newQuery: SqlQuery, rename: Rename | undefined): void;
   querySource: QuerySource;
+  where: SqlExpression;
   runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
   onClose(): void;
 }
 
 export const ColumnDialog = React.memo(function ColumnDialog(props: 
ColumnDialogProps) {
-  const { initExpression, columnToDuplicate, onApply, querySource, 
runSqlQuery, onClose } = props;
+  const { initExpression, columnToDuplicate, onApply, querySource, where, 
runSqlQuery, onClose } =
+    props;
 
   const [outputName, setOutputName] = useState(initExpression?.getOutputName() 
|| '');
   const [formula, setFormula] = 
useState(String(initExpression?.getUnderlyingExpression() || ''));
@@ -48,15 +50,16 @@ export const ColumnDialog = React.memo(function 
ColumnDialog(props: ColumnDialog
   const previewQuery = useMemo(() => {
     const expression = SqlExpression.maybeParse(formula);
     if (!expression) return;
+
+    const expressionToPreview = F.cast(expression, 'VARCHAR');
     return querySource
       .getInitBaseQuery()
-      .addSelect(F.cast(expression, 'VARCHAR').as('v'), { addToGroupBy: 'end' 
})
-      .applyIf(querySource.hasBaseTimeColumn(), q =>
-        q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
-      )
-      .changeLimitValue(100)
+      .addSelect(expressionToPreview.as('v'))
+      .addWhere(expressionToPreview.isNotNull())
+      .addWhere(querySource.transformExpressionToBaseColumns(where))
+      .changeLimitValue(200)
       .toString();
-  }, [querySource, formula]);
+  }, [querySource, formula, where]);
 
   return (
     <Dialog
@@ -90,7 +93,12 @@ export const ColumnDialog = React.memo(function 
ColumnDialog(props: ColumnDialog
             />
           </FormGroup>
         </div>
-        <PreviewPane previewQuery={previewQuery} runSqlQuery={runSqlQuery} />
+        <PreviewPane
+          previewQuery={previewQuery}
+          runSqlQuery={runSqlQuery}
+          deduplicate
+          info="The preview samples values for the column within the selected 
filter."
+        />
       </div>
       <div className={Classes.DIALOG_FOOTER}>
         <div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git 
a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
 
b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
index 308e2563ebf..5299a5dca2e 100644
--- 
a/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
+++ 
b/web-console/src/views/explore-view/components/resource-pane/measure-dialog/measure-dialog.tsx
@@ -17,14 +17,7 @@
  */
 
 import { Button, Classes, Dialog, FormGroup, InputGroup, Intent, Tag } from 
'@blueprintjs/core';
-import {
-  L,
-  type QueryResult,
-  sql,
-  SqlExpression,
-  SqlQuery,
-  SqlWithPart,
-} from 'druid-query-toolkit';
+import { L, type QueryResult, SqlExpression, SqlQuery, SqlWithPart } from 
'druid-query-toolkit';
 import React, { useMemo, useState } from 'react';
 
 import { AppToaster } from '../../../../../singletons';
@@ -40,12 +33,14 @@ export interface MeasureDialogProps {
   measureToDuplicate?: string;
   onApply(newQuery: SqlQuery, rename: Rename | undefined): void;
   querySource: QuerySource;
+  where: SqlExpression;
   runSqlQuery(query: string | SqlQuery): Promise<QueryResult>;
   onClose(): void;
 }
 
 export const MeasureDialog = React.memo(function MeasureDialog(props: 
MeasureDialogProps) {
-  const { initMeasure, measureToDuplicate, onApply, querySource, runSqlQuery, 
onClose } = props;
+  const { initMeasure, measureToDuplicate, onApply, querySource, where, 
runSqlQuery, onClose } =
+    props;
 
   const [outputName, setOutputName] = useState(initMeasure?.name || '');
   const [formula, setFormula] = useState(String(initMeasure?.expression || 
''));
@@ -57,12 +52,9 @@ export const MeasureDialog = React.memo(function 
MeasureDialog(props: MeasureDia
       .changeWithParts([SqlWithPart.simple('t', 
QuerySource.stripToBaseSource(querySource.query))])
       .addSelect(L('Overall').as('label'))
       .addSelect(expression.as('value'))
-      .applyIf(querySource.hasBaseTimeColumn(), q =>
-        q.addWhere(sql`MAX_DATA_TIME() - INTERVAL '14' DAY <= __time`),
-      )
+      
.changeWhereExpression(querySource.transformExpressionToBaseColumns(where))
       .toString();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [querySource.query, formula]);
+  }, [querySource, formula, where]);
 
   return (
     <Dialog
@@ -96,7 +88,11 @@ export const MeasureDialog = React.memo(function 
MeasureDialog(props: MeasureDia
             />
           </FormGroup>
         </div>
-        <PreviewPane previewQuery={previewQuery} runSqlQuery={runSqlQuery} />
+        <PreviewPane
+          previewQuery={previewQuery}
+          runSqlQuery={runSqlQuery}
+          info="The preview shows the overal value for this measure within the 
selected filter."
+        />
       </div>
       <div className={Classes.DIALOG_FOOTER}>
         <div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git 
a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx 
b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
index 9c6146dfd1b..068404a8853 100644
--- 
a/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
+++ 
b/web-console/src/views/explore-view/components/resource-pane/resource-pane.tsx
@@ -70,6 +70,7 @@ interface MeasureEditorOpenOn {
 export interface ResourcePaneProps {
   querySource: QuerySource;
   onQueryChange: (newQuery: SqlQuery, rename: Rename | undefined) => void;
+  where: SqlExpression;
   onFilter?: (column: Column) => void;
   onShowColumn(column: Column): void;
   onShowMeasure(measure: Measure): void;
@@ -77,7 +78,8 @@ export interface ResourcePaneProps {
 }
 
 export const ResourcePane = function ResourcePane(props: ResourcePaneProps) {
-  const { querySource, onQueryChange, onFilter, onShowColumn, onShowMeasure, 
runSqlQuery } = props;
+  const { querySource, onQueryChange, where, onFilter, onShowColumn, 
onShowMeasure, runSqlQuery } =
+    props;
   const [columnSearch, setColumnSearch] = useState('');
 
   const [columnEditorOpenOn, setColumnEditorOpenOn] = 
useState<ColumnEditorOpenOn | undefined>();
@@ -317,6 +319,7 @@ export const ResourcePane = function ResourcePane(props: 
ResourcePaneProps) {
           columnToDuplicate={columnEditorOpenOn.columnToDuplicate}
           onApply={onQueryChange}
           querySource={querySource}
+          where={where}
           runSqlQuery={runSqlQuery}
           onClose={() => setColumnEditorOpenOn(undefined)}
         />
@@ -336,6 +339,7 @@ export const ResourcePane = function ResourcePane(props: 
ResourcePaneProps) {
           measureToDuplicate={measureEditorOpenOn.measureToDuplicate}
           onApply={onQueryChange}
           querySource={querySource}
+          where={where}
           runSqlQuery={runSqlQuery}
           onClose={() => setMeasureEditorOpenOn(undefined)}
         />
diff --git a/web-console/src/views/explore-view/explore-view.scss 
b/web-console/src/views/explore-view/explore-view.scss
index 54a976f780d..d0541870089 100644
--- a/web-console/src/views/explore-view/explore-view.scss
+++ b/web-console/src/views/explore-view/explore-view.scss
@@ -72,6 +72,7 @@ $resources-width: 240px;
       .source-pane-container {
         padding: 8px 0;
         border-right: 1px solid $dark-gray2;
+        align-self: stretch;
       }
 
       .filter-pane {
diff --git a/web-console/src/views/explore-view/explore-view.tsx 
b/web-console/src/views/explore-view/explore-view.tsx
index 0d1624cbd7f..fd5b43a51f1 100644
--- a/web-console/src/views/explore-view/explore-view.tsx
+++ b/web-console/src/views/explore-view/explore-view.tsx
@@ -36,7 +36,7 @@ import { Timezone } from 'chronoshift';
 import classNames from 'classnames';
 import copy from 'copy-to-clipboard';
 import type { Column, QueryResult, SqlExpression } from 'druid-query-toolkit';
-import { QueryRunner, SqlQuery } from 'druid-query-toolkit';
+import { QueryRunner, SqlLiteral, SqlQuery } from 'druid-query-toolkit';
 import React, { useEffect, useMemo, useRef, useState } from 'react';
 
 import { Loader, SplitterLayout, TimezoneMenuItems } from '../../components';
@@ -144,9 +144,7 @@ export const ExploreView = React.memo(function 
ExploreView() {
     '#explore/v/',
     LocalStorageKeys.EXPLORE_STATE,
     ExploreState.DEFAULT_STATE,
-    s => {
-      return ExploreState.fromJS(s);
-    },
+    s => ExploreState.fromJS(s),
   );
 
   // -------------------------------------------------------
@@ -321,6 +319,7 @@ export const ExploreView = React.memo(function 
ExploreView() {
               <FilterPane
                 ref={filterPane}
                 querySource={querySource}
+                extraFilter={SqlLiteral.TRUE}
                 timezone={timezone}
                 filter={where}
                 onFilterChange={setWhere}
@@ -329,7 +328,9 @@ export const ExploreView = React.memo(function 
ExploreView() {
                   if (!querySource) return;
                   setExploreState(
                     effectiveExploreState.changeSource(
-                      
querySource.addColumn(querySource.transformToBaseColumns(expression)),
+                      querySource.addColumn(
+                        
querySource.transformExpressionToBaseColumns(expression),
+                      ),
                       undefined,
                     ),
                   );
@@ -340,7 +341,9 @@ export const ExploreView = React.memo(function 
ExploreView() {
                     effectiveExploreState
                       .change({ where: changeWhere })
                       .changeSource(
-                        
querySource.addWhereClause(querySource.transformToBaseColumns(expression)),
+                        querySource.addWhereClause(
+                          
querySource.transformExpressionToBaseColumns(expression),
+                        ),
                         undefined,
                       ),
                   );
@@ -461,6 +464,7 @@ export const ExploreView = React.memo(function 
ExploreView() {
                     <ResourcePane
                       querySource={querySource}
                       onQueryChange={setSource}
+                      where={where}
                       onFilter={c => {
                         filterPane.current?.filterOn(c);
                       }}
@@ -502,7 +506,7 @@ export const ExploreView = React.memo(function 
ExploreView() {
                             setExploreState(
                               effectiveExploreState.changeSource(
                                 querySource.addColumn(
-                                  
querySource.transformToBaseColumns(expression),
+                                  
querySource.transformExpressionToBaseColumns(expression),
                                 ),
                                 undefined,
                               ),
@@ -514,7 +518,9 @@ export const ExploreView = React.memo(function 
ExploreView() {
                               effectiveExploreState.changeSource(
                                 querySource.addMeasure(
                                   measure.changeExpression(
-                                    
querySource.transformToBaseColumns(measure.expression),
+                                    
querySource.transformExpressionToBaseColumns(
+                                      measure.expression,
+                                    ),
                                   ),
                                 ),
                                 undefined,
diff --git a/web-console/src/views/explore-view/models/module-state.ts 
b/web-console/src/views/explore-view/models/module-state.ts
index 93959adea6d..19230fc18af 100644
--- a/web-console/src/views/explore-view/models/module-state.ts
+++ b/web-console/src/views/explore-view/models/module-state.ts
@@ -16,9 +16,9 @@
  * limitations under the License.
  */
 
-import type { Column, SqlExpression } from 'druid-query-toolkit';
+import type { Column } from 'druid-query-toolkit';
+import { SqlExpression, SqlLiteral } from 'druid-query-toolkit';
 
-import { isEmpty } from '../../../utils';
 import { ModuleRepository } from '../module-repository/module-repository';
 import type { Rename } from '../utils';
 
@@ -30,7 +30,9 @@ import type { QuerySource } from './query-source';
 
 interface ModuleStateValue {
   moduleId: string;
+  moduleWhere?: SqlExpression;
   parameterValues: ParameterValues;
+  showModuleWhere?: boolean;
   showControls?: boolean;
 }
 
@@ -44,17 +46,22 @@ export class ModuleState {
     );
     return new ModuleState({
       ...js,
+      moduleWhere: SqlExpression.maybeParse(js.moduleWhere),
       parameterValues: inflatedParameterValues,
     });
   }
 
   public readonly moduleId: string;
+  public readonly moduleWhere: SqlExpression;
   public readonly parameterValues: ParameterValues;
+  public readonly showModuleWhere: boolean;
   public readonly showControls: boolean;
 
   constructor(value: ModuleStateValue) {
     this.moduleId = value.moduleId;
+    this.moduleWhere = value.moduleWhere || SqlLiteral.TRUE;
     this.parameterValues = value.parameterValues;
+    this.showModuleWhere = Boolean(value.showModuleWhere);
     this.showControls = Boolean(value.showControls);
   }
 
@@ -63,6 +70,8 @@ export class ModuleState {
       moduleId: this.moduleId,
       parameterValues: this.parameterValues,
     };
+    if (!SqlLiteral.isTrue(this.moduleWhere)) value.moduleWhere = 
this.moduleWhere;
+    if (this.showModuleWhere) value.showModuleWhere = true;
     if (this.showControls) value.showControls = true;
     return value;
   }
@@ -74,6 +83,10 @@ export class ModuleState {
     });
   }
 
+  public changeModuleWhere(moduleWhere: SqlExpression): ModuleState {
+    return this.change({ moduleWhere });
+  }
+
   public changeParameterValues(parameterValues: ParameterValues): ModuleState {
     return this.change({ parameterValues });
   }
@@ -92,17 +105,19 @@ export class ModuleState {
   }
 
   public restrictToQuerySource(querySource: QuerySource, where: 
SqlExpression): ModuleState {
-    const { moduleId, parameterValues } = this;
+    const { moduleId, moduleWhere, parameterValues } = this;
     const module = ModuleRepository.getModule(moduleId);
     if (!module) return this;
+    const newModuleWhere = querySource.restrictWhere(moduleWhere);
     const newParameterValues = querySource.restrictParameterValues(
       parameterValues,
       module.parameters,
       where,
     );
-    if (parameterValues === newParameterValues) return this;
+    if (moduleWhere === newModuleWhere && parameterValues === 
newParameterValues) return this;
 
     return this.change({
+      moduleWhere: newModuleWhere,
       parameterValues: newParameterValues,
     });
   }
@@ -161,10 +176,6 @@ export class ModuleState {
       },
     });
   }
-
-  public isInitState(): boolean {
-    return this.moduleId === 'record-table' && isEmpty(this.parameterValues);
-  }
 }
 
 ModuleState.INIT_STATE = new ModuleState({
diff --git a/web-console/src/views/explore-view/models/parameter.ts 
b/web-console/src/views/explore-view/models/parameter.ts
index ed021da6f5b..1929795bc19 100644
--- a/web-console/src/views/explore-view/models/parameter.ts
+++ b/web-console/src/views/explore-view/models/parameter.ts
@@ -108,6 +108,7 @@ export type TypedParameterDefinition<Type extends keyof 
ParameterTypes> = TypedE
   placeholder?: string;
   defined?: ModuleFunctor<boolean>;
   visible?: ModuleFunctor<boolean>;
+  legacyName?: string;
 };
 
 export type ParameterDefinition =
@@ -157,7 +158,11 @@ export function inflateParameterValues(
   parameters: Parameters,
 ): ParameterValues {
   return mapRecord(parameters, (parameter, parameterName) =>
-    inflateParameterValue(parameterValues?.[parameterName], parameter),
+    inflateParameterValue(
+      parameterValues?.[parameterName] ??
+        (parameter.legacyName ? parameterValues?.[parameter.legacyName] : 
undefined),
+      parameter,
+    ),
   );
 }
 
diff --git a/web-console/src/views/explore-view/models/query-source.ts 
b/web-console/src/views/explore-view/models/query-source.ts
index fa3f047426e..cb67dbc89a9 100644
--- a/web-console/src/views/explore-view/models/query-source.ts
+++ b/web-console/src/views/explore-view/models/query-source.ts
@@ -224,7 +224,7 @@ export class QuerySource {
     );
   }
 
-  public transformToBaseColumns(expression: SqlExpression): SqlExpression {
+  public transformExpressionToBaseColumns(expression: SqlExpression): 
SqlExpression {
     const sourceToBaseSubstitutions = this.getSourceToBaseSubstitutions();
     return expression.walk(ex => {
       if (ex instanceof SqlColumn) {
diff --git 
a/web-console/src/views/explore-view/module-repository/module-repository.ts 
b/web-console/src/views/explore-view/module-repository/module-repository.ts
index 11bd25145a5..698dcf03672 100644
--- a/web-console/src/views/explore-view/module-repository/module-repository.ts
+++ b/web-console/src/views/explore-view/module-repository/module-repository.ts
@@ -38,6 +38,7 @@ interface ModuleComponentProps<P> {
   timezone: Timezone;
   where: SqlExpression;
   setWhere(where: SqlExpression): void;
+  moduleWhere: SqlExpression;
   parameterValues: P;
   setParameterValues: (parameters: Partial<P>) => void;
   runSqlQuery(
diff --git 
a/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
 
b/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
index bdf94e7196e..6d08c77f987 100644
--- 
a/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/bar-chart-module/bar-chart-module.tsx
@@ -96,7 +96,16 @@ ModuleRepository.registerModule<BarChartParameterValues>({
     },
   },
   component: function BarChartModule(props) {
-    const { querySource, timezone, where, setWhere, parameterValues, stage, 
runSqlQuery } = props;
+    const {
+      querySource,
+      timezone,
+      where,
+      setWhere,
+      moduleWhere,
+      parameterValues,
+      stage,
+      runSqlQuery,
+    } = props;
     const containerRef = useRef<HTMLDivElement>();
     const chartRef = useRef<ECharts>();
     const [highlight, setHighlight] = useState<BarChartHighlight | 
undefined>();
@@ -108,7 +117,7 @@ ModuleRepository.registerModule<BarChartParameterValues>({
 
       return {
         query: querySource
-          .getInitQuery(where)
+          .getInitQuery(where.and(moduleWhere))
           .addSelect(
             splitExpression.applyIf(timeBucket, ex => F.timeFloor(ex, 
timeBucket)).as('dim'),
             {
@@ -127,7 +136,17 @@ ModuleRepository.registerModule<BarChartParameterValues>({
           .changeLimitValue(limit),
         timezone,
       };
-    }, [querySource, timezone, where, splitColumn, timeBucket, measure, 
measureToSort, limit]);
+    }, [
+      querySource,
+      timezone,
+      where,
+      moduleWhere,
+      splitColumn,
+      timeBucket,
+      measure,
+      measureToSort,
+      limit,
+    ]);
 
     const [sourceDataState, queryManager] = useQueryManager({
       query: dataQuery,
diff --git 
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
 
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
index eeeee925249..a06b6152722 100644
--- 
a/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/grouping-table-module/grouping-table-module.tsx
@@ -47,7 +47,7 @@ const NEEDS_GROUPING_TO_ORDER = true;
 
 interface QueryAndMore {
   timezone: Timezone;
-  originalWhere: SqlExpression;
+  globalWhere: SqlExpression;
   queryAndHints: QueryAndHints;
 }
 
@@ -213,6 +213,7 @@ 
ModuleRepository.registerModule<GroupingTableParameterValues>({
       timezone,
       where,
       setWhere,
+      moduleWhere,
       parameterValues,
       setParameterValues,
       runSqlQuery,
@@ -224,13 +225,13 @@ 
ModuleRepository.registerModule<GroupingTableParameterValues>({
       if (!pivotColumn) return;
 
       return querySource
-        .getInitQuery(where)
+        .getInitQuery(where.and(moduleWhere))
         .addSelect(pivotColumn.expression.as('v'), { addToGroupBy: 'end' })
         .changeOrderByExpression(
           (measures.length ? measures[0].expression : 
F.count()).toOrderByExpression('DESC'),
         )
         .changeLimitValue(maxPivotValues);
-    }, [querySource, where, parameterValues]);
+    }, [querySource, where, moduleWhere, parameterValues]);
 
     const [pivotValueState, queryManager] = useQueryManager({
       query: pivotValueQuery,
@@ -249,10 +250,10 @@ 
ModuleRepository.registerModule<GroupingTableParameterValues>({
 
       return {
         timezone,
-        originalWhere: where,
+        globalWhere: where,
         queryAndHints: makeTableQueryAndHints({
           source: querySource.query,
-          where,
+          where: where.and(moduleWhere),
           splitColumns: parameterValues.splitColumns,
           timeBucket: parameterValues.timeBucket,
           showColumns: parameterValues.showColumns,
@@ -274,13 +275,13 @@ 
ModuleRepository.registerModule<GroupingTableParameterValues>({
     const [resultState] = useQueryManager({
       query: queryAndMore,
       processQuery: async (queryAndMore, cancelToken) => {
-        const { timezone, originalWhere, queryAndHints } = queryAndMore;
+        const { timezone, globalWhere, queryAndHints } = queryAndMore;
         const { query, columnHints } = queryAndHints;
         let result = await runSqlQuery({ query, timezone }, cancelToken);
         if (result.sqlQuery) {
           result = result.attachQuery(
             { query: '' },
-            result.sqlQuery.changeWhereExpression(originalWhere),
+            result.sqlQuery.changeWhereExpression(globalWhere),
           );
         }
         return {
diff --git 
a/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
 
b/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
index bffb93677aa..2dff7e42eeb 100644
--- 
a/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/multi-axis-chart-module/multi-axis-chart-module.tsx
@@ -71,7 +71,16 @@ 
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
     },
   },
   component: function MultiAxisChartModule(props) {
-    const { querySource, timezone, where, setWhere, parameterValues, stage, 
runSqlQuery } = props;
+    const {
+      querySource,
+      timezone,
+      where,
+      setWhere,
+      moduleWhere,
+      parameterValues,
+      stage,
+      runSqlQuery,
+    } = props;
     const containerRef = useRef<HTMLDivElement>();
     const chartRef = useRef<ECharts>();
     const [highlight, setHighlight] = useState<MultiAxisChartHighlight | 
undefined>();
@@ -87,7 +96,7 @@ 
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
     const dataQuery = useMemo(() => {
       return {
         query: querySource
-          .getInitQuery(where)
+          .getInitQuery(where.and(moduleWhere))
           .addSelect(F.timeFloor(C(timeColumnName || '__time'), 
L(timeGranularity)).as('time'), {
             addToGroupBy: 'end',
             addToOrderBy: 'end',
@@ -96,7 +105,7 @@ 
ModuleRepository.registerModule<MultiAxisChartParameterValues>({
           .applyForEach(measures, (q, measure) => 
q.addSelect(measure.expression.as(measure.name))),
         timezone,
       };
-    }, [querySource, timezone, where, timeColumnName, timeGranularity, 
measures]);
+    }, [querySource, timezone, where, moduleWhere, timeColumnName, 
timeGranularity, measures]);
 
     const [sourceDataState, queryManager] = useQueryManager({
       query: dataQuery,
diff --git 
a/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
 
b/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
index 69c9b927879..b98c876fb33 100644
--- 
a/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/pie-chart-module/pie-chart-module.tsx
@@ -97,7 +97,8 @@ ModuleRepository.registerModule<PieChartParameterValues>({
     },
   },
   component: function PieChartModule(props) {
-    const { querySource, where, setWhere, parameterValues, stage, runSqlQuery 
} = props;
+    const { querySource, where, setWhere, moduleWhere, parameterValues, stage, 
runSqlQuery } =
+      props;
     const containerRef = useRef<HTMLDivElement>();
     const chartRef = useRef<ECharts>();
     const [highlight, setHighlight] = useState<PieChartHighlight | 
undefined>();
@@ -106,34 +107,43 @@ ModuleRepository.registerModule<PieChartParameterValues>({
 
     const dataQueries = useMemo(() => {
       const splitExpression = splitColumn ? splitColumn.expression : 
L(OVERALL_LABEL);
+      const effectiveWhere = where.and(moduleWhere);
 
       return {
         mainQuery: querySource
-          .getInitQuery(where)
+          .getInitQuery(effectiveWhere)
           .addSelect(F.cast(splitExpression, 'VARCHAR').as('name'), { 
addToGroupBy: 'end' })
           .addSelect(measure.expression.as('value'), {
             addToOrderBy: 'end',
             direction: 'DESC',
           })
-          .changeLimitValue(limit),
+          .changeLimitValue(limit + (showOthers ? 1 : 0)),
+        limit,
         splitExpression: splitColumn?.expression,
         othersPartialQuery: showOthers
-          ? 
querySource.getInitQuery(where).addSelect(measure.expression.as('value'))
+          ? 
querySource.getInitQuery(effectiveWhere).addSelect(measure.expression.as('value'))
           : undefined,
       };
-    }, [querySource, where, splitColumn, measure, limit, showOthers]);
+    }, [querySource, where, moduleWhere, splitColumn, measure, limit, 
showOthers]);
 
     const [sourceDataState, queryManager] = useQueryManager({
       query: dataQueries,
-      processQuery: async ({ mainQuery, splitExpression, othersPartialQuery }, 
cancelToken) => {
+      processQuery: async (
+        { mainQuery, limit, splitExpression, othersPartialQuery },
+        cancelToken,
+      ) => {
         const result = await runSqlQuery({ query: mainQuery }, cancelToken);
         const data = result.toObjectArray();
 
         if (splitExpression && othersPartialQuery) {
-          const othersResult = await runSqlQuery({
-            query: 
othersPartialQuery.addWhere(splitExpression.notIn(result.getColumnByIndex(0)!)),
-          });
-          data.push({ name: 'Others', value: othersResult.rows[0][0], 
__isOthers: true });
+          const pieValues = result.getColumnByIndex(0)!;
+
+          if (pieValues.length > limit) {
+            const othersResult = await runSqlQuery({
+              query: 
othersPartialQuery.addWhere(splitExpression.notIn(pieValues.slice(0, limit))),
+            });
+            data.push({ name: 'Others', value: othersResult.rows[0][0], 
__isOthers: true });
+          }
         }
 
         return data;
diff --git 
a/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
 
b/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
index c9d842dfb9c..822f01c329e 100644
--- 
a/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/record-table-module/record-table-module.tsx
@@ -67,11 +67,20 @@ 
ModuleRepository.registerModule<RecordTableParameterValues>({
     },
   },
   component: function RecordTableModule(props) {
-    const { stage, querySource, timezone, where, setWhere, parameterValues, 
runSqlQuery } = props;
+    const {
+      stage,
+      querySource,
+      timezone,
+      where,
+      setWhere,
+      moduleWhere,
+      parameterValues,
+      runSqlQuery,
+    } = props;
 
     const query = useMemo((): string | undefined => {
       return SqlQuery.create(querySource.query)
-        .changeWhereExpression(where)
+        .changeWhereExpression(where.and(moduleWhere))
         .changeLimitValue(parameterValues.maxRows)
         .applyIf(
           querySource.columns.some(e => e.name === '__time') && 
!parameterValues.ascending,
@@ -79,7 +88,7 @@ ModuleRepository.registerModule<RecordTableParameterValues>({
           q => q.changeOrderByClause(querySource.query.orderByClause),
         )
         .toString();
-    }, [querySource, where, parameterValues]);
+    }, [querySource, where, moduleWhere, parameterValues]);
 
     const [resultState, queryManager] = useQueryManager({
       query,
diff --git 
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
 
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
index 5f7be42df95..63f23758091 100644
--- 
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
+++ 
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.scss
@@ -18,6 +18,8 @@
 
 @import '../../../../variables';
 
+$default-chart-color: $druid-brand;
+
 .continuous-chart-render {
   position: relative;
   overflow: hidden;
@@ -30,10 +32,6 @@
       user-select: none;
     }
 
-    .mark-bar {
-      fill: #497ee6;
-    }
-
     .selection {
       fill: white;
       fill-opacity: 0.1;
@@ -89,25 +87,29 @@
       opacity: 0.7;
     }
 
+    .mark-bar {
+      fill: #00b6c3;
+    }
+
     .mark-line {
       stroke-width: 1.5px;
-      stroke: #497ee6;
+      stroke: $default-chart-color;
       fill: none;
     }
 
     .mark-area {
-      fill: #497ee6;
-      opacity: 0.7;
+      fill: $default-chart-color;
+      opacity: 0.5;
     }
 
     .single-point {
-      stroke: #497ee6;
+      stroke: $default-chart-color;
       opacity: 0.7;
       stroke-width: 1.5px;
     }
 
     .selected-point {
-      fill: #497ee6;
+      fill: $default-chart-color;
     }
   }
 
diff --git 
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
 
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
index 378c652142e..c0d759530d5 100644
--- 
a/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
+++ 
b/web-console/src/views/explore-view/modules/time-chart-module/continuous-chart-render.tsx
@@ -42,6 +42,7 @@ import {
   formatStartDuration,
   groupBy,
   lookupBy,
+  minBy,
   tickFormatWithTimezone,
   timezoneAwareTicks,
 } from '../../../../utils';
@@ -73,7 +74,7 @@ export interface RangeDatum {
   start: number;
   end: number;
   measure: number;
-  stack: string | undefined;
+  facet: string | undefined;
 }
 
 export interface StackedRangeDatum extends RangeDatum {
@@ -126,7 +127,7 @@ export interface ContinuousChartRenderProps {
    * If stacking is used then the stack bars should be ordered bottom to top.
    */
   data: RangeDatum[];
-  stacks: string[] | undefined;
+  facets: string[] | undefined;
 
   /**
    * The granularity that was used for bucketing.
@@ -161,7 +162,7 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
 ) {
   const {
     data,
-    stacks,
+    facets,
     granularity,
 
     markType,
@@ -199,10 +200,10 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
   const svgRef = useRef<SVGSVGElement | null>(null);
 
   const stackedData: StackedRangeDatum[] = useMemo(() => {
-    const effectiveStacks = stacks || ['undefined'];
-    const stackToIndex = lookupBy(
-      effectiveStacks,
-      s => s,
+    const effectiveFacet = facets || ['undefined'];
+    const facetToIndex = lookupBy(
+      effectiveFacet,
+      f => f,
       (_, i) => i,
     );
 
@@ -211,7 +212,7 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
       const diffStart = b.start - a.start;
       if (diffStart) return diffStart;
 
-      return stackToIndex[String(a.stack)] - stackToIndex[String(b.stack)];
+      return facetToIndex[String(a.facet)] - facetToIndex[String(b.facet)];
     });
 
     if (markType === 'line') {
@@ -230,18 +231,26 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
         return withOffset;
       });
     }
-  }, [data, stacks, markType]);
+  }, [data, facets, markType]);
 
-  function findStackedDatum(time: number, measure: number): StackedRangeDatum 
| undefined {
+  function findStackedDatum(
+    time: number,
+    measure: number,
+    isStacked: boolean,
+  ): StackedRangeDatum | undefined {
     const dataInRange = stackedData.filter(d => d.start <= time && time < 
d.end);
     if (!dataInRange.length) return;
-    return (
-      dataInRange.find(r => r.offset <= measure && measure < r.measure + 
r.offset) ||
-      dataInRange[dataInRange.length - 1]
-    );
+    if (isStacked) {
+      return (
+        dataInRange.find(r => r.offset <= measure && measure < r.measure + 
r.offset) ||
+        dataInRange[dataInRange.length - 1]
+      );
+    } else {
+      return minBy(dataInRange, r => Math.abs(r.measure - measure));
+    }
   }
 
-  const stackColorizer = useMemo(() => {
+  const facetColorizer = useMemo(() => {
     const s = scaleOrdinal(COLORS);
     return (v: string) => (v === OTHER_VALUE ? OTHER_COLOR : s(v));
   }, []);
@@ -342,7 +351,7 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
         setSelectionIfNeeded({
           start: start.valueOf(),
           end: end.valueOf(),
-          selectedDatum: findStackedDatum(time, measure),
+          selectedDatum: findStackedDatum(time, measure, markType !== 'line'),
         });
       } else {
         setSelection(undefined);
@@ -389,40 +398,43 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
     }
   });
 
-  const byStack = useMemo(() => {
+  const byFacet = useMemo(() => {
     if (markType === 'bar' || !stackedData.length) return [];
+    const isStacked = markType !== 'line';
 
-    const effectiveStacks = stacks || ['undefined'];
-    const numStacks = effectiveStacks.length;
+    const effectiveFacets = facets || ['undefined'];
+    const numFacets = effectiveFacets.length;
 
-    // Fill in 0s and make sure that the stacks are in the same order
+    // Fill in 0s and make sure that the facets are in the same order
     const fullTimeIntervals = groupBy(
       stackedData,
       d => String(d.start),
       dataForStart => {
-        if (numStacks === 1) return [dataForStart[0]];
-        const stackToDatum = lookupBy(dataForStart, d => d.stack!);
-        return effectiveStacks.map(
-          (stack, stackIndex) =>
-            stackToDatum[stack] || {
+        if (numFacets === 1) return [dataForStart[0]];
+        const facetToDatum = lookupBy(dataForStart, d => d.facet!);
+        return effectiveFacets.map(
+          (facet, facetIndex) =>
+            facetToDatum[facet] || {
               ...dataForStart[0],
-              stack,
+              facet,
               measure: 0,
-              offset: Math.max(
-                0,
-                ...filterMap(effectiveStacks.slice(0, stackIndex), s => 
stackToDatum[s]).map(
-                  d => d.offset + d.measure,
-                ),
-              ),
+              offset: isStacked
+                ? Math.max(
+                    0,
+                    ...filterMap(effectiveFacets.slice(0, facetIndex), s => 
facetToDatum[s]).map(
+                      d => d.offset + d.measure,
+                    ),
+                  )
+                : 0,
             },
         );
       },
     );
 
     // Add nulls to mark gaps in data
-    const seriesForStack: Record<string, (StackedRangeDatum | null)[]> = {};
-    for (const stack of effectiveStacks) {
-      seriesForStack[stack] = [];
+    const seriesForFacet: Record<string, (StackedRangeDatum | null)[]> = {};
+    for (const stack of effectiveFacets) {
+      seriesForFacet[stack] = [];
     }
 
     let lastDatum: StackedRangeDatum | undefined;
@@ -430,19 +442,19 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
       const datum = fullTimeInterval[0];
 
       if (lastDatum && lastDatum.start !== datum.end) {
-        for (const stack of effectiveStacks) {
-          seriesForStack[stack].push(null);
+        for (const facet of effectiveFacets) {
+          seriesForFacet[facet].push(null);
         }
       }
 
-      for (let i = 0; i < numStacks; i++) {
-        seriesForStack[effectiveStacks[i]].push(fullTimeInterval[i]);
+      for (let i = 0; i < numFacets; i++) {
+        seriesForFacet[effectiveFacets[i]].push(fullTimeInterval[i]);
       }
       lastDatum = datum;
     }
 
-    return Object.values(seriesForStack);
-  }, [markType, stackedData, stacks]);
+    return Object.values(seriesForFacet);
+  }, [markType, stackedData, facets]);
 
   if (innerStage.isInvalid()) return;
 
@@ -531,7 +543,7 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
       title,
       text: (
         <>
-          {selectedDatum?.stack && <div>{selectedDatum?.stack}</div>}
+          {selectedDatum?.facet && <div>{selectedDatum?.facet}</div>}
           <div>{info}</div>
           {selection.finalized && (
             <div className="button-bar">
@@ -605,13 +617,13 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
                 if (!r) return;
                 return (
                   <rect
-                    
key={`${stackedRow.start}/${stackedRow.end}/${stackedRow.stack}`}
+                    
key={`${stackedRow.start}/${stackedRow.end}/${stackedRow.facet}`}
                     className="mark-bar"
                     {...r}
                     style={
-                      typeof stackedRow.stack !== 'undefined'
+                      typeof stackedRow.facet !== 'undefined'
                         ? {
-                            fill: stackColorizer(stackedRow.stack),
+                            fill: facetColorizer(stackedRow.facet),
                           }
                         : undefined
                     }
@@ -625,17 +637,17 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
               />
             )}
             {markType === 'area' &&
-              byStack.map(ds => {
-                const stack = ds[0]!.stack;
+              byFacet.map(ds => {
+                const facet = ds[0]!.facet;
                 return (
                   <path
-                    key={String(stack)}
+                    key={String(facet)}
                     className="mark-area"
                     d={areaFn(ds)!}
                     style={
-                      typeof stack !== 'undefined'
+                      typeof facet !== 'undefined'
                         ? {
-                            fill: stackColorizer(stack),
+                            fill: facetColorizer(facet),
                           }
                         : undefined
                     }
@@ -643,17 +655,17 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
                 );
               })}
             {(markType === 'area' || markType === 'line') &&
-              byStack.map(ds => {
-                const stack = ds[0]!.stack;
+              byFacet.map(ds => {
+                const facet = ds[0]!.facet;
                 return (
                   <path
-                    key={String(stack)}
+                    key={String(facet)}
                     className="mark-line"
                     d={lineFn(ds)!}
                     style={
-                      typeof stack !== 'undefined'
+                      typeof facet !== 'undefined'
                         ? {
-                            stroke: stackColorizer(stack),
+                            stroke: facetColorizer(facet),
                           }
                         : undefined
                     }
@@ -661,22 +673,22 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
                 );
               })}
             {(markType === 'area' || markType === 'line') &&
-              byStack.flatMap(ds =>
+              byFacet.flatMap(ds =>
                 filterMap(ds, (d, i) => {
                   if (!d || ds[i - 1] || ds[i + 1]) return; // Not a single 
point
                   const x = timeScale((d.start + d.end) / 2);
                   return (
                     <line
-                      key={`single_${i}_${d.stack}`}
+                      key={`single_${i}_${d.facet}`}
                       className="single-point"
                       x1={x}
                       x2={x}
                       y1={measureScale(d.measure + d.offset)}
                       y2={measureScale(d.offset)}
                       style={
-                        typeof d.stack !== 'undefined'
+                        typeof d.facet !== 'undefined'
                           ? {
-                              stroke: stackColorizer(d.stack),
+                              stroke: facetColorizer(d.facet),
                             }
                           : undefined
                       }
@@ -690,9 +702,9 @@ export const ContinuousChartRender = function 
ContinuousChartRender(
                 {...datumToCxCy(selection.selectedDatum)}
                 r={3}
                 style={
-                  typeof selection.selectedDatum.stack !== 'undefined'
+                  typeof selection.selectedDatum.facet !== 'undefined'
                     ? {
-                        fill: stackColorizer(selection.selectedDatum.stack),
+                        fill: facetColorizer(selection.selectedDatum.facet),
                       }
                     : undefined
                 }
diff --git 
a/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
 
b/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
index 5ac00a310d0..cd97284a770 100644
--- 
a/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
+++ 
b/web-console/src/views/explore-view/modules/time-chart-module/time-chart-module.tsx
@@ -45,7 +45,7 @@ import { ContinuousChartRender, OTHER_VALUE } from 
'./continuous-chart-render';
 
 const TIME_NAME = 't';
 const MEASURE_NAME = 'm';
-const STACK_NAME = 's';
+const FACET_NAME = 'f';
 const MIN_SLICE_WIDTH = 8;
 
 function getRangeInExpression(
@@ -92,12 +92,12 @@ function getRangeInExpression(
 }
 
 interface TimeChartParameterValues {
+  markType: ContinuousChartMarkType;
   granularity: string;
-  splitColumn?: ExpressionMeta;
-  numberToStack: number;
+  facetColumn?: ExpressionMeta;
+  maxFacets: number;
   showOthers: boolean;
   measure: ExpressionMeta;
-  markType: ContinuousChartMarkType;
   curveType: ContinuousChartCurveType;
 }
 
@@ -106,6 +106,12 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
   title: 'Time chart',
   icon: IconNames.TIMELINE_LINE_CHART,
   parameters: {
+    markType: {
+      type: 'option',
+      options: ['line', 'area', 'bar'],
+      defaultValue: 'line',
+      optionLabels: capitalizeFirst,
+    },
     granularity: {
       type: 'option',
       options: ({ querySource, where }) => {
@@ -132,24 +138,25 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
       important: true,
       optionLabels: g => (g === 'auto' ? 'Auto' : new 
Duration(g).getDescription(true)),
     },
-    splitColumn: {
+    facetColumn: {
       type: 'expression',
-      label: 'Stack by',
+      label: 'Facet by',
       transferGroup: 'show',
       important: true,
+      legacyName: 'splitColumn',
     },
-    numberToStack: {
+    maxFacets: {
       type: 'number',
-      label: 'Max stacks',
       defaultValue: 7,
-      min: 2,
+      min: 1,
       required: true,
-      visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+      visible: ({ parameterValues }) => Boolean(parameterValues.facetColumn),
+      legacyName: 'numberToStack',
     },
     showOthers: {
       type: 'boolean',
       defaultValue: true,
-      visible: ({ parameterValues }) => Boolean(parameterValues.splitColumn),
+      visible: ({ parameterValues }) => Boolean(parameterValues.facetColumn),
     },
     measure: {
       type: 'measure',
@@ -159,12 +166,6 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
       defaultValue: ({ querySource }) => 
querySource?.getFirstAggregateMeasure(),
       required: true,
     },
-    markType: {
-      type: 'option',
-      options: ['area', 'bar', 'line'],
-      defaultValue: 'area',
-      optionLabels: capitalizeFirst,
-    },
     curveType: {
       type: 'option',
       options: ['smooth', 'linear', 'step'],
@@ -174,7 +175,16 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
     },
   },
   component: function TimeChartModule(props) {
-    const { querySource, timezone, where, setWhere, parameterValues, stage, 
runSqlQuery } = props;
+    const {
+      querySource,
+      timezone,
+      where,
+      setWhere,
+      moduleWhere,
+      parameterValues,
+      stage,
+      runSqlQuery,
+    } = props;
 
     const timeColumnName = querySource.columns.find(column => column.sqlType 
=== 'TIMESTAMP')?.name;
     const timeGranularity =
@@ -186,17 +196,18 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
           )
         : parameterValues.granularity;
 
-    const { splitColumn, numberToStack, showOthers, measure, markType } = 
parameterValues;
+    const { facetColumn, maxFacets, showOthers, measure, markType } = 
parameterValues;
 
     const dataQuery = useMemo(() => {
       return {
         querySource,
         timezone,
         where,
+        moduleWhere,
         timeGranularity,
         measure,
-        splitExpression: splitColumn?.expression,
-        numberToStack,
+        facetExpression: facetColumn?.expression,
+        maxFacets,
         showOthers,
         oneExtra: markType !== 'bar',
       };
@@ -204,10 +215,11 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
       querySource,
       timezone,
       where,
+      moduleWhere,
       timeGranularity,
       measure,
-      splitColumn,
-      numberToStack,
+      facetColumn,
+      maxFacets,
       showOthers,
       markType,
     ]);
@@ -219,10 +231,11 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
           querySource,
           timezone,
           where,
+          moduleWhere,
           timeGranularity,
           measure,
-          splitExpression,
-          numberToStack,
+          facetExpression,
+          maxFacets,
           showOthers,
           oneExtra,
         },
@@ -232,17 +245,20 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
           throw new Error(`Must have a column of type TIMESTAMP for the time 
chart to work`);
         }
 
+        const effectiveWhere = where.and(moduleWhere);
         const granularity = new Duration(timeGranularity);
 
-        const vs = splitExpression
+        const detectedFacets: string[] | undefined = facetExpression
           ? (
               await runSqlQuery(
                 {
                   query: querySource
-                    .getInitQuery(where)
-                    .addSelect(splitExpression.cast('VARCHAR').as('v'), { 
addToGroupBy: 'end' })
+                    .getInitQuery(effectiveWhere)
+                    .addSelect(facetExpression.cast('VARCHAR').as(FACET_NAME), 
{
+                      addToGroupBy: 'end',
+                    })
                     
.changeOrderByExpression(measure.expression.toOrderByExpression('DESC'))
-                    .changeLimitValue(numberToStack),
+                    .changeLimitValue(maxFacets + (showOthers ? 1 : 0)), // If 
we want to show others add 1 to check if we need to query for them
                   timezone,
                 },
                 cancelToken,
@@ -252,44 +268,54 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
 
         cancelToken.throwIfRequested();
 
-        if (vs?.length === 0) {
-          // If vs is empty then there is no data at all and no need to do a 
larger query
+        if (detectedFacets?.length === 0) {
+          // If detectedFacets is empty then there is no data at all and no 
need to do a larger query
           return {
-            effectiveVs: [],
+            effectiveFacets: [],
             sourceData: [],
             measure,
             granularity,
           };
         }
 
-        const effectiveVs = vs && showOthers ? vs.concat(OTHER_VALUE) : vs;
+        const facetsToQuery =
+          showOthers && detectedFacets && maxFacets < detectedFacets.length
+            ? detectedFacets.slice(0, maxFacets)
+            : undefined;
+        const effectiveFacets = facetsToQuery ? 
facetsToQuery.concat(OTHER_VALUE) : detectedFacets;
 
         const result = await runSqlQuery(
           {
             query: querySource
-              .getInitQuery(overqueryWhere(where, timeColumnName, granularity, 
oneExtra))
-              .applyIf(splitExpression && vs && !showOthers, q =>
-                q.addWhere(splitExpression!.cast('VARCHAR').in(vs!)),
+              .getInitQuery(overqueryWhere(effectiveWhere, timeColumnName, 
granularity, oneExtra))
+              .applyIf(facetExpression && detectedFacets && !facetsToQuery, q 
=>
+                
q.addWhere(facetExpression!.cast('VARCHAR').in(detectedFacets!)),
               )
               .addSelect(F.timeFloor(C(timeColumnName), 
L(timeGranularity)).as(TIME_NAME), {
                 addToGroupBy: 'end',
                 addToOrderBy: 'end',
                 direction: 'DESC',
               })
-              .applyIf(splitExpression, q => {
-                if (!splitExpression || !vs) return q; // Should never get 
here, doing this to make peace between eslint and TS
+              .applyIf(facetExpression, q => {
+                if (!facetExpression) return q; // Should never get here, 
doing this to make peace between eslint and TS
                 return q.addSelect(
-                  (showOthers
-                    ? SqlCase.ifThenElse(splitExpression.in(vs), 
splitExpression, L(OTHER_VALUE))
-                    : splitExpression
+                  (facetsToQuery
+                    ? SqlCase.ifThenElse(
+                        facetExpression.in(facetsToQuery),
+                        facetExpression,
+                        L(OTHER_VALUE),
+                      )
+                    : facetExpression
                   )
                     .cast('VARCHAR')
-                    .as(STACK_NAME),
+                    .as(FACET_NAME),
                   { addToGroupBy: 'end' },
                 );
               })
               .addSelect(measure.expression.as(MEASURE_NAME))
-              .changeLimitValue(10000 * (effectiveVs ? 
Math.min(effectiveVs.length, 10) : 1)),
+              .changeLimitValue(
+                10000 * (effectiveFacets ? Math.min(effectiveFacets.length, 
10) : 1),
+              ),
             timezone,
           },
           cancelToken,
@@ -300,12 +326,12 @@ 
ModuleRepository.registerModule<TimeChartParameterValues>({
             start: b[TIME_NAME].valueOf(),
             end: granularity.shift(b[TIME_NAME], Timezone.UTC, 1).valueOf(),
             measure: b[MEASURE_NAME],
-            stack: b[STACK_NAME],
+            facet: b[FACET_NAME],
           }),
         );
 
         return {
-          effectiveVs,
+          effectiveFacets,
           sourceData: dataset,
           measure,
           granularity,
@@ -326,7 +352,7 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
         {sourceData && (
           <ContinuousChartRender
             data={sourceData.sourceData}
-            stacks={sourceData.effectiveVs}
+            facets={sourceData.effectiveFacets}
             granularity={sourceData.granularity}
             markType={parameterValues.markType}
             curveType={parameterValues.curveType}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to