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; +}