Chart XY 위젯
Chart XY는 객체를 막대, 선 또는 산점도로 표시합니다. 속성별 집계와 세분화, 함수 집계 레이어, 축과 범례 및 선택 필터를 구성할 수 있습니다. 더 세밀한 문법 기반 시각화는 Vega Chart를 사용합니다.
레이어 추가와 입력
데이터를 표시하려면 Layer를 구성합니다. 레이어 Title은 작성자가 구성 패널에서 구분하는 이름이며 소비자 화면에는 표시하지 않습니다.
| Data input | 입력 |
|---|---|
| Object set | Workshop 객체 집합 변수 |
| Function aggregation | 2차원 또는 3차원 집계를 반환하는 함수. 사용 가능한 레이어 설정은 일부로 제한됩니다. |
| Time series set | 시계열 집합 변수. X축은 시간 범위, Y축은 시계열값이며 Line Chart만 지원합니다. |
일반 객체 입력의 Layer type은 Bar Chart, Line Chart 또는 Scatter Chart입니다. Line에는 Line, 아래 영역을 칠하는 Area와 세그먼트 값을 쌓는 Stacked가 있습니다. Labels는 Bar와 Line에서 값 레이블을 표시합니다.
속성과 series
X axis property에서 분류할 속성을 고릅니다. 한 속성에 단일 또는 여러 series를 표시할 수 있습니다. 예를 들어 Alert Type별 경고 수와 Hours Delayed 합계를 함께 볼 수 있습니다.
Series aggregation의 기본값은 Count입니다. Average, Min, Max, Sum과 Approximate Unique Count도 사용할 수 있습니다.
Segment by는 두 번째 속성으로 각 값을 나눕니다. Alert Type별 막대를 Aircraft Type으로 세분화하는 방식이 한 예입니다.
| 설정 | 범위와 동작 |
|---|---|
| Display of null/missing values | Line 전용입니다. Gap은 빈 구간, Ignored는 앞뒤 관측값을 연결, Zeroes는 0으로 취급하여 표시합니다. |
| Display override | Series의 범례 이름을 바꾸거나 세분화한 차트의 특정 segment 이름을 바꿉니다. |
| Segment overrides | Bar 전용입니다. Stacked, Percentage와 Grouped 중 표현을 정합니다. |
| Selection as filter | Object set 입력에만 제공하며 선택 결과를 Object Set Filter로 출력하여 후속 위젯에 적용합니다. |
| Delete layer | 현재 레이어를 제거합니다. |
Zeroes는 선택한 표시 규칙이며 원천에 실제 0이 관측되었다는 뜻은 아닙니다.
시나리오 비교
Compare against Scenarios에 Scenario Array를 연결하면 시나리오별 값을 segment 축으로 비교합니다. 이 옵션을 켜면 다른 속성으로 Segment by를 동시에 지정할 수 없습니다.
한 레이어 안의 시나리오 색은 자동 배정하며 레이어 Color는 그 레이어의 모든 시나리오에 적용합니다. 시나리오마다 색을 따로 정하려면 레이어를 각각 만들고 각 레이어의 Load data from scenario를 사용합니다.
실행 중 시간 버킷 조정
X축이 Date 또는 Timestamp 속성이고 기본 집계를 쓰는 레이어에는 실행 중 차트 toolbar의 톱니바퀴가 나타납니다. 여기서 일, 주나 월 같은 timestamp bucket size를 고릅니다. 함수 집계 레이어나 다른 X축 유형에는 이 버튼을 제공하지 않습니다.
차트 전체 설정
| 구역 | 설정 |
|---|---|
| Categorical axis 제목 | Show title로 표시하거나 덮어씁니다. 기본값은 series의 속성 유형 이름입니다. |
| Categorical axis 숫자 서식 | 그룹 구분, 소수점 최소/최대, 과학 표기 등을 구성합니다. |
| Categorical axis 정렬 | 기본 A~Z입니다. 값 오름차순/내림차순 또는 Custom을 선택할 수 있습니다. Custom 순서는 속성에 숫자 정렬값을 부여하고 그 속성을 sort metric으로 지정합니다. |
| Value axis 여러 축 | 여러 series가 있을 때 series별 축을 정할 수 있습니다. 서로 다른 값 범위를 표시하는 데 사용합니다. |
| Value axis 제목 | 기본 집계 유형 이름을 표시하거나 바꿉니다. |
| Value axis 숫자 서식 | 그룹 구분, 소수점 자릿수와 과학 표기 등을 지정합니다. |
| Value axis Scale type | 기본 Linear 또는 Logarithmic입니다. |
| Minimum bound / Maximum bound | 기본적으로 표시값에서 자동 계산합니다. Min 또는 Max로 수동 지정할 수 있습니다. |
| Legend | 표시 여부와 위치를 정합니다. |
| Bar orientation | Bar의 기본값은 Horizontal입니다. Line과 Scatter는 Vertical만 가능합니다. |
함수 집계의 반환형
함수는 TwoDimensionalAggregation<Key, Value> 또는 ThreeDimensionalAggregation<Key, Segment, Value>를 반환합니다. 등록 방식과 내부 구조는 언어별로 다릅니다.
| 언어 | 등록과 구조 |
|---|---|
| TypeScript v1 | 클래스 메서드에 @Function()을 붙입니다. 바깥은 buckets이며 3차원의 안쪽 배열은 value입니다. |
| TypeScript v2 | 파일의 default export입니다. 버킷 배열을 직접 반환하고 3차원의 안쪽 배열은 groups입니다. |
| Python | @function으로 등록합니다. 집계 클래스의 buckets와 SingleBucket 또는 NestedBucket을 사용합니다. |
선언한 집계 타입을 올바르게 구성하면 같은 의미의 출력으로 직렬화하므로 위젯의 연결 방식을 바꿀 필요는 없습니다. TypeScript v2에서 v1의 { buckets: ... } 구조를 그대로 반환하면 배열형 반환 계약에 맞지 않습니다.
두 버킷의 자료형 예
다음은 bucket1에 5.0, bucket2에 6.0을 제공하는 고정 예시 함수입니다. 온톨로지 데이터를 조회한 결과를 대신하는 구현이 아니라 반환 구조를 보여 줍니다.
import { Double, Function, TwoDimensionalAggregation } from "@foundry/functions-api";
export class MyFunctions {
@Function()
public myTwoDimensionalAggregation(): TwoDimensionalAggregation<string, Double> {
return {
buckets: [
{ key: "bucket1", value: 5.0 },
{ key: "bucket2", value: 6.0 },
],
};
}
}
import { Double, TwoDimensionalAggregation } from "@osdk/functions";
function myTwoDimensionalAggregationFunction(): TwoDimensionalAggregation<string, Double> {
return [
{ key: "bucket1", value: 5.0 },
{ key: "bucket2", value: 6.0 },
];
}
export default myTwoDimensionalAggregationFunction;
from functions.api import (
function,
Double,
TwoDimensionalAggregation,
SingleBucket
)
@function
def my_two_dimensional_aggregation_function() -> TwoDimensionalAggregation[str, Double]:
return TwoDimensionalAggregation(
buckets=[
SingleBucket(key="bucket1", value=Double(5.0)),
SingleBucket(key="bucket2", value=Double(6.0)),
]
)
일별 비율 집계 예
가상의 MyObjectType에 Timestamp date와 숫자 value 및 total이 있다고 가정합니다. 같은 날짜의 value 합을 total 합으로 나눕니다. 함수는 비율을 반환하며 백분율 표현은 표시 서식에서 정합니다.
이 예제는 합계 분모가 0인 버킷의 표시값을 0으로 정합니다. 0으로 나누기의 수학적 결과가 아니라 예제에서 선택한 경계 처리입니다. 없는 분모 버킷을 0으로 채우는 것과 구분합니다.
TypeScript v1의 범위 키는 IRange<Timestamp>의 min/max입니다. 따로 수행한 집계를 배열 위치로 나누지 않고 같은 날짜 키로 대응시킵니다. 날짜나 대응 분모가 없으면 오류를 보고합니다.
import {
Double, Function, IRange, Timestamp, TwoDimensionalAggregation, UserFacingError,
} from "@foundry/functions-api";
import { MyObjectType, ObjectSet } from "@foundry/ontology-api";
function bucketStart(range: IRange<Timestamp>): number {
if (range.min === undefined) throw new UserFacingError("날짜 버킷의 시작이 없습니다.");
return range.min.getTime();
}
export class DailyRatioFunctions {
@Function()
public async percentOfTotal(
objects: ObjectSet<MyObjectType>,
): Promise<TwoDimensionalAggregation<IRange<Timestamp>, Double>> {
const [numerators, denominators] = await Promise.all([
objects.groupBy(object => object.date.byDays()).sum(object => object.value),
objects.groupBy(object => object.date.byDays()).sum(object => object.total),
]);
const totals = new Map<number, Double>();
for (const bucket of denominators.buckets) totals.set(bucketStart(bucket.key), bucket.value);
const buckets: TwoDimensionalAggregation<IRange<Timestamp>, Double>["buckets"] = [];
for (const bucket of numerators.buckets) {
const total = totals.get(bucketStart(bucket.key));
if (total === undefined) throw new UserFacingError("대응하는 분모 날짜 버킷이 없습니다.");
buckets.push({ key: bucket.key, value: total === 0 ? 0 : bucket.value / total });
}
return { buckets };
}
}
TypeScript v2의 aggregate 결과는 평평한 행 배열입니다. $group에서 그룹 키를, row.value.sum과 row.total.sum에서 지표를 가져옵니다. 속성값이 없어 지표가 빠진 경우에는 이 예제의 합계값을 0으로 처리합니다.
같은 입력과 그룹에서 두 합계를 구하므로 한 요청의 $select에 두 지표를 넣습니다. 별도로 요청한다면 unordered 결과의 순서가 같다는 보장이 없어 그룹 키로 연결해야 합니다.
$duration의 그룹값은 버킷 시작 시각 하나입니다. 아래의 1일 예는 시작에 24시간을 더하여 Range<TimestampISOString>의 startValue/endValue를 만듭니다. 범위형 반환과 $ranges 입력의 [start, end] 튜플은 구분합니다.
import { ObjectSet } from "@osdk/client";
import { Double, Range, TimestampISOString, TwoDimensionalAggregation } from "@osdk/functions";
import { MyObjectType } from "@ontology/sdk";
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
function toDayRange(start: Date): Range<TimestampISOString> {
return {
startValue: start.toISOString(),
endValue: new Date(start.getTime() + ONE_DAY_IN_MS).toISOString(),
};
}
export default async function percentOfTotal(
objects: ObjectSet<MyObjectType>,
): Promise<TwoDimensionalAggregation<Range<TimestampISOString>, Double>> {
const rows = await objects.aggregate({
$select: { "value:sum": "unordered", "total:sum": "unordered" },
$groupBy: { date: { $duration: [1, "days"] } },
});
const buckets: TwoDimensionalAggregation<Range<TimestampISOString>, Double> = [];
for (const row of rows) {
if (row.$group.date == null) continue;
const numerator = row.value.sum ?? 0;
const denominator = row.total.sum ?? 0;
buckets.push({
key: toDayRange(new Date(row.$group.date)),
value: denominator === 0 ? 0 : numerator / denominator,
});
}
return buckets;
}
날짜와 groupId별 비율
TypeScript v1은 segmentBy로 두 번째 차원을 만듭니다. 아래는 가상의 String groupId를 사용하며 날짜와 groupId를 모두 맞춰 나눕니다. 앞 예와 같이 분모 0은 0을 표시하고, 대응 버킷 자체가 없으면 오류를 반환합니다.
import {
Double, Function, IRange, ThreeDimensionalAggregation, Timestamp, UserFacingError,
} from "@foundry/functions-api";
import { MyObjectType, ObjectSet } from "@foundry/ontology-api";
function bucketStart(range: IRange<Timestamp>): number {
if (range.min === undefined) throw new UserFacingError("날짜 버킷의 시작이 없습니다.");
return range.min.getTime();
}
export class SegmentedRatioFunctions {
@Function()
public async percentOfTotalSegmented(
objects: ObjectSet<MyObjectType>,
): Promise<ThreeDimensionalAggregation<IRange<Timestamp>, string, Double>> {
const [numerators, denominators] = await Promise.all([
objects.groupBy(object => object.date.byDays())
.segmentBy(object => object.groupId.topValues()).sum(object => object.value),
objects.groupBy(object => object.date.byDays())
.segmentBy(object => object.groupId.topValues()).sum(object => object.total),
]);
const totals = new Map<number, Map<string, Double>>();
for (const bucket of denominators.buckets) {
const segments = new Map<string, Double>();
for (const segment of bucket.value) segments.set(segment.key, segment.value);
totals.set(bucketStart(bucket.key), segments);
}
const buckets: ThreeDimensionalAggregation<IRange<Timestamp>, string, Double>["buckets"] = [];
for (const bucket of numerators.buckets) {
const denominatorsBySegment = totals.get(bucketStart(bucket.key));
if (denominatorsBySegment === undefined) {
throw new UserFacingError("대응하는 분모 날짜 버킷이 없습니다.");
}
const value: Array<{ key: string; value: Double }> = [];
for (const segment of bucket.value) {
const denominator = denominatorsBySegment.get(segment.key);
if (denominator === undefined) {
throw new UserFacingError("대응하는 분모 세그먼트가 없습니다.");
}
value.push({
key: segment.key,
value: denominator === 0 ? 0 : segment.value / denominator,
});
}
buckets.push({ key: bucket.key, value });
}
return { buckets };
}
}
TypeScript v2에는 별도 segmentBy가 없습니다. $groupBy에 날짜와 groupId를 함께 넣으면 각 조합의 평평한 행을 받으므로 날짜 아래 groups로 묶습니다. 여러 그룹 키를 사용할 때 모든 $select 값은 unordered여야 합니다.
import { ObjectSet } from "@osdk/client";
import { Double, Range, ThreeDimensionalAggregation, TimestampISOString } from "@osdk/functions";
import { MyObjectType } from "@ontology/sdk";
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
type DayBucket = {
key: Range<TimestampISOString>;
groups: Array<{ key: string; value: Double }>;
};
function toDayRange(start: Date): Range<TimestampISOString> {
return {
startValue: start.toISOString(),
endValue: new Date(start.getTime() + ONE_DAY_IN_MS).toISOString(),
};
}
export default async function percentOfTotalSegmented(
objects: ObjectSet<MyObjectType>,
): Promise<ThreeDimensionalAggregation<Range<TimestampISOString>, string, Double>> {
const rows = await objects.aggregate({
$select: { "value:sum": "unordered", "total:sum": "unordered" },
$groupBy: { date: { $duration: [1, "days"] }, groupId: "exact" },
});
const byDate = new Map<number, DayBucket>();
for (const row of rows) {
if (row.$group.date == null || row.$group.groupId == null) continue;
const start = new Date(row.$group.date);
const startMillis = start.getTime();
let bucket = byDate.get(startMillis);
if (bucket === undefined) {
bucket = { key: toDayRange(start), groups: [] };
byDate.set(startMillis, bucket);
}
const numerator = row.value.sum ?? 0;
const denominator = row.total.sum ?? 0;
bucket.groups.push({
key: row.$group.groupId,
value: denominator === 0 ? 0 : numerator / denominator,
});
}
return Array.from(byDate.values());
}
버킷 수와 정확도
TSv1 topValues의 기본 버킷 수는 1,000, TSv2 exact는 10,000입니다. 고유 값이 이 범위 안에 있을 때만 해당 그룹화를 정확하게 유지할 수 있습니다. 초과하면 일부 객체가 반환 그룹에서 제외되어 근사 결과가 될 수 있습니다.
플랫폼의 정확/근사 정보는 TypeScript OSDK에서 노출하지 않습니다. 높은 cardinality에서는 필요한 버킷 수 n을 $exactWithLimit로 지정하는 구성을 검토합니다. 버킷 수를 키우는 비용과 실제 대상 범위도 함께 고려합니다.
Python 사용자 집계
Python은 TwoDimensionalAggregation과 ThreeDimensionalAggregation 클래스의 구조를 사용합니다. Python Ontology SDK v2에서는 from_osdk 변환을 사용할 수 있습니다. 3차원 변환의 ThreeDimensionalAggregation.from_osdk(result, "date", "groupId")처럼 group-by 속성 API 이름을 전달하며, 주변 Python 식별자가 snake_case여도 API 이름은 해당 camelCase를 유지합니다.
Python 사용자 집계에 전체 예가 있고 TypeScript v1 사용자 집계에는 범위를 다루는 추가 예가 있습니다. 시계열 변수의 변환과 요약은 시계열 계산을 따릅니다.