@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
93 lines (92 loc) • 3.29 kB
JavaScript
import { aggregatePoints } from './rows.js';
/** Above this many categories, labels sit on the Y axis. */
const VERTICAL_CATEGORY_MAX = 8;
/**
* Bar object the chart element can plot.
* Single series: category × metric, no groupBy.
* Grouped: a second category column, never the same field as the axes.
* Many categories flip to horizontal so the labels stay readable.
*/
export function barChartPayload(_chartType, fields, rows) {
const category = fields.category;
const metric = fields.metric;
if (!category || !metric || category === metric)
return null;
const series = seriesField(fields, category, metric, rows);
const data = series
? rowsByCategoryAndSeries(rows, category, metric, series)
: singleSeriesRows(rows, category, metric);
if (!data)
return null;
const orientation = categoryCount(data, category) > VERTICAL_CATEGORY_MAX
? 'horizontal'
: 'vertical';
const grouped = !!series;
const xField = orientation === 'horizontal' ? metric : category;
const yField = orientation === 'horizontal' ? category : metric;
return {
chartType: barChartType(orientation, grouped),
name: 'bar-chart',
orientation,
xAxe: [xField],
yAxe: [yField],
...(series ? { groupBy: [series], stacked: false } : {}),
data,
};
}
function seriesField(fields, category, metric, rows) {
const candidate = fields.series ?? fields.group ?? fields.groupBy;
if (!candidate || candidate === category || candidate === metric)
return undefined;
if (!(candidate in rows[0]))
return undefined;
const values = new Set();
for (const row of rows) {
const value = String(row[candidate] ?? '').trim();
if (value)
values.add(value);
if (values.size > 1)
return candidate;
}
return undefined;
}
function singleSeriesRows(rows, category, metric) {
const points = aggregatePoints(rows, category, metric);
if (!points)
return null;
return points.map((point) => ({ [category]: point.label, [metric]: point.value }));
}
function rowsByCategoryAndSeries(rows, category, metric, series) {
const sums = new Map();
const order = [];
for (const row of rows) {
const cat = String(row[category] ?? '').trim();
const ser = String(row[series] ?? '').trim();
const value = Number(row[metric]);
if (!cat || !ser || !Number.isFinite(value))
continue;
const key = `${cat}\0${ser}`;
if (!sums.has(key))
order.push(key);
sums.set(key, (sums.get(key) ?? 0) + value);
}
if (order.length === 0)
return null;
return order.map((key) => {
const split = key.indexOf('\0');
return {
[category]: key.slice(0, split),
[series]: key.slice(split + 1),
[metric]: sums.get(key),
};
});
}
function categoryCount(rows, category) {
return new Set(rows.map((row) => String(row[category] ?? ''))).size;
}
function barChartType(orientation, grouped) {
if (orientation === 'horizontal') {
return grouped ? 'barHorizontalGrouped' : 'barHorizontal';
}
return grouped ? 'barGrouped' : 'barChart';
}