Skip to main content

The chart extension

mc-semantic-ui-ext-chart brings both halves of chart: the UiChart node type and the two painters that draw it — one for the browser, one for server-side rendering.

That pairing is the rule the project follows: a node type and the ability to render it belong in the same place. The core owns only what it can actually draw, so it never promises a picture it cannot produce, and never has to depend on a charting library. chart is therefore not part of the core vocabulary — add this module and the node appears in the Java model, in Jackson, in the browser renderer and in SSR at once.

Live — four chart types from one data set. Hover anywhere over a column: the tooltip shows every series at once, the column lifts and a crosshair marks it. Rendered on the server without the script, the same chart falls back to native SVG <title> tooltips.

What you get​

Node typechart (UiChart), contributed by this module
TypesBAR, LINE, AREA, PIE, DONUT
Outputplain SVG — no canvas, no charting library, no runtime dependency
Works without JavaScriptyes, when rendered server-side — with native tooltips instead of the hover

The diagram extension is built the same way, and is the model to copy if you write your own.

Install​

With the asset registry

A Spring Boot host with the core's asset registry needs none of the browser wiring below: the jar declares its files, and installAll(renderer, bus) from /sui/assets.js installs it. See the asset registry.

<dependency>
<groupId>ai.mindconnect</groupId>
<artifactId>mc-semantic-ui-ext-chart</artifactId>
<version>0.4.3</version>
</dependency>

Nothing else to configure. The module ships templates/sui/chart.hbs plus the Handlebars helper it needs, contributed through SuiHelperContributor — found either as a Spring bean or via ServiceLoader, so it works in a plain-Java app with no Spring on the classpath.

@GetMapping("/dashboard")
public UiPage dashboard() {
var chart = new UiChart();
chart.setId("revenue");
chart.setTitle("Revenue");
chart.setChartType(UiChart.ChartType.BAR);
chart.setData(quarterlyRevenue()); // labels + one series
return UiPage.of("/dashboard", chart);
}
A chart that survives JavaScript being off

The server-rendered output is static SVG with <title> tooltips — no script tag anywhere. That makes it the rare chart that still works in an email client, a PDF export, or a hardened browser.

The node​

FieldTypeMeaning
idStringNode id — also the DOM id and the patch target.
titleStringOptional heading rendered above the chart (<h2>).
chartTypeLINE · BAR · PIE · DONUT · AREAWhich chart to draw.
dataUiChart.ChartDataThe payload.
stackedBooleanA bar chart with several series stacks them, one bar per label, the total in the tooltip. Otherwise they stand side by side.
crosshairBooleanThe dashed line down the hovered column of a bar, line or area chart. On unless false.
valueFormatUiChart.ValueFormatprefix, suffix, decimals — how values read in tooltips, legends and on the axis.
cssClassStringExtra CSS class on the wrapper <div>.

UiChart.ChartData and its nested Series:

FieldTypeMeaning
data.labelsList<String>Category labels — the x axis, or the pie slice names.
data.seriesList<ChartData.Series>One or more series.
series[].nameStringSeries name, for legends and tooltips.
series[].valuesList<Number>The values, positionally aligned with labels.

There are no colour, axis or scale fields: the painter picks round ticks for the value axis itself, and the palette comes from CSS. What a value means — money, a unit — is the model's, through valueFormat:

UiChart.of("cost", "Cost per day (CHF)", UiChart.ChartType.BAR, data)
.valueFormat(null, " CHF", 2); // tooltip "1.61 CHF", axis "1.5"
UiChart.of("tokens", "Tokens per day", UiChart.ChartType.BAR, data)
.stacked(true); // input and output, one bar a day

Values are written with thousands separators (143,911). Without decimals a whole number gets none, a number of at least 1 two, and a smaller one up to four. The axis keeps the prefix and leaves the suffix to the title, and writes large ticks short (250k, 1.5M).

Building one​

var data = new UiChart.ChartData();
data.setLabels(List.of("Q1", "Q2", "Q3", "Q4"));

var revenue = new UiChart.ChartData.Series();
revenue.setName("Revenue");
revenue.setValues(List.of(24, 38, 30, 45));
data.setSeries(List.of(revenue));

UiChart.of("chart-revenue", "Revenue by quarter", UiChart.ChartType.BAR, data);

data is intentionally minimal. Labels plus named numeric series is the common denominator every charting library accepts. If your chart needs more (dual axes, time scales), carry it in your own extension node type rather than stretching UiChart.

The hover​

With the extension's script on the page, every bar, line and area chart has one transparent band per column over the whole plot height. The pointer anywhere over a column:

  • shows a tooltip at once — the label, each series with its colour and value, and for stacked bars the total — beside the column, flipping sides at the chart's right edge;
  • marks the column: its bars grow slightly and brighten, its dots grow, every other column dims (CSS transitions, off under prefers-reduced-motion);
  • draws the dashed crosshair down its centre, unless crosshair is false.

A donut or pie segment under the pointer, or its legend row, marks the segment and the row together. The listeners sit on the document, added once by install() (or enableHover()), so server-rendered charts come alive too; the script removes the native <title>s the first time it draws its own.

Charts nest. A chart is a plain node, so it fits anywhere a node fits — including a tree-node's content slot or a detail panel.

Both renderers draw the same picture​

The painter exists twice — ChartPainter.java and chart/extension.ts — because the project renders from both sides. They are held to byte-identical output: ChartPainterGoldenTest draws every fixture in src/test/resources/chart-golden/ in Java and compares it with what the TypeScript painter drew for it (npm run build && node scripts/chart-golden.mjs writes those).

Three formatting rules exist only to keep that true, and each is pinned by a test because each one actually broke once:

RuleWhy
Whole numbers print without .0Java would write 30.0 where JS writes 30 — visible in tooltips
Negative zero is collapsedThe first pie segment starts at -0.0: Java prints -0.00, JS prints 0.00
Decimals always use a pointA German default locale would produce 12,5 and break the SVG
Rounding starts from the exact binary valueString.format rounds 1.005 to 1.01, JavaScript's toFixed to 1.00
Tick steps are found by multiplying by tenMath.pow may differ in the last bit between the two

Swapping the painter​

install() is one renderer.register("chart", …) call. Registering your own handler afterwards replaces it, which is how you swap in a charting library or a different look:

installChart(renderer);
renderer.register("chart", (node) => `<div class="sui-chart" id="${node.id}">…</div>`);

The handler receives the node and returns an HTML string, exactly like the built-in renderers — see adding a node type of your own; the mechanism is identical, except that chart already has a Java model, so you only supply the drawing half.

Styling​

Colours come from CSS variables with literal fallbacks, so a chart restyles from your stylesheet and still renders standalone:

:root {
--sui-chart-1: #0f766e;
--sui-chart-2: #b91c1c;
/* … up to --sui-chart-6 */
}

Layout (size, legend placement, the empty state, the hover's animation and the tooltip) lives in /sui-ext/chart/chart.css — override the .sui-chart* classes to change it: .sui-chart-tip is the tooltip, .is-active marks the hovered column or segment, .sui-chart--hover the chart while one is.

See also​