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:
Родитель
5f17e34bbf
Коммит
f24483f02a
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче