Pivot Table 위젯
Pivot Table은 객체를 속성으로 묶고 집계한 값을 표로 표시합니다. 행에는 여러 그룹, 열에는 최대 하나의 그룹을 사용하며 Date와 Timestamp는 날짜, 주, 월 또는 연 단위로 묶을 수 있습니다.
Flight Alerts를 집계하고 선택한 구간으로 아래 Object List를 필터링하는 예입니다.
객체 기반 입력
Base object set은 단일 객체 유형의 집합이어야 합니다.
행 그룹
Add row grouping에서 속성을 선택합니다. 그룹별 Column width를 지정하며 사용자는 실행 중 열 경계를 끌어 너비를 바꿀 수 있습니다. Date 또는 Timestamp 그룹은 Time interval에서 정확한 날짜/시각, 주나 월 등의 구간을 정합니다.
행 그룹은 최대 일곱 개입니다. 더 많은 기준을 제공하려면 숨긴 그룹을 구성하여 사용자가 UI에서 교체하도록 합니다. 숨긴 기준을 동시에 일곱 개를 넘겨 사용하는 방식으로 해석하지 않습니다.
Show totals는 표 아래에 Total 행을 추가합니다. 여러 단계의 집계를 사용하는 경우 전체 원시값에 같은 집계 처리를 적용한 결과입니다. 표시된 각 셀의 결과를 단순히 더하는 것과 다를 수 있습니다.
Sort rows로 하나 이상의 구성한 행 그룹 속성에 정렬을 적용합니다.
열 그룹과 지표
열 그룹은 속성 하나까지 선택할 수 있습니다. 날짜/시각의 Time interval, 오른쪽 Total 열과 값의 오름차순 또는 내림차순을 구성합니다.
Add aggregation으로 속성 집계나 객체 수를 추가합니다. 지표는 Average, Max, Min, Sum, Count와 Cardinality 중 선택하며 제목과 열 너비를 정합니다. Aggregation placement의 기본값은 Columns이며 Rows로 바꿀 수도 있습니다.
선택과 복사
Selected filter는 선택한 셀, 셀 묶음, 행 또는 열의 그룹 조건을 Object Set Filter로 출력합니다. 다른 위젯이나 객체 집합에 적용할 수 있습니다. On selection에는 drawer 열기 등의 이벤트를 연결합니다.
선택한 셀은 macOS Cmd+C 또는 Windows Ctrl+C로 탭으로 구분한 텍스트를 복사합니다. 선택 범위에 따라 행 그룹, 집계값 또는 둘 다 포함합니다. Shift+Left와 Shift+Right로 전체 행, 그룹만 또는 집계값만 선택하도록 범위를 조정할 수 있습니다.
함수 기반 입력
함수 결과를 사용하면 여러 소스의 자료를 결합하거나 표시 전에 사용자 정의 집계와 업무 로직을 적용할 수 있습니다. 함수는 Struct 배열을 반환해야 하며 각 항목에는 셀의 지표를 담는 values 필드가 있어야 합니다.
다음은 가상의 판매 행을 합산하는 완전한 함수 예입니다. 입력 rows는 region, year, productType, productName과 정수 totalSales 및 estimatedSales를 가진 평평한 Struct 배열입니다. 실제 조회나 SQL에서 준비한 행을 연결합니다. 예제는 합계도 Integer 범위에 들어가는 입력을 전제로 합니다.
행 계층은 region, productType, productName이고 열은 year입니다. 함수는 각 계층의 연도별 값, 열 합계, 연도별 전체 행 합계와 전체 합계를 계산합니다. 빈 입력은 빈 목록을 반환합니다. 빈 문자열은 합계로 해석되는 문제를 피하기 위해 입력 오류로 처리하며 미기록 그룹값은 undefined로 전달합니다.
TypeScript v1
import { Function, Integer } from "@foundry/functions-api";
interface SalesInputRow {
region?: string;
year?: string;
productType?: string;
productName?: string;
totalSales: Integer;
estimatedSales: Integer;
}
interface SalesValues {
totalSales: Integer;
estimatedSales: Integer;
}
interface SalesData {
region?: string;
year?: string;
productType?: string;
productName?: string;
values: SalesValues;
}
interface GroupNode {
totalSales: Integer;
estimatedSales: Integer;
years: Map<string | undefined, SalesValues>;
children?: Map<string | undefined, GroupNode>;
}
const ROW_FIELDS = ["region", "productType", "productName"] as const;
function newNode(): GroupNode {
return { totalSales: 0, estimatedSales: 0, years: new Map() };
}
function addValues(node: GroupNode, row: SalesInputRow): void {
node.totalSales += row.totalSales;
node.estimatedSales += row.estimatedSales;
let values = node.years.get(row.year);
if (values === undefined) {
values = { totalSales: 0, estimatedSales: 0 };
node.years.set(row.year, values);
}
values.totalSales += row.totalSales;
values.estimatedSales += row.estimatedSales;
}
function aggregateSales(rows: readonly SalesInputRow[]): SalesData[] {
if (rows.length === 0) return [];
const root = newNode();
for (const row of rows) {
if (row.region === "" || row.year === "" || row.productType === "" || row.productName === "") {
throw new Error("빈 그룹 문자열은 사용할 수 없습니다. 미기록 값은 undefined로 전달하세요.");
}
let node = root;
addValues(node, row);
for (const field of ROW_FIELDS) {
const children = node.children ?? (node.children = new Map());
let child = children.get(row[field]);
if (child === undefined) {
child = newNode();
children.set(row[field], child);
}
node = child;
addValues(node, row);
}
}
const result: SalesData[] = [];
const path: Array<string | undefined> = [];
function makeRow(values: SalesValues): SalesData {
const row: SalesData = { values };
if (path.length > 0) row.region = path[0];
if (path.length > 1) row.productType = path[1];
if (path.length > 2) row.productName = path[2];
return row;
}
function emit(node: GroupNode): void {
for (const [year, values] of node.years) {
const row = makeRow(values);
row.year = year;
result.push(row);
}
result.push(makeRow({ totalSales: node.totalSales, estimatedSales: node.estimatedSales }));
if (node.children !== undefined) {
for (const [key, child] of node.children) {
path.push(key);
emit(child);
path.pop();
}
}
}
emit(root);
return result;
}
export class SalesPivotFunctions {
@Function()
public salesFunctionBackedPivotTable(rows: SalesInputRow[]): SalesData[] {
return aggregateSales(rows);
}
}
TypeScript v2
import { Integer } from "@osdk/functions";
interface SalesInputRow {
region?: string;
year?: string;
productType?: string;
productName?: string;
totalSales: Integer;
estimatedSales: Integer;
}
interface SalesValues {
totalSales: Integer;
estimatedSales: Integer;
}
interface SalesData {
region?: string;
year?: string;
productType?: string;
productName?: string;
values: SalesValues;
}
interface GroupNode {
totalSales: Integer;
estimatedSales: Integer;
years: Map<string | undefined, SalesValues>;
children?: Map<string | undefined, GroupNode>;
}
const ROW_FIELDS = ["region", "productType", "productName"] as const;
function newNode(): GroupNode {
return { totalSales: 0, estimatedSales: 0, years: new Map() };
}
function addValues(node: GroupNode, row: SalesInputRow): void {
node.totalSales += row.totalSales;
node.estimatedSales += row.estimatedSales;
let values = node.years.get(row.year);
if (values === undefined) {
values = { totalSales: 0, estimatedSales: 0 };
node.years.set(row.year, values);
}
values.totalSales += row.totalSales;
values.estimatedSales += row.estimatedSales;
}
function aggregateSales(rows: readonly SalesInputRow[]): SalesData[] {
if (rows.length === 0) return [];
const root = newNode();
for (const row of rows) {
if (row.region === "" || row.year === "" || row.productType === "" || row.productName === "") {
throw new Error("빈 그룹 문자열은 사용할 수 없습니다. 미기록 값은 undefined로 전달하세요.");
}
let node = root;
addValues(node, row);
for (const field of ROW_FIELDS) {
const children = node.children ?? (node.children = new Map());
let child = children.get(row[field]);
if (child === undefined) {
child = newNode();
children.set(row[field], child);
}
node = child;
addValues(node, row);
}
}
const result: SalesData[] = [];
const path: Array<string | undefined> = [];
function makeRow(values: SalesValues): SalesData {
const row: SalesData = { values };
if (path.length > 0) row.region = path[0];
if (path.length > 1) row.productType = path[1];
if (path.length > 2) row.productName = path[2];
return row;
}
function emit(node: GroupNode): void {
for (const [year, values] of node.years) {
const row = makeRow(values);
row.year = year;
result.push(row);
}
result.push(makeRow({ totalSales: node.totalSales, estimatedSales: node.estimatedSales }));
if (node.children !== undefined) {
for (const [key, child] of node.children) {
path.push(key);
emit(child);
path.pop();
}
}
}
emit(root);
return result;
}
export default function salesFunctionBackedPivotTable(rows: SalesInputRow[]): SalesData[] {
return aggregateSales(rows);
}
같은 반환 계약을 v1에서는 @Function() 클래스 메서드, v2에서는 파일의 default export로 등록합니다. 출력의 중첩 values 구조는 Pivot Table의 함수 입력 계약이며 일반 Struct 변수의 중첩 지원 범위와 구분합니다.
함수 결과 연결
함수를 선택하고 rows에 실제 입력 배열을 연결합니다. Group-by fields로 행과 열을 정하고 Value fields에서 totalSales와 estimatedSales를 선택합니다. Expandable rows에는 productType과 productName을 계층 순서에 맞춰 구성합니다.
합계 행의 표현
그룹 필드를 생략하면 해당 차원 전체의 합계를 표현합니다. 아래 세 JSON은 각각의 합계 형태를 보여 주는 별도의 가상 예입니다.
행 그룹이 region과 productType이고 열 그룹이 year일 때, 행 그룹을 모두 생략하면 해당 연도의 전체 행 합계입니다.
{
"year": "2021",
"values": { "totalSales": 622000, "estimatedSales": 57000 }
}
열 그룹을 생략하면 지정한 행 그룹의 전체 열 합계입니다.
{
"region": "EU",
"productType": "Clothing",
"values": { "totalSales": 57000, "estimatedSales": 57000 }
}
모든 그룹 필드를 생략하면 Grand total입니다.
{
"values": { "totalSales": 3147000, "estimatedSales": 3160000 }
}
Null bucket과 필드 생략
Null bucket은 필드를 포함한 채 값을 undefined로 반환합니다. 인터페이스도 그 선택 값을 허용해야 합니다. 필드 자체를 생략하는 합계와 구분합니다.
아래는 TypeScript v2에서 반환할 null bucket의 형태입니다. source와 part를 그룹으로 사용하는 예이며 values 안에는 quantity가 있습니다.
import { Double } from "@osdk/functions";
interface SourcePartData {
source?: string;
part?: string | undefined;
values: { quantity: Double };
}
export default function nullBucketExample(): SourcePartData[] {
return [{ source: "SourceA", part: undefined, values: { quantity: 100 } }];
}
빈 문자열은 필드를 생략한 것처럼 해석되어 의도하지 않은 subtotal로 올라갈 수 있으므로 사용하지 않습니다. 또한 일반 JSON에는 undefined가 없고 JSON.stringify는 undefined 속성을 생략합니다. 이 구분을 유지할 함수 반환 객체를 일반 JSON 문자열로 바꾸어 전달하지 않습니다.
단계별 확장 데이터
Expandable rows에 필요한 필드를 추가하고 각 깊이의 집계 행을 반환합니다. 다음은 totalSales 하나만 표시하는 별도 가상 예이며 NA의 2021년 합계는 300,000입니다.
| 깊이 | 포함하는 그룹과 값 |
|---|---|
| 기본 | region=NA, year=2021, totalSales=300,000 |
| 첫 확장 | Clothing=90,000, Electronics=150,000, Furniture=60,000 |
| Electronics 아래 확장 | ProductA=50,000, ProductB=50,000, ProductC=50,000 |
기본 행은 productType과 productName을 생략하고 첫 확장 행은 productName만 생략합니다. 두 번째 확장에는 두 필드를 모두 포함합니다. 위의 실제 합산 함수는 이런 계층별 행을 입력에서 계산합니다.
함수 기반 선택 출력
선택 결과를 Struct 변수에 쓸 수 있으며 필드는 함수 출력에서 파생합니다.
표시와 스타일
| 설정 | 동작 |
|---|---|
| Layout: Tabular | 기본 표시입니다. |
| Layout: Stacked | 행 그룹을 한 열에 모읍니다. 최상위 값은 한 번 표시하고 하위 그룹을 들여씁니다. Customize stacked groupby label로 그 열 이름을 바꿀 수 있습니다. |
| Fill Width | 부모 컨테이너의 너비에 맞추고 크기 변화에 따라 조정합니다. |
| Table style: Basic | 기본 셀과 테두리 스타일입니다. |
| Table style: Outlined | 최상위 행 그룹의 위아래에 진한 선을 둡니다. |
| Table style: Banded rows | 같은 구획선과 번갈아 나타나는 옅은 회색 행 배경을 사용합니다. |
| Color | Header 색을 지정합니다. Basic에서는 행 그룹 열에도 그 색의 옅은 배경을 적용합니다. |
| Minimal / Prominent | Header에 선택한 색의 옅은 음영 또는 그대로의 색을 적용합니다. |
집계 대상 객체 자체를 보려면 Object Table이나 Object List를 연결합니다. 앱과 자원을 모으는 진입 화면에는 Resource List를 사용할 수 있습니다.