bis-view
v0.1.4
Published
Vue 3 components for tables, cards, and charts with typed column definitions (BiS view).
Maintainers
Readme
bis-view
Vue 3 components to present the same dataset in multiple ways—table, card grid, line chart, bar chart, and pie chart—using one column definition (ColumnsDef) as the single source of truth for labels, types, formatting, and chart eligibility.
For tool- or agent-oriented API detail (types, props, filter contracts, edge cases), see AI-AGENT-REFERENCE.md in this package (node_modules/bis-view/AI-AGENT-REFERENCE.md after install).
Install
npm install bis-viewPeer dependencies
Install these in your app (versions should satisfy the ranges in this package’s peerDependencies):
vuepinia— required when usinguseBisFilters,VdvFilterBar, or integratedDataViewfilters (Pinia backs thebis-view-filtersstore).echartsvue-echartsvue-router— optional (only needed if you usecreateVueRouterBisFiltersUrlSyncfor URL ⇄ filters sync).
Styles
Import the bundled CSS once (e.g. in main.ts or your app shell):
import "bis-view/style.css";If your bundler struggles with the export subpath, use:
import "bis-view/dist/style.css";The stylesheet maps design tokens to CSS variables (--vdv-*, with fallbacks to common --color-* / --space-* names). Override those variables in your app to theme tables and charts.
Core idea
items: an array of row objects (TItem extends ColumnObject).columns: a typed array describing every visible column (including a special"actions"column if you need row buttons).- Derived behavior: sorting, pagination, cell text, chart axes, and aggregates all read from that same
columnsmetadata plusitems—you do not configure charts with a second, parallel schema unless you opt in (e.g.filterDefsfor bar/pie categories and, when using filters, one sharedfilterDefslist forVdvFilterBar/useBisFilters).
Filtering
bis-view exposes a Pinia store (bis-view-filters) plus composables and VdvFilterBar, so one DataViewFilterDef[] can drive the filter UI, server query params, and client-side row filtering.
The library never fetches. Your app:
- Calls
useBisFilters(scope, filterDefs)to readserverFilterParams/filterItems. - Loads data (your HTTP client).
- Passes
serverFilterParamsinto the backend whenapplyMode === "server". - Passes the result through
filterItems(items)forapplyMode === "client"defs.
Prerequisites
app.use(createPinia())before anyuseBisFilters/VdvFilterBarusage.- (Optional — URL sync for shareable/bookmarkable filter state)
app.provideafterapp.use(router):
import {
bisFiltersUrlSyncKey,
createVueRouterBisFiltersUrlSync,
} from "bis-view";
app.use(router);
app.provide(
bisFiltersUrlSyncKey,
createVueRouterBisFiltersUrlSync(router),
);Without this inject, useBisFilters still updates Pinia only; pushQueryFromStore becomes a no-op.
Definitions (DataViewFilterDef)
| Field | Notes |
|-------|------|
| key, type | select | date | dateRange | text | number. |
| labelKey | Filter labels go through translate(labelKey) in VdvFilterBar (supply translate from your i18n). |
| applyMode | server → only serverFilterParams (backend must enforce). client → filterItems / filterItemsByDefs in the browser; not added to serverFilterParams. |
| urlKey | Optional; defaults to key. dateRange maps to query ${urlKey}From / ${urlKey}To. |
| customFilter | Optional; for client defs, replaces default field matching: (item, filterValue) => boolean. |
| select.options() | Runtime () => { value, label }[] (not serializable as JSON by itself). |
useBisFilters(pageScope, filterDefs, options?)
pageScope: stable string per page (e.g."/orders"). Use the same value asVdvFilterBar’sfilter-scopeandDataView’sfilter-scope-key.filterDefs: static array orRef/computedarray.
Returns (important):
filterValues,setFilter,setFilters,clearFiltersserverFilterParams—computed, map to your API (seepickServerFilterswhen building requests manually).filterItems(items, getField?)— applies client defs toitemsusing current store values;getField(row, key)optional for nested keys.
On first invocation, useBisFilters hydrates store from queryToFilters (+ mergeDateRangeDefaults for empty ranges). options.onSyncFromUrl runs when the URL seeded filters.
Calling useBisFilters twice with the same scope is OK (e.g. parent + VdvFilterBar) — they share one Pinia store. Keep filterDefs aligned.
Filter bar UI (VdvFilterBar)
Renders useBisFilters(filterScope, filterDefs) internally. Parent must still call useBisFilters so filteredItems and serverFilterParams reflect the same controls.
Pass translate for labelKey, emptyLabelKey, clearFilters, applyFilters, all, dateFrom, dateTo, etc. (fallback English strings exist when translate is omitted).
Dates use <input type="date" (ISO yyyy-mm-dd).
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { VdvFilterBar, useBisFilters, type DataViewFilterDef } from "bis-view";
const { t } = useI18n();
const PAGE = "/example";
const filterDefs = computed<DataViewFilterDef[]>(() => [
{ key: "q", type: "text", labelKey: "search", applyMode: "client" },
]);
const { filterItems, serverFilterParams } = useBisFilters(PAGE, filterDefs);
const translate = (key: string) => t(key);
</script>
<template>
<VdvFilterBar
:filter-scope="PAGE"
:filter-defs="filterDefs"
:translate="translate"
/>
</template>DataView: built-in filter row
<DataView
:items="items"
:columns="columns"
:filter-defs="filterDefs"
show-filter-bar
filter-scope-key="/example"
:translate="translate"
/>Requires non-empty filterDefs. You still use useBisFilters("/example", filterDefs) (same scope string) in script for data loading and filterItems.
Bar / pie and filterDefs
filterDefs on DataView is also used by DataBarChart / DataPieChart: type: "select" entries participate in getBarChartXDescriptors (see Bar chart).
Headless exports
| Export | Role |
|--------|------|
| useBisViewFilterStore | Pinia API: getFilters, setFilter, filterItems, … |
| filterItemsByDefs, pickServerFilters, matchesFilter, mergeDateRangeDefaults, isEmptyFilterValue | Pure logic / tests / custom pipelines |
| filtersToQuery, queryToFilters | Router query ↔ FilterValues |
| bisFiltersUrlSyncKey, createVueRouterBisFiltersUrlSync | Optional URL bridge |
| calendarMonthRangeISO | Default current month range for empty dateRange during merge |
Business logic
This section describes how the library decides what to render and how, not just API names.
Column model
- Each column has a
keymatching a property on the row (unless you usegetValueor the specialactionskey). typedrives:- Default formatting when no custom
formatteris set (tableFormatters). - Sort behavior (numbers, dates, currencies, text).
- Chart eligibility (see below).
- Default formatting when no custom
- Column header text (table, cards, chart axis pickers) uses
resolveColumnDisplayLabel:- If
labelKeyis set,translate(labelKey)is used with the optionaltranslateprop (i18n lookup). - Otherwise
labelis shown as-is (use for literals or already translated text, e.g. Hebrew resolved in the parent).
- If
actionscolumns renderTableActionbuttons; optionaliconis resolved viaiconResolver(e.g. map a string name to a Vue component).
label vs labelKey
| Field | Behavior |
| ----- | -------- |
| label | Display string when labelKey is absent. Not passed through translate. |
| labelKey | Optional. When set (non-empty), the visible title is translate(labelKey). |
You can export resolveColumnDisplayLabel from the package if you need the same rule outside components.
Table & cards (DataTable, DataColumnar)
Sorting
- Clicking a sortable column header toggles asc/desc on that column key.
- Sort uses
getValuewhen present, otherwiserow[key]. - Comparison is type-aware (
number/currency/date/ text).
Pagination
- Operates on the sorted list.
- Page size changes emit
update:pageSize; the parent owns the effective page size if you store it. - When disabled or page size is zero, all sorted rows are shown.
Cells
- Optional named slots per column key override rendering for that column.
- Otherwise
TableCellformats fromtype,format, orformatter. wrapper(text|button|chip|link) chooses the host element;linkuseslinkComponent(e.g.RouterLink) or a plain<a>withhref(row).
Footer totals
- If any column defines
total, a footer row runs those functions on the full sorted dataset (not only the current page).
- If any column defines
Cards repeat the same row/column semantics in a denser layout; actions appear in a footer strip per card.
DataView (unified toggle)
DataViewMode:"table"|"cards"|"chart"|"scatterChart"|"barChart"|"pieChart".- Renders modes based on
view(v-model viaupdate:view). - Line, scatter (raw points, same date + numeric columns as line, no group-by), bar, and pie buttons when supported.
- Optional
chartColors: string array forwarded to line, scatter, bar, and pie ECharts options (seeresolveChartColors). - Optional
pieTopN,pieInnerRadius: forwarded to the pie view only (top slices + Other, donut hole). - If the requested view becomes invalid (e.g. columns change), the view falls back to
tableandupdate:viewmay emittable. - Use
normalizeDataViewModeto mirror the same fallback in parent state.
Scatter (DataScatterChart)
Same column eligibility as the line chart (chartSupported: at least one date and one numeric/currency). Uses the same X/Y pickers; plots one point per row (no group by / bucketing). Implemented with useTimeSeriesChart (plotPoints only).
Line chart (DataChart)
Eligibility (“business rule”): at least one column with type === "date" and one with type === "number" or "currency" (see chartSupported / dataChartSupport).
Behavior:
- X axis uses a chosen date column; Y uses a numeric/currency column.
- Raw points are
(time, value)pairs; invalid dates or numbers are skipped. Group by(granularity none / day / week / month / year) buckets points in local calendar time. Inside each bucket, Y values are combined withaggregateY—defaultsumAggregate—so overlapping rows in the same bucket add up (typical for amounts over time).- Tooltip titles use
formatChartBucketLabelso labels match the granularity.
Charts render with Apache ECharts via vue-echarts. If every Y value is zero after aggregation (line) or in raw points (scatter), or for bar/pie, charts show the same empty state as noData where applicable (no misleading charts).
Pie chart (DataPieChart)
Same eligibility, pickers, and aggregation as the bar chart (aggregateBarChart, getBarChartXDescriptors, filterDefs). Optional pieTopN (≥ 2): keep the largest values and merge the rest into pieChartOther. Optional pieInnerRadius (e.g. 40 or "45%") for a donut.
Bar chart (DataBarChart)
Eligibility: at least one categorical X source:
type === "boolean"columns, orDataViewSelectFilterDefentries passed infilterDefs(type: "select"), paired bykey—optionally joined to a real column via the samekey(if the row only holds an id,getValue/ dot-path lookups can still resolve a value).
Ordering of X descriptors: boolean columns first, then select-backed keys; duplicate keys dedupe (boolean wins if both exist—unlikely).
Y axis:
sum/meanneed a numeric/currency column.countcounts rows per category and does not require a numeric column.
Buckets: values are aggregated per category label. Booleans map to yes / no / empty** via **translate**. Select categories use **options()** on the **DataViewSelectFilterDef` for display labels where possible.
See aggregateBarChart, getBarChartXDescriptors, barChartSupported.
Internationalization (translate)
- Components accept optional
translate: (key: string) => string(e.g.vue-i18nt). - Built-in UI strings (toolbar, pagination,
yes/no, chart chrome, …) go through the same helper that applies default English whentranslateis missing, or when a key is unknown. - Column headers do not call
translate(label)anymore. Uselabelfor final text, or setlabelKeyto pass only that string throughtranslate.
Icons (iconResolver)
TableAction.iconmay be a Vue component or an opaque string (your app maps strings to icons if you supplyiconResolver).
Integration props (summary)
| Prop | Role |
|------|------|
| translate | Localize all built-in keys (table, loading, chart strings, pagination, …). |
| iconResolver | Resolve actions[k].icon when icons are strings. |
| linkComponent | e.g. RouterLink; default link behavior otherwise. |
| filterDefs | Bar and pie category sources from select filter definitions (DataViewFilterDef[]); also the schema for VdvFilterBar / useBisFilters when you use filtering. |
| showFilterBar | When true, render VdvFilterBar above the view toggle (requires filter-scope-key and non-empty filterDefs). |
| filterScopeKey | Scope string for embedded bar + useBisFilters (must match the scope you pass to useBisFilters in script). |
| chartColors | Optional ECharts colors for line / bar / pie. |
| pieTopN | Pie only: max slices; remainder merged into pieChartOther (requires ≥ 2 to take effect). |
| pieInnerRadius | Pie only: donut inner radius (number → percent, or string e.g. "40%"). |
Quick usage
<script setup lang="ts">
import { DataView, type ColumnsDef } from "bis-view";
interface Row extends Record<string, unknown> {
id: number;
name: string;
amount: number;
createdAt: string;
}
const items: Row[] = [
/* ... */
];
const columns = [
{ key: "name", label: "Name", type: "text" },
{ key: "amount", label: "Amount", type: "currency" },
{
key: "createdAt",
label: "Created",
type: "date",
format: "datetime",
},
] as const satisfies ColumnsDef<Row>;
</script>
<template>
<DataView
:items="items"
:columns="columns"
:translate="(k) => k"
:show-view-toggle="true"
/>
</template>Pass translate from vue-i18n, iconResolver, and link-component in real apps.
Exported utilities
Use these when you want the same rules outside the UI (e.g. feature flags or server docs):
| Export | Purpose |
|--------|---------|
| useBisFilters, UseBisFiltersOptions | Page scope + defs → filterValues, serverFilterParams, filterItems, URL sync when provided. |
| VdvFilterBar | Filter form UI (Pinia + optional URL). |
| useBisViewFilterStore | Low-level Pinia store for filter state. |
| filterItemsByDefs, pickServerFilters, matchesFilter, mergeDateRangeDefaults, isEmptyFilterValue, defaultGetField, urlHasDateRange | Pure filter engine. |
| filtersToQuery, queryToFilters | Serialize / parse router query. |
| bisFiltersUrlSyncKey, BisFiltersUrlSync, createVueRouterBisFiltersUrlSync | Optional vue-router URL bridge. |
| calendarMonthRangeISO | Default month range for dateRange merge. |
| translateDataViewKey | Resolve a key with the same built-in English fallbacks as DataView. |
| chartSupported, getDateColumnKeys, getNumericColumnKeys | Line-chart column detection. |
| barChartSupported, getBarChartXDescriptors | Bar and pie category detection (+ filterDefs). |
| normalizeDataViewMode | Same view fallbacks as DataView (unsupported chart / scatterChart / barChart / pieChart → table). |
| aggregateBarChart | Same aggregation as DataBarChart / DataPieChart. |
| applyPieTopN, hasRenderableSeriesValues | Pie bucketing and “all zeros” checks. |
| resolveChartColors, DEFAULT_CHART_COLORS, chartLineAreaFill | Chart palette helpers. |
| useCategoricalChart | Shared bar/pie axis state (category column, aggregate, series). |
| useTimeSeriesChart | Shared line/scatter pipeline (date + numeric columns, raw plotPoints, aggregated chartPlotPoints). |
| aggregateTimeSeriesPoints, formatChartBucketLabel, sumAggregate, … | Line-chart bucketing. |
| formatCell, getCellValue, getRowId | Table/card formatting and identity. |
| resolveColumnDisplayLabel, ColumnLikeForLabel | Same label / labelKey title rules as the UI. |
| useTableSort, useTablePagination | Headless sort/pagination. |
Build from source
In this repo’s package folder:
npm run build
npm testprepublishOnly runs npm run build before npm publish.
License
Released under the MIT License. See LICENSE in this package.
