Revise the order by another column (#909)

* Revise the order by another column

* Fixed numers sort method

* Update chart saving

* Update popup position

* Remove unused import

Co-authored-by: Ramil Minyukov (Akvelon INC) <v-rminyukov@microsoft.com>
This commit is contained in:
Ramil Minyukov 2022-02-04 16:49:44 +03:00 коммит произвёл GitHub
Родитель 5f17e34bbf
Коммит f24483f02a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 290 добавлений и 67 удалений

Просмотреть файл

@ -1803,7 +1803,8 @@
font-size: 14px;
padding: 0 10px;
min-width: 100px;
max-height: 700px;
max-width: 500px;
.el-row {
margin: 10px 0;
}

Просмотреть файл

@ -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,

Просмотреть файл

@ -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<
<div className="charticulator__widget-popup-reorder-widget">
<div className="el-row el-list-view">
<ReorderListView
enabled={true}
enabled={this.props.allowDragItems ?? true}
onReorder={(a, b) => {
ReorderListView.ReorderArray(items, a, b);
this.setState({ items, customOrder: true, sortOrder: false });
if (this.props.onReorderHandler) {
this.props.onReorderHandler();
}
}}
>
{items.map((x) => (
<div key={x + getRandomNumber()} className="el-item">
{x}
<TooltipHost content={x}>{x}</TooltipHost>
</div>
))}
</ReorderListView>
</div>
<div className="el-row">
<DefaultButton
iconProps={{
iconName: "SortLines",
}}
text={strings.reOrder.sort}
onClick={() => {
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,
},
}}
/>
<DefaultButton
iconProps={{
iconName: "Sort",
@ -75,27 +109,9 @@ export class FluentUIReorderStringsValue extends React.Component<
items: this.state.items.reverse(),
customOrder: true,
});
}}
/>
<DefaultButton
iconProps={{
iconName: "SortLines",
}}
text={strings.reOrder.sort}
onClick={() => {
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();
}
}
}}
/>

Просмотреть файл

@ -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
</PopupView>
);
},
{ anchor: container }
{
anchor: container,
alignX:
this.store.editorType == EditorType.Embedded
? PopupAlignment.EndInner
: PopupAlignment.StartInner,
}
);
}}
/>

Просмотреть файл

@ -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 {

Просмотреть файл

@ -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,
}
)
)

Просмотреть файл

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