diff --git a/sass/components/canvas/attribute_editor.scss b/sass/components/canvas/attribute_editor.scss
index f54282cb..458a07a6 100644
--- a/sass/components/canvas/attribute_editor.scss
+++ b/sass/components/canvas/attribute_editor.scss
@@ -1803,7 +1803,8 @@
font-size: 14px;
padding: 0 10px;
min-width: 100px;
-
+ max-height: 700px;
+ max-width: 500px;
.el-row {
margin: 10px 0;
}
diff --git a/src/app/stores/app_store.ts b/src/app/stores/app_store.ts
index a28b155b..1ff80b4f 100644
--- a/src/app/stores/app_store.ts
+++ b/src/app/stores/app_store.ts
@@ -18,6 +18,7 @@ import {
Specification,
uniqueID,
zipArray,
+ makeRange,
} from "../../core";
import { BaseStore } from "../../core/store/base";
import { CharticulatorWorkerInterface } from "../../worker";
@@ -60,6 +61,7 @@ import {
} from "../../core/specification";
import { RenderEvents } from "../../core/graphics";
import {
+ AxisDataBinding,
AxisDataBindingType,
AxisRenderingStyle,
NumericalMode,
@@ -84,8 +86,14 @@ import {
import { LineGuideProperties } from "../../core/prototypes/plot_segments/line";
import { DataAxisProperties } from "../../core/prototypes/marks/data_axis.attrs";
import { isBase64Image } from "../../core/dataset/data_types";
-import { getColumnNameByExpression } from "../../core/prototypes/plot_segments/utils";
+import {
+ getColumnNameByExpression,
+ parseDerivedColumnsExpression,
+ transformOrderByExpression,
+ updateWidgetCategoriesByExpression,
+} from "../../core/prototypes/plot_segments/utils";
import { AxisRenderer } from "../../core/prototypes/plot_segments/axis";
+import { CompiledGroupBy } from "../../core/prototypes/group_by";
export interface ChartStoreStateSolverStatus {
solving: boolean;
@@ -1949,8 +1957,15 @@ export class AppStore extends BaseStore {
dataExpression.valueType,
values
);
-
- dataBinding.orderByCategories = deepClone(categories);
+ try {
+ dataBinding.orderByCategories = this.getCategoriesForOrderByColumn(
+ dataBinding.orderByExpression,
+ dataBinding.expression,
+ dataBinding
+ );
+ } catch (e) {
+ dataBinding.orderByCategories = deepClone(categories);
+ }
dataBinding.order = order != undefined ? order : null;
dataBinding.allCategories = deepClone(categories);
@@ -2137,6 +2152,47 @@ export class AppStore extends BaseStore {
}
}
+ public getCategoriesForOrderByColumn(
+ orderExpression: string,
+ expression: string,
+ data: AxisDataBinding
+ ) {
+ const parsed = Expression.parse(expression);
+ let groupByExpression: string = null;
+ if (parsed instanceof Expression.FunctionCall) {
+ groupByExpression = parsed.args[0].toString();
+ groupByExpression = groupByExpression?.split("`").join("");
+ //need to provide date.year() etc.
+ groupByExpression = parseDerivedColumnsExpression(groupByExpression);
+ }
+ const table = this.getTables()[0].name;
+
+ const df = new Prototypes.Dataflow.DataflowManager(this.dataset);
+ const getExpressionVector = (
+ expression: string,
+ table: string,
+ groupBy?: Specification.Types.GroupBy
+ ): any[] => {
+ const newExpression = transformOrderByExpression(expression);
+ groupBy.expression = transformOrderByExpression(groupBy.expression);
+
+ const expr = Expression.parse(newExpression);
+ const tableContext = df.getTable(table);
+ const indices = groupBy
+ ? new CompiledGroupBy(groupBy, df.cache).groupBy(tableContext)
+ : makeRange(0, tableContext.rows.length).map((x) => [x]);
+ return indices.map((is) =>
+ expr.getValue(tableContext.getGroupedContext(is))
+ );
+ };
+ const vectorData = getExpressionVector(data.orderByExpression, table, {
+ expression: groupByExpression,
+ });
+ const items = vectorData.map((item) => [...new Set(item)]);
+ const newData = updateWidgetCategoriesByExpression(items);
+ return [...new Set(newData)];
+ }
+
public getCategoriesForDataBinding(
metadata: Dataset.ColumnMetadata,
type: DataType,
diff --git a/src/app/views/panels/widgets/controls/fluentui_reorder_string_value.tsx b/src/app/views/panels/widgets/controls/fluentui_reorder_string_value.tsx
index e0412dad..aff65725 100644
--- a/src/app/views/panels/widgets/controls/fluentui_reorder_string_value.tsx
+++ b/src/app/views/panels/widgets/controls/fluentui_reorder_string_value.tsx
@@ -5,9 +5,10 @@ import * as React from "react";
import { ReorderListView } from "../../object_list_editor";
import { ButtonRaised } from "../../../../components";
import { strings } from "../../../../../strings";
-import { DefaultButton } from "@fluentui/react";
+import { DefaultButton, TooltipHost } from "@fluentui/react";
import { defultComponentsHeight } from "./fluentui_customized_components";
import { getRandomNumber } from "../../../../../core";
+import { DataType } from "../../../../../core/specification";
interface ReorderStringsValueProps {
items: string[];
@@ -16,8 +17,13 @@ interface ReorderStringsValueProps {
customOrder: boolean,
sortOrder: boolean
) => void;
+ sortedCategories?: string[];
allowReset?: boolean;
onReset?: () => string[];
+ itemsDataType?: DataType.Number | DataType.String;
+ allowDragItems?: boolean;
+ onReorderHandler?: () => void;
+ onButtonHandler?: () => void;
}
interface ReorderStringsValueState {
@@ -43,20 +49,48 @@ export class FluentUIReorderStringsValue extends React.Component<
{
ReorderListView.ReorderArray(items, a, b);
this.setState({ items, customOrder: true, sortOrder: false });
+ if (this.props.onReorderHandler) {
+ this.props.onReorderHandler();
+ }
}}
>
{items.map((x) => (
- {x}
+ {x}
))}
+ {
+ this.setState({
+ items:
+ [...this.props.sortedCategories] ?? this.state.items.sort(),
+ customOrder: false,
+ sortOrder: true,
+ });
+ if (this.props.onButtonHandler) {
+ this.props.onButtonHandler();
+ }
+ }}
+ styles={{
+ root: {
+ minWidth: "unset",
+ ...defultComponentsHeight,
+ padding: 0,
+ marginRight: 5,
+ },
+ }}
+ />
- {
- this.setState({
- items: this.state.items.sort(),
- customOrder: false,
- sortOrder: true,
- });
- }}
- styles={{
- root: {
- minWidth: "unset",
- ...defultComponentsHeight,
- padding: 0,
- marginRight: 5,
- },
+ if (this.props.onButtonHandler) {
+ this.props.onButtonHandler();
+ }
}}
/>
{this.props.allowReset && (
@@ -120,6 +136,9 @@ export class FluentUIReorderStringsValue extends React.Component<
customOrder: false,
sortOrder: false,
});
+ if (this.props.onButtonHandler) {
+ this.props.onButtonHandler();
+ }
}
}}
/>
diff --git a/src/app/views/panels/widgets/fluentui_manager.tsx b/src/app/views/panels/widgets/fluentui_manager.tsx
index 3588fbed..91d8a4f1 100644
--- a/src/app/views/panels/widgets/fluentui_manager.tsx
+++ b/src/app/views/panels/widgets/fluentui_manager.tsx
@@ -25,7 +25,7 @@ import {
import { Actions, DragData } from "../../../actions";
import { ButtonRaised } from "../../../components";
import { SVGImageIcon } from "../../../components/icons";
-import { getAlignment, PopupView } from "../../../controllers";
+import { getAlignment, PopupAlignment, PopupView } from "../../../controllers";
import {
DragContext,
DragModifiers,
@@ -117,6 +117,7 @@ import { FluentUIReorderStringsValue } from "./controls/fluentui_reorder_string_
import { InputColorGradient } from "./controls/input_gradient";
import { dropdownStyles, onRenderOption, onRenderTitle } from "./styles";
import { getDropzoneAcceptTables } from "./utils";
+import { EditorType } from "../../../stores/app_store";
export type OnEditMappingHandler = (
attribute: string,
@@ -1781,7 +1782,13 @@ export class FluentUIWidgetManager
);
},
- { anchor: container }
+ {
+ anchor: container,
+ alignX:
+ this.store.editorType == EditorType.Embedded
+ ? PopupAlignment.EndInner
+ : PopupAlignment.StartInner,
+ }
);
}}
/>
diff --git a/src/core/prototypes/controls.ts b/src/core/prototypes/controls.ts
index 3df2925e..44aa68bd 100644
--- a/src/core/prototypes/controls.ts
+++ b/src/core/prototypes/controls.ts
@@ -7,6 +7,7 @@ import * as Specification from "../specification";
import * as Dataset from "../dataset";
import { CSSProperties } from "react";
import { ICheckboxStyles, IDropdownOption } from "@fluentui/react";
+import { DataType } from "../specification";
export type Widget = any;
@@ -269,6 +270,11 @@ export interface ReOrderWidgetOptions {
items?: string[];
onConfirmClick?: (items: string[]) => void;
onResetCategories?: string[];
+ sortedCategories?: string[];
+ itemsDataType?: DataType.Number | DataType.String;
+ allowDragItems?: boolean;
+ onReorderHandler?: () => void;
+ onButtonHandler?: () => void;
}
export interface InputFormatOptions {
diff --git a/src/core/prototypes/plot_segments/axis.ts b/src/core/prototypes/plot_segments/axis.ts
index 29bc7429..72c60f02 100644
--- a/src/core/prototypes/plot_segments/axis.ts
+++ b/src/core/prototypes/plot_segments/axis.ts
@@ -41,7 +41,6 @@ import {
AxisDataBinding,
AxisDataBindingType,
NumericalMode,
- OrderMode,
} from "../../specification/types";
import { VirtualScrollBar, VirtualScrollBarPropertes } from "./virtualScroll";
import {
@@ -49,6 +48,11 @@ import {
parseDerivedColumnsExpression,
shouldShowTickFormatForTickExpression,
transformOrderByExpression,
+ CategoryItemsWithIds,
+ getOnConfirmFunction,
+ transformOnResetCategories,
+ updateWidgetCategoriesByExpression,
+ getSortedCategories,
} from "./utils";
import { DataflowManager, DataflowTable } from "../dataflow";
import * as Expression from "../../expression";
@@ -2152,6 +2156,7 @@ function applySelectionFilter(
}
return filteredIndices;
}
+let orderChanged = false;
function getOrderByAnotherColumnWidgets(
data: Specification.Types.AxisDataBinding,
@@ -2164,7 +2169,7 @@ function getOrderByAnotherColumnWidgets(
manager as Controls.WidgetManager & CharticulatorPropertyAccessors
);
- const columnsDisplayNames = tableColumns
+ let columnsDisplayNames = tableColumns
.filter((item) => !item.metadata?.isRaw)
.map((column) => column.displayName);
const columnsNames = tableColumns
@@ -2240,47 +2245,27 @@ function getOrderByAnotherColumnWidgets(
groupByExpression = parseDerivedColumnsExpression(groupByExpression);
}
+ const isOriginalColumn = groupByExpression === data.orderByExpression;
const vectorData = getExpressionVector(data.orderByExpression, table, {
expression: groupByExpression,
});
const items = vectorData.map((item) => [...new Set(item)]);
- const items_idx = items.map((item, idx) => [item, idx]);
+ const items_idx: CategoryItemsWithIds = items.map((item, idx) => [item, idx]);
const axisData = getExpressionVector(data.expression, table, {
expression: groupByExpression,
}).map((item, idx) => [item, idx]);
- const rawAxisData = items_idx.map((item) =>
- Array.isArray(item[0]) ? item[0].join(", ") : item[0].toString()
- );
+ const isNumberValueType = Array.isArray(items_idx[0][0])
+ ? typeof items_idx[0][0][0] === "number"
+ : typeof items_idx[0][0] === "number";
+
+ const onResetAxisCategories = transformOnResetCategories(items_idx);
+ const sortedCategories = getSortedCategories(items_idx);
const onConfirm = (items: string[]) => {
try {
- const newData = [...axisData];
- const new_order = [];
-
- for (let i = 0; i < items.length; i++) {
- const currentItemIndex = items_idx.findIndex(
- (item) =>
- (Array.isArray(item[0])
- ? item[0].join(", ")
- : item[0].toString()) == items[i]
- );
- const foundItem = newData.find(
- (item) => item[1] === items_idx[currentItemIndex]?.[1]
- );
- new_order.push(foundItem);
- items_idx.splice(currentItemIndex, 1);
- }
- const getItem = (item: any) => {
- if (data.valueType == DataType.Number) {
- return "" + item;
- }
- return item;
- };
- data.order = new_order.map((item) => getItem(item[0]));
- data.orderMode = OrderMode.order;
- data.categories = new_order.map((item) => getItem(item[0]));
+ getOnConfirmFunction(axisData, items, items_idx, data);
} catch (e) {
console.log(e);
}
@@ -2291,12 +2276,20 @@ function getOrderByAnotherColumnWidgets(
expression: groupByExpression,
});
const items = vectorData.map((item) => [...new Set(item)]);
- const newData = items.map((item) =>
- Array.isArray(item) ? item.join(", ") : item
- );
- data.orderByCategories = newData;
+ const newData = updateWidgetCategoriesByExpression(items);
+ data.orderByCategories = [...new Set(newData)];
};
+ if (orderChanged) {
+ columnsDisplayNames = columnsDisplayNames.map((name) => {
+ if (isOriginalColumn && name == data.orderByExpression) {
+ return "Custom";
+ } else {
+ return name;
+ }
+ });
+ }
+
widgets.push(
manager.label(strings.objects.axes.orderBy),
@@ -2315,9 +2308,21 @@ function getOrderByAnotherColumnWidgets(
manager.reorderByAnotherColumnWidget(
{ property: axisProperty, field: "orderByCategories" },
{
- allowReset: true,
+ allowReset: isNumberValueType == false,
onConfirmClick: onConfirm,
- onResetCategories: rawAxisData,
+ onResetCategories: onResetAxisCategories,
+ sortedCategories: sortedCategories,
+ allowDragItems: isNumberValueType == false,
+ onReorderHandler: isOriginalColumn
+ ? () => {
+ orderChanged = true;
+ }
+ : undefined,
+ onButtonHandler: isOriginalColumn
+ ? () => {
+ orderChanged = false;
+ }
+ : undefined,
}
)
)
diff --git a/src/core/prototypes/plot_segments/utils.ts b/src/core/prototypes/plot_segments/utils.ts
index 847a0eab..14276f1e 100644
--- a/src/core/prototypes/plot_segments/utils.ts
+++ b/src/core/prototypes/plot_segments/utils.ts
@@ -5,6 +5,8 @@ import { Controls } from "../common";
import { deepClone } from "../../common";
import { Dataset, Expression, Specification } from "../../index";
import { CharticulatorPropertyAccessors } from "../../../app/views/panels/widgets/types";
+import { AxisDataBinding, OrderMode } from "../../../core/specification/types";
+import { DataType } from "../../../core/specification";
export function getTableColumns(
manager: Controls.WidgetManager & CharticulatorPropertyAccessors
@@ -96,3 +98,130 @@ export function shouldShowTickFormatForTickExpression(
}
return showInputFormat;
}
+
+export type CategoryItemsWithId = [unknown[], number];
+export type CategoryItemsWithIds = CategoryItemsWithId[];
+
+function isNumbers(array: unknown[]): array is number[] {
+ return typeof array[0] === "number";
+}
+
+function numbersSortFunction(a: number, b: number) {
+ return a - b;
+}
+
+export const JoinSymbol = ", ";
+
+function getStingValueFromCategoryItemsWithIds(
+ itemWithId: CategoryItemsWithId
+): string {
+ const item = itemWithId[0];
+ if (isNumbers(item)) {
+ if (Array.isArray(item)) {
+ return item.sort(numbersSortFunction).join(JoinSymbol);
+ } else {
+ return item;
+ }
+ } else {
+ if (Array.isArray(item)) {
+ return item.sort().join(JoinSymbol);
+ } else {
+ return item;
+ }
+ }
+}
+
+/**
+ * Transform data to sting array
+ * [
+ * [[data], id],
+ * [....]
+ * ]
+ * @param itemsWithIds
+ * @return unique string array
+ */
+export function transformOnResetCategories(
+ itemsWithIds: CategoryItemsWithIds
+): string[] {
+ const data = itemsWithIds.map((itemWithId) =>
+ getStingValueFromCategoryItemsWithIds(itemWithId)
+ );
+ const uniqueValues = new Set(data);
+ return [...uniqueValues];
+}
+
+export function getOnConfirmFunction(
+ datasetAxisData: any[][],
+ items: string[],
+ itemsWithIds: CategoryItemsWithIds,
+ data: AxisDataBinding
+) {
+ try {
+ const newDataOrder = [...datasetAxisData];
+ const new_order = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const idxForItem: number[] = [];
+ for (let j = 0; j < itemsWithIds.length; j++) {
+ const item = itemsWithIds[j];
+ const stringSortedValue = getStingValueFromCategoryItemsWithIds(item);
+ if (stringSortedValue === items[i]) {
+ idxForItem.push(item[1]);
+ }
+ }
+
+ for (let j = 0; j < idxForItem.length; j++) {
+ const foundItem = newDataOrder.find(
+ (item) => item[1] === idxForItem[j]
+ );
+ new_order.push(foundItem);
+ }
+ }
+ const getItem = (item: any) => {
+ if (data.valueType == DataType.Number) {
+ return "" + item;
+ }
+ return item;
+ };
+ data.order = new_order.map((item) => getItem(item[0]));
+ data.orderMode = OrderMode.order;
+ data.categories = new_order.map((item) => getItem(item[0]));
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+export function transformDataToCategoryItemsWithIds(
+ data: unknown[][]
+): CategoryItemsWithIds {
+ return data.map((item, idx) => [item, idx]);
+}
+
+export function updateWidgetCategoriesByExpression(
+ widgetData: unknown[][]
+): string[] {
+ const newWidgetData: string[] = [];
+ const transformedWidgetData = transformDataToCategoryItemsWithIds(widgetData);
+ transformedWidgetData.map((item) => {
+ const stringValueForItem = getStingValueFromCategoryItemsWithIds(item);
+ newWidgetData.push(stringValueForItem);
+ });
+ return newWidgetData;
+}
+
+export function getSortedCategories(itemsWithIds: CategoryItemsWithIds) {
+ let sortedData: string[];
+ if (itemsWithIds[0] && isNumbers(itemsWithIds[0][0])) {
+ sortedData = transformOnResetCategories(
+ itemsWithIds.sort((firstItem, secondItem) => {
+ if (isNumbers(firstItem[0]) && isNumbers(secondItem[0])) {
+ return firstItem[0][0] - secondItem[0][0];
+ }
+ return 0;
+ })
+ );
+ } else {
+ sortedData = transformOnResetCategories(itemsWithIds).sort();
+ }
+ return sortedData;
+}