diff --git a/chartkit/src/lib/components/BarChart.tsx b/chartkit/src/lib/components/BarChart.tsx
index b671367..64f9de4 100644
--- a/chartkit/src/lib/components/BarChart.tsx
+++ b/chartkit/src/lib/components/BarChart.tsx
@@ -1,10 +1,10 @@
import { useQuery } from "../useQuery";
import { formatChartKitDisplayValue } from "../format";
-import { asNumber, asString } from "./utils";
import { chartKitTooltipBase } from "./chartTooltip";
import { echarts, ReactEChartsCore } from "./echartsCore";
import { themeVars } from "./themeVars";
import { useChartKitNamespace } from "./useChartKitNamespace";
+import { useChartSeries } from "./useChartSeries";
export interface BarChartProps {
namespace: string;
@@ -39,6 +39,16 @@ export function BarChart({
}: BarChartProps) {
const theme = useChartKitNamespace(namespace).theme;
const { loading, error, rows } = useQuery(namespace, data);
+ const { xLabels, rankedSeries } = useChartSeries({
+ rows,
+ xValueField,
+ xLabelField,
+ yValueField,
+ yLabelField,
+ yAxisTitle,
+ seriesValueField,
+ seriesLabelField,
+ });
if (loading) {
return
Loading...
;
@@ -46,53 +56,10 @@ export function BarChart({
if (error) {
return Error: {error}
;
}
-
- const xValues = new Map();
- const seriesLabels = new Map();
- const matrix = new Map>();
-
- for (const row of rows) {
- const xValue = asString(row[xValueField]);
- const xLabelCandidate = xLabelField ? asString(row[xLabelField]) : "";
- const xLabel = xLabelCandidate || xValue;
- if (!xValues.has(xValue)) {
- xValues.set(xValue, xLabel);
- }
-
- const seriesValue = seriesValueField ? asString(row[seriesValueField]) : "__single";
- const seriesLabelCandidate = seriesLabelField ? asString(row[seriesLabelField]) : "";
- const fallbackSeriesLabel = yAxisTitle ?? yLabelField ?? "Value";
- const seriesLabel = seriesValueField
- ? seriesLabelCandidate || seriesValue
- : fallbackSeriesLabel;
- if (!seriesLabels.has(seriesValue)) {
- seriesLabels.set(seriesValue, seriesLabel);
- }
-
- const yValue = asNumber(row[yValueField]);
- const currentSeries = matrix.get(seriesValue) ?? new Map();
- currentSeries.set(xValue, (currentSeries.get(xValue) ?? 0) + yValue);
- matrix.set(seriesValue, currentSeries);
- }
-
- const xKeys = Array.from(xValues.keys());
- const xLabels = xKeys.map((key) => xValues.get(key) ?? key);
const shouldRotate = xLabels.length > 8 || xLabels.some((label) => label.length > 10);
const valueFmt = yFmt ?? fmt;
const palette = theme.charts.palette;
- const rankedSeries = Array.from(seriesLabels.entries())
- .map(([seriesKey, seriesName]) => ({
- seriesKey,
- seriesName,
- values: xKeys.map((xKey) => matrix.get(seriesKey)?.get(xKey) ?? 0),
- }))
- .map((entry) => ({
- ...entry,
- total: entry.values.reduce((sum, value) => sum + value, 0),
- }))
- .sort((left, right) => right.total - left.total || left.seriesName.localeCompare(right.seriesName));
-
const series = rankedSeries.map(({ seriesName, values }, index) => ({
name: seriesName,
type: "bar",
diff --git a/chartkit/src/lib/components/DataTable.tsx b/chartkit/src/lib/components/DataTable.tsx
index dc39730..b52599b 100644
--- a/chartkit/src/lib/components/DataTable.tsx
+++ b/chartkit/src/lib/components/DataTable.tsx
@@ -1,6 +1,6 @@
import { Children, isValidElement, type ReactNode } from "react";
import { useQuery } from "../useQuery";
-import { formatChartKitDisplayValue } from "../format";
+import { formatChartKitDisplayValue, formatChartKitNumber } from "../format";
import type { DataTableColumnDefinition } from "../runtime/definitions";
import { DataColumn, type DataColumnProps } from "./DataColumn";
import { OverflowTooltip } from "./OverflowTooltip";
@@ -85,13 +85,10 @@ export function DataTable({ namespace, data, columns, children }: DataTableProps
}
if (typeof value === "number") {
const fractionDigits = Number.isInteger(value) ? 0 : 2;
- return value.toLocaleString("en-AU", {
- minimumFractionDigits: fractionDigits,
- maximumFractionDigits: fractionDigits,
- });
+ return formatChartKitNumber(value, `num${fractionDigits}`);
}
if (typeof value === "bigint") {
- return value.toLocaleString("en-AU");
+ return formatChartKitNumber(Number(value), "num0");
}
return asString(value);
};
diff --git a/chartkit/src/lib/components/LineChart.tsx b/chartkit/src/lib/components/LineChart.tsx
index f4a161c..f73975b 100644
--- a/chartkit/src/lib/components/LineChart.tsx
+++ b/chartkit/src/lib/components/LineChart.tsx
@@ -1,10 +1,10 @@
import { useQuery } from "../useQuery";
import { formatChartKitDisplayValue } from "../format";
-import { asNumber, asString } from "./utils";
import { chartKitTooltipBase } from "./chartTooltip";
import { echarts, ReactEChartsCore } from "./echartsCore";
import { themeVars } from "./themeVars";
import { useChartKitNamespace } from "./useChartKitNamespace";
+import { useChartSeries } from "./useChartSeries";
export interface LineChartProps {
namespace: string;
@@ -37,6 +37,16 @@ export function LineChart({
}: LineChartProps) {
const theme = useChartKitNamespace(namespace).theme;
const { loading, error, rows } = useQuery(namespace, data);
+ const { xLabels, rankedSeries } = useChartSeries({
+ rows,
+ xValueField,
+ xLabelField,
+ yValueField,
+ yLabelField,
+ yAxisTitle,
+ seriesValueField,
+ seriesLabelField,
+ });
if (loading) {
return Loading...
;
@@ -44,53 +54,10 @@ export function LineChart({
if (error) {
return Error: {error}
;
}
-
- const xValues = new Map();
- const seriesLabels = new Map();
- const matrix = new Map>();
-
- for (const row of rows) {
- const xValue = asString(row[xValueField]);
- const xLabelCandidate = xLabelField ? asString(row[xLabelField]) : "";
- const xLabel = xLabelCandidate || xValue;
- if (!xValues.has(xValue)) {
- xValues.set(xValue, xLabel);
- }
-
- const seriesValue = seriesValueField ? asString(row[seriesValueField]) : "__single";
- const seriesLabelCandidate = seriesLabelField ? asString(row[seriesLabelField]) : "";
- const fallbackSeriesLabel = yAxisTitle ?? yLabelField ?? "Value";
- const seriesLabel = seriesValueField
- ? seriesLabelCandidate || seriesValue
- : fallbackSeriesLabel;
- if (!seriesLabels.has(seriesValue)) {
- seriesLabels.set(seriesValue, seriesLabel);
- }
-
- const yValue = asNumber(row[yValueField]);
- const currentSeries = matrix.get(seriesValue) ?? new Map();
- currentSeries.set(xValue, (currentSeries.get(xValue) ?? 0) + yValue);
- matrix.set(seriesValue, currentSeries);
- }
-
- const xKeys = Array.from(xValues.keys());
- const xLabels = xKeys.map((key) => xValues.get(key) ?? key);
const shouldRotate = xLabels.length > 10 || xLabels.some((label) => label.length > 11);
const valueFmt = yFmt ?? fmt;
const palette = theme.charts.palette;
- const rankedSeries = Array.from(seriesLabels.entries())
- .map(([seriesKey, seriesName]) => ({
- seriesKey,
- seriesName,
- values: xKeys.map((xKey) => matrix.get(seriesKey)?.get(xKey) ?? 0),
- }))
- .map((entry) => ({
- ...entry,
- total: entry.values.reduce((sum, value) => sum + value, 0),
- }))
- .sort((left, right) => right.total - left.total || left.seriesName.localeCompare(right.seriesName));
-
const series = rankedSeries.map(({ seriesName, values }, index) => {
const color = palette[index % palette.length];
diff --git a/chartkit/src/lib/components/useChartSeries.ts b/chartkit/src/lib/components/useChartSeries.ts
new file mode 100644
index 0000000..dcc95b6
--- /dev/null
+++ b/chartkit/src/lib/components/useChartSeries.ts
@@ -0,0 +1,97 @@
+import { useMemo } from "react";
+import { asNumber, asString } from "./utils";
+
+export interface ChartSeriesEntry {
+ seriesKey: string;
+ seriesName: string;
+ values: number[];
+ total: number;
+}
+
+export interface ChartSeriesResult {
+ xKeys: string[];
+ xLabels: string[];
+ rankedSeries: ChartSeriesEntry[];
+}
+
+interface ChartSeriesOptions {
+ rows: Array>;
+ xValueField: string;
+ xLabelField?: string;
+ yValueField: string;
+ yLabelField?: string;
+ yAxisTitle?: string;
+ seriesValueField?: string;
+ seriesLabelField?: string;
+}
+
+function pivotRows(options: ChartSeriesOptions): ChartSeriesResult {
+ const {
+ rows,
+ xValueField,
+ xLabelField,
+ yValueField,
+ yLabelField,
+ yAxisTitle,
+ seriesValueField,
+ seriesLabelField,
+ } = options;
+
+ const xValues = new Map();
+ const seriesLabels = new Map();
+ const matrix = new Map>();
+
+ for (const row of rows) {
+ const xValue = asString(row[xValueField]);
+ const xLabelCandidate = xLabelField ? asString(row[xLabelField]) : "";
+ const xLabel = xLabelCandidate || xValue;
+ if (!xValues.has(xValue)) {
+ xValues.set(xValue, xLabel);
+ }
+
+ const seriesValue = seriesValueField ? asString(row[seriesValueField]) : "__single";
+ const seriesLabelCandidate = seriesLabelField ? asString(row[seriesLabelField]) : "";
+ const fallbackSeriesLabel = yAxisTitle ?? yLabelField ?? "Value";
+ const seriesLabel = seriesValueField
+ ? seriesLabelCandidate || seriesValue
+ : fallbackSeriesLabel;
+ if (!seriesLabels.has(seriesValue)) {
+ seriesLabels.set(seriesValue, seriesLabel);
+ }
+
+ const yValue = asNumber(row[yValueField]);
+ const currentSeries = matrix.get(seriesValue) ?? new Map();
+ currentSeries.set(xValue, (currentSeries.get(xValue) ?? 0) + yValue);
+ matrix.set(seriesValue, currentSeries);
+ }
+
+ const xKeys = Array.from(xValues.keys());
+ const xLabels = xKeys.map((key) => xValues.get(key) ?? key);
+
+ const rankedSeries = Array.from(seriesLabels.entries())
+ .map(([seriesKey, seriesName]) => ({
+ seriesKey,
+ seriesName,
+ values: xKeys.map((xKey) => matrix.get(seriesKey)?.get(xKey) ?? 0),
+ }))
+ .map((entry) => ({
+ ...entry,
+ total: entry.values.reduce((sum, value) => sum + value, 0),
+ }))
+ .sort((left, right) => right.total - left.total || left.seriesName.localeCompare(right.seriesName));
+
+ return { xKeys, xLabels, rankedSeries };
+}
+
+export function useChartSeries(options: ChartSeriesOptions): ChartSeriesResult {
+ return useMemo(() => pivotRows(options), [
+ options.rows,
+ options.xValueField,
+ options.xLabelField,
+ options.yValueField,
+ options.yLabelField,
+ options.yAxisTitle,
+ options.seriesValueField,
+ options.seriesLabelField,
+ ]);
+}
diff --git a/chartkit/src/lib/format.ts b/chartkit/src/lib/format.ts
index c6d53da..0cb5d40 100644
--- a/chartkit/src/lib/format.ts
+++ b/chartkit/src/lib/format.ts
@@ -2,6 +2,18 @@ export type ChartKitFmt = string | undefined;
const LOCALE = "en-AU";
+const formatterCache = new Map();
+
+function cachedFormatter(options: Intl.NumberFormatOptions): Intl.NumberFormat {
+ const key = JSON.stringify(options);
+ let fmt = formatterCache.get(key);
+ if (!fmt) {
+ fmt = new Intl.NumberFormat(LOCALE, options);
+ formatterCache.set(key, fmt);
+ }
+ return fmt;
+}
+
function parseDecimals(fmt: string, prefix: string): number | null {
if (!fmt.startsWith(prefix)) {
return null;
@@ -47,14 +59,14 @@ function parseCurrencyThousandsToken(
}
function formatCompactNumber(value: number): string {
- return new Intl.NumberFormat(LOCALE, {
+ return cachedFormatter({
notation: "compact",
maximumFractionDigits: 1,
}).format(value);
}
function formatCurrency(value: number, currency: "AUD" | "USD", decimals: number): string {
- return new Intl.NumberFormat(LOCALE, {
+ return cachedFormatter({
style: "currency",
currency,
minimumFractionDigits: decimals,
@@ -75,13 +87,13 @@ export function formatChartKitNumber(value: number, fmt?: ChartKitFmt): string {
return `US$${value.toFixed(1)}B`;
}
if (fmt === "currency_usd_m") {
- return `US$${value.toLocaleString(LOCALE, { maximumFractionDigits: 0 })}M`;
+ return `US$${cachedFormatter({ maximumFractionDigits: 0 }).format(value)}M`;
}
if (fmt === "currency_aud_bn") {
return `A$${value.toFixed(1)}B`;
}
if (fmt === "currency_aud_m") {
- return `A$${value.toLocaleString(LOCALE, { maximumFractionDigits: 0 })}M`;
+ return `A$${cachedFormatter({ maximumFractionDigits: 0 }).format(value)}M`;
}
if (fmt === "usd") {
return formatCurrency(value, "USD", 2);
@@ -100,10 +112,10 @@ export function formatChartKitNumber(value: number, fmt?: ChartKitFmt): string {
const numDecimals = parseDecimals(fmt, "num");
if (numDecimals !== null) {
- return value.toLocaleString(LOCALE, {
+ return cachedFormatter({
minimumFractionDigits: numDecimals,
maximumFractionDigits: numDecimals,
- });
+ }).format(value);
}
const audDecimals = parseDecimals(fmt, "aud");
@@ -120,10 +132,10 @@ export function formatChartKitNumber(value: number, fmt?: ChartKitFmt): string {
if (currencyThousandsToken) {
const symbol = currencyThousandsToken.currency === "AUD" ? "A$" : "US$";
const scaled = value / 1_000;
- return `${symbol}${scaled.toLocaleString(LOCALE, {
+ return `${symbol}${cachedFormatter({
minimumFractionDigits: currencyThousandsToken.decimals,
maximumFractionDigits: currencyThousandsToken.decimals,
- })}K`;
+ }).format(scaled)}K`;
}
const currencyToken = parseCurrencyToken(fmt);
@@ -131,7 +143,7 @@ export function formatChartKitNumber(value: number, fmt?: ChartKitFmt): string {
return formatCurrency(value, currencyToken.currency, currencyToken.decimals);
}
- return value.toLocaleString(LOCALE);
+ return cachedFormatter({}).format(value);
}
export function formatChartKitDisplayValue(value: unknown, fmt?: ChartKitFmt): string {
diff --git a/chartkit/src/lib/runtime/db-manager.ts b/chartkit/src/lib/runtime/db-manager.ts
index d9cc521..1f1eb0f 100644
--- a/chartkit/src/lib/runtime/db-manager.ts
+++ b/chartkit/src/lib/runtime/db-manager.ts
@@ -44,12 +44,12 @@ export async function getSharedRuntime(
return runtime;
}
+ // createDuckDbRuntime already calls registerDataSources with the initial
+ // dataSources array, so we skip the redundant second call for new runtimes.
const runtimePromise = createDuckDbRuntime(dataSources, onTelemetry, options).catch((error) => {
runtimeByKey.delete(key);
throw error;
});
runtimeByKey.set(key, runtimePromise);
- const runtime = await runtimePromise;
- await runtime.registerDataSources(dataSources);
- return runtime;
+ return runtimePromise;
}