apex-commons
The shared core behind the ApexCharts product family: licensing, watermarking, theming, internationalisation, spring motion and coordinated filtering.
It exists because these things are not chart-specific. A licence key, a theme token, a text direction and a filter mean the same thing to a chart, a tree, a sankey diagram and a grid, and each of those having its own copy meant four implementations drifting apart. Anything the family needs to agree on lives here, so it is agreed on once.
Most users never install this directly. It arrives as a dependency of the product you are using, and the parts you touch are reached through that product's own API. Install it yourself when you are building against several of them at once, or using the crossfilter engine with no chart present.
Installation
npm install apex-commons
Licensing
LicenseManager is the enforcement path for the whole family, so a key set once here is honoured by every Apex product on the page.
import {LicenseManager} from 'apex-commons';
LicenseManager.setLicense('your-license-key');
Without a valid key a product keeps working and carries a trial watermark. That is deterrence and honest-customer compliance rather than DRM: no feature is ever degraded.
The key is shared by every product on the page, so a later setLicense never takes away what an earlier one established: a different key is ignored, with a warning, when it is invalid or when its plan is lower than the valid key already in force (a Pro key after a Premium one). Call setLicense('') first to move to a lower plan on purpose.
The products this serves are free under the Community terms for organisations under the revenue threshold, and need a Commercial licence otherwise. See https://apexcharts.com/pricing.
| Member | Description |
|---|---|
LicenseManager |
Static class holding the licence for every Apex product on the page. |
setLicense |
Set the key. Call before creating any instance. See above. |
getKey |
The key currently in force, or null. |
isLicenseValid |
Whether the key in force is valid right now. |
isKeyValid |
Whether a given key would be valid, without setting it. |
getLicenseStatus |
The full LicenseValidationResult for the key in force. |
validateKey |
Validate a given key and return its result, without setting it. |
onChange |
Subscribe to licence changes. Returns an unsubscribe function. |
LicenseData |
The decoded payload: plan, issue and expiry dates, optional domains. |
LicenseValidationResult |
The outcome of a check: whether it is valid, and why not when it is not. |
LicenseChangeListener |
The callback shape onChange takes. |
Plan tiers
Whether a licence covers a feature, in one place, so every Apex product answers it the same way.
planAtLeast has one hard boundary and one deliberate softness. Without a valid licence it always returns false. With a valid licence whose plan string is not recognised it returns true and warns once, because the holder has a signed, unexpired key: refusing them a feature over a string we failed to map is worse than granting one. Gating here is deterrence and honest-customer compliance, not DRM.
import {planAtLeast} from 'apex-commons';
if (planAtLeast('premium')) {
// show the tool
}
| Member | Description |
|---|---|
planAtLeast |
Whether the licence in force covers a feature needing at least the given plan. |
currentPlan |
The plan the licence grants, or null. For display; use planAtLeast to gate. |
normalisePlan |
Resolve a raw plan string to a known plan, ignoring case and space. null if unknown. |
PLAN_ORDER |
Every plan string a key may carry, in commercial ladder order. Not the gating order. |
PLAN_ALIASES |
Plan strings that may appear in issued keys but are not current plan names. |
ApexPlan |
The plan names: community, pro, premium, embedded, oem. |
There are three gating levels, not five. community < pro < premium, and embedded and oem rank equal to premium rather than above it. That is because they are not a higher feature tier: they are a second axis. pro and premium price the features a product uses, while embedded and oem price redistributing it, and an OEM licence is sold with every licensed feature included. So a key carrying oem grants exactly what premium grants, no more, and premium is the ceiling. A feature gating above it would be reachable by no purchasable licence at all.
The two axes are bought together, not instead of each other: an OEM customer buys redistribution rights per application and a developer licence for each developer who works on that application. Neither axis is visible here, because seats and application counts are a billing concern and this module only reads what a key already carries.
Both strings stay in the vocabulary rather than being removed, because planAtLeast fails open on a plan it does not recognise: drop them and a key carrying one would be granted everything, which is the opposite of the intent.
community never appears in a key, because that tier is issued none, so planAtLeast('community') means "holds any licence at all".
oem was called enterprise until 2026-09-14, and PLAN_ALIASES maps the old string so a key carrying it still resolves to a real rank rather than fail-opening.
Watermark
The trial mark an unlicensed product adds to its container.
| Member | Description |
|---|---|
Watermark |
Static class that adds, finds and removes the trial mark. |
WatermarkOptions |
Options accepted by add and remove. |
ATTR |
The attribute the mark carries, for finding it. |
add |
Add the mark to a container. Returns the element, or null. |
remove |
Remove the mark from a container. |
exists |
Whether a container currently carries a mark. |
node |
The container's own mark (a direct child), or null. |
applyStyles |
Apply the mark's styles to an element. |
untrack |
Stop tracking a container, without removing its mark. |
add and remove track the container, and a later licence change (a late setLicense, or a signature failing verification) adds or removes its mark again. A product calls untrack when it is destroyed and leaves its container to the page. Each container answers for its own mark only, so remove on a container never erases the mark of another product's container nested inside it. With requires set, a valid key whose plan is too low logs a warning once per page saying why the mark stays. WatermarkOptions steers that:
| Option | Type | Default | Description |
|---|---|---|---|
manage |
boolean |
true |
Whether this package reconciles the container against the page licence. Pass false when the product decides for itself. |
requires |
ApexPlan |
undefined |
The lowest plan that removes the mark from this container, held per container and honoured on every reconcile. Without it any valid licence removes it. Decide add or remove at render with planAtLeast(requires) so the two agree. |
import {planAtLeast, Watermark} from 'apex-commons';
// A product sold from Premium up: a valid Pro key keeps the mark.
const options = {requires: 'premium'};
if (planAtLeast('premium')) Watermark.remove(container, options);
else Watermark.add(container, options);
Internationalisation
| Member | Description |
|---|---|
TextDirection |
'ltr', 'rtl' or 'auto'. |
resolveMessages |
Merge a caller's partial message overrides onto a product's defaults. |
applyDirection |
Set text direction on an element, resolving 'auto' against the document. |
isRTL |
Whether a direction resolves to right-to-left, given an optional element. |
Shadow DOM
Helpers that give the same answer whether a product renders in the light DOM, in a shadow root, or in a shadow root nested inside another. Each takes the product's own element and works out its root from it.
| Member | Description |
|---|---|
rootOf |
The root an element renders in: its ShadowRoot, or its Document. |
eventOrigin |
The node an event really started on, through shadow retargeting. Use it for "was this click inside?" tests in document listeners. |
activeElementIn |
The focused element, followed down through open shadow roots. |
queryInRoot |
querySelector in the element's root first, then in the document. |
queryAllInRoot |
querySelectorAll in the element's root, or in the document when the root has no match. |
injectStyle |
Add a stylesheet to the element's root once, prepended so page rules win. Returns a release function; the sheet goes with its last user. |
InjectStyleOptions |
Options for injectStyle: a CSP nonce. |
styleSheetsFor |
Every stylesheet that applies to an element, including each enclosing root's own and adopted sheets. For exporters. |
Theming
A page declares the --apx-* custom properties once on :root and every Apex product on it follows. Anything set explicitly on a product wins, so a product already themed through its own options is untouched.
| Member | Description |
|---|---|
ApxTokens |
The resolved token set: accent, foreground, grid, surface and the series palette. |
APX_TOKEN_VARS |
The custom property name for each token role. |
APX_SERIES_VAR_PREFIX |
Prefix for the ordered series palette, --apx-series-N. |
readTokens |
Read the tokens currently in force on an element. |
resolveTokenLayer |
Read the tokens for an element, with a registered theme underneath. |
pickToken |
Resolve one value through the precedence order: option, then CSS variable, then token, then fallback. |
TokenPrecedenceInput |
The candidates pickToken chooses between. |
ApxTheme |
A named theme: the token roles it contributes. |
registerTheme |
Register or override a named theme on the shared registry. |
getTheme |
Look up a registered theme by name. |
registeredThemes |
The names currently registered. |
unregisterTheme |
Remove a registered theme. |
OwnedToken |
Tracks a custom property a product set itself, so it can put back what it found. |
OSThemeWatcher |
Watches the operating system's light and dark preference. |
OSThemeState |
The current OS preference. |
OSThemeWatcherOptions |
Options for the watcher. |
Motion
A headless spring driver, shared so that motion feels the same across the family. It computes values and holds no opinion about what draws them.
| Member | Description |
|---|---|
Spring |
One spring: its current value, velocity, target and rest thresholds. |
makeSpring |
Create a spring at a value, with a stiffness and damping. |
stepSpring |
Advance a spring by a timestep. Returns whether it is still moving. |
retarget |
Point a spring at a new target, keeping its velocity. |
snapSpring |
Move a spring to a value immediately, clearing velocity. |
SpringRest |
The velocity and displacement below which a spring counts as arrived. |
UNIT_REST |
Rest thresholds for springs running over a 0 to 1 range, rather than pixels. |
SpringPreset |
'crisp', 'gentle' or 'snappy'. |
SPRING_PRESETS |
The stiffness and damping pair behind each preset. |
resolveSpring |
The pair for a preset, falling back to the default. |
CAMERA_SPRING |
The pair used for pan and zoom, which wants less overshoot than content. |
WAVE_STAGGER_MS |
Delay between neighbours in a staggered entrance. |
SpringScene |
Drives many named springs on one animation loop, with a single rest callback. |
SpringSceneOptions |
Options for a scene, including its clock. |
SpringValues |
The current value of every spring in a scene, by name. |
SceneObjectOptions |
Options for one object added to a scene. |
SceneClock |
The scene's time source. Substitutable, so tests need no timers. |
diffNodes |
Split previous and next id sets into entering, surviving and leaving. |
NodeDiff |
The result of diffNodes. |
ringDistances |
Distance of each node from a set of roots, for staggering a wave outwards. |
ReducedMotionWatcher |
Watches the user's reduced-motion preference. |
ReducedMotionWatcherOptions |
Options for the watcher. |
Crossfilter
Coordinated filtering across any number of views. One shared record set, and per view a dimension (row => key), a reduction and a filter.
A view's aggregation is computed over the records passing every other view's filter, never its own, so each view keeps showing what else is reachable instead of collapsing to its own selection. A view's own filter only marks which of its buckets are selected.
Because a dimension is an arbitrary function, a map region, a tree subtree, a sankey link, a grid column and a chart category are all the same thing to it, and a set of views can coordinate with no chart present at all.
import {Crossfilter} from 'apex-commons';
const cf = Crossfilter.getOrCreate({id: 'audit'});
cf.setRecords(rows);
cf.registerDimension('byRegion', {dimension: (r) => r.region});
cf.on('change', () => render(cf.aggregateAll()));
cf.filter('byRegion', ['EU']);
| Member | Description |
|---|---|
Crossfilter |
The coordinator. |
getOrCreate |
Static. The coordinator for an id, creating it if needed. |
get |
Static. The coordinator for an id, or null. |
setRecords |
Replace the shared record set. |
registerDimension |
Add or replace a view's dimension and reduction. |
hasDimension |
Whether a view is registered. |
removeDimension |
Remove a view. |
filter |
Set a view's filter. |
toggleKey |
Add or remove one key from a view's filter. |
filterOf |
A view's current filter, or null. |
clear |
Clear one view's filter. |
reset |
Clear every filter. |
aggregateFor |
The aggregation for one view. |
aggregateAll |
The aggregation for every registered view. |
filteredRecords |
Records passing every filter, optionally excluding one view's own. |
filteredRows |
Records passing every filter. |
state |
A snapshot of records, filters and aggregations. |
on |
Subscribe to an event. Returns an unsubscribe function. |
off |
Remove a handler. |
dataTable |
Render a paged table of the filtered rows into an element. |
Crossfilter types
| Type | Description |
|---|---|
CrossfilterOptions |
Options for getOrCreate, including the shared id. |
CrossfilterRow |
One record. Shape is entirely the caller's business. |
CrossfilterState |
The snapshot state() returns. |
CrossfilterEvent |
The events a coordinator emits. |
CrossfilterListener |
The callback shape on takes. |
DimensionSpec |
A view's dimension, reduction, ordering and binning. |
DimensionAccessor |
Maps a row to this view's key. |
DimensionType |
Whether a dimension is categorical or numeric. |
DimensionOrder |
How a view's buckets are ordered. |
BinSpec |
How a numeric dimension is bucketed. |
ReduceSpec |
How rows in a bucket become a scalar. |
Reducer |
A custom reduction function. |
FilterInput |
What filter accepts: a set of keys, or a numeric range. |
Aggregate |
Union of the aggregation shapes. Narrow it before reading. |
CategoryAggregate |
A flat list of keys and values. |
RangeAggregate |
A numeric range aggregation. |
MatrixAggregate |
A two-dimensional aggregation, which has no flat keys list. |
DataTableOptions |
Options for dataTable. |
DataTableColumn |
One column in a data table. |
DataTableHandle |
The handle dataTable returns. |
Indicators
Moving-window indicators as pure functions, lifted out of apexstock so the chart family and the stock product share one implementation. Each takes Array<number | null> and returns the same length, with null wherever a value is not defined.
A window containing a gap yields null rather than an average of however many values were present, which would quietly compute a mean of four and show it beside means of ten.
sma is slice-invariant: a window averages to the same value whatever precedes it, so sma(values.slice(i - period + 1, i + 1), period) at its last position equals sma(values, period)[i] exactly. That is what lets a streaming consumer extend a series by recomputing only the tail window. It costs a running total, so sma is O(n * period).
| Member | Description |
|---|---|
sma |
Trailing simple moving average, aligned to the last position in each window. Slice-invariant, as above. |
ema |
Exponential moving average, seeded from the first complete simple window rather than the first value. A gap ends the run and the next complete window starts a new one. |
bollinger |
A simple moving average with a moving standard deviation either side, using the population denominator, which is the convention for this band. |
Bands |
What bollinger returns: middle, upper and lower, each the length of the input. |
import {bollinger, ema, sma} from '@apex/commons';
sma([1, 2, 3, 4, 5, 6], 3); // [null, null, 2, 3, 4, 5]
ema([1, 2, 3, 4, 5], 3); // [null, null, 2, 3, 4]
bollinger([2, 4, 4], 3, 2).upper;
Histogram binning
Observations in, bin edges and counts out. Pure arithmetic, so it is testable on its own and inert under SSR.
Gaps mean the opposite of what they mean above. A moving average over a window containing a gap is undefined, because the window is a claim about a stretch of the series. A histogram is a tally of the observations that exist, so a gap is simply not one; histogram skips them and reports how many it skipped.
| Member | Description |
|---|---|
histogram |
Bin a series and count it in one call, tolerating gaps. The convenience most callers want. |
computeBinning |
Choose bin edges for a set of finite observations. Precedence: an explicit binWidth, then an explicit bin count, then a named rule. |
binCounts |
Count observations into a set of edges. |
binIndexOf |
The bin holding one value, or -1 outside the edges. Bins are half-open except the last, which includes its upper edge. |
quantileSorted |
Quantile of an ascending-sorted array, by the "type 7" definition that R, NumPy and a spreadsheet's QUARTILE all share. |
Histogram |
What histogram returns: a Binning plus counts, midpoints, total and missing. |
Binning |
edges, binWidth, the rule that actually chose the width, and whether the count was capped. |
BinOptions |
bins (a rule name or a fixed count), binWidth, and a range to frame the bins on so several histograms share a scale. |
BinRule |
The named rules: auto, fd, rice, scott, sqrt, sturges. auto is the narrower of fd and sturges, falling back to sturges when the IQR is zero. |
import {histogram} from '@apex/commons';
const h = histogram([1, 2, 2, 3, null, 9]);
h.counts; // observations per bin
h.missing; // 1
Browser support
Every module is SSR-safe: nothing touches document at import time, and the pieces that need the DOM take the element they act on. Crossfilter is DOM-free apart from its optional dataTable helper.