/** * Copyright (c) 2021 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ import { isArray, isBoolean, isFunction, isNumber, isObjectType, isString, tplProcess } from "../../module/util"; import Plugin from "../Plugin"; import {defaultStyle, tpl} from "./const"; import Options from "./Options"; export interface TableViewOptions { selector?: string; categoryTitle?: string; categoryFormat?: (v: Date | number | string) => string; class?: string; style?: boolean; title?: string; updateOnToggle?: boolean; nullString?: string; numberFormat?: (v: number | string) => string; } /** * Expected value type for each supported option, used to validate user input. * Acts as the single source of truth for the set of allowed option keys. * @private */ const optionValidators: { [key in keyof Required]: (v: unknown) => boolean } = { selector: isString, categoryTitle: isString, categoryFormat: isFunction, class: isString, style: isBoolean, title: isString, updateOnToggle: isBoolean, nullString: isString, numberFormat: isFunction }; /** * Check whether the given value is a valid TableView options object. * An empty object is valid (every option falls back to its default) and * unknown keys are ignored, so only a known option holding a value of the * wrong type makes the object invalid. An explicit `undefined` value is * allowed so a consumer can opt out of an option and let it fall back to * the default. * @param {unknown} options Value given to the TableView constructor * @returns {boolean} `true` when `options` is a valid TableView options object * @private */ export function isValidTableViewOptions(options: unknown): options is TableViewOptions { if (!isObjectType(options) || isArray(options) || options === null) { return false; } return Object.entries(options).every(([key, value]) => { const validate = optionValidators[key]; // unknown keys are ignored; known keys must match their expected type return !isFunction(validate) || value === undefined || validate(value); }); } /** * Table view plugin.
* Generates table view for bound dataset. * - **NOTE:** * - Plugins aren't built-in. Need to be loaded or imported to be used. * - Non required modules from billboard.js core, need to be installed separately. * @class plugin-tableview * @param {object} options table view plugin options * @augments Plugin * @returns {TableView} * @example * // Plugin must be loaded before the use. * * * var chart = bb.generate({ * ... * plugins: [ * new bb.plugin.tableview({ * selector: "#my-table-view", * categoryTitle: "Category", * categoryFormat: function(v) { * // do some transformation * ... * return v; * }, * class: "my-class-name", * style: true, * title: "My Data List", * updateOnToggle: false, * nullString: "N/A", * numberFormat: function(v) { * // do some transformation like number formatting * // return typeof v === "number" ? v.toFixed(2) : v; * // or use d3.format * // return typeof v === "number" ? d3.format(".2f")(v) : v; * // or use Intl.NumberFormat * // return typeof v === "number" ? new Intl.NumberFormat("en-US", {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(v) : v; * // or any other number formatting library * ... * return v; * } * }), * ] * }); * @example * import {bb} from "billboard.js"; * import TableView from "billboard.js/dist/billboardjs-plugin-tableview"; * * bb.generate({ * ... * plugins: [ * new TableView({ ... }) * ] * }) */ export default class TableView extends Plugin { private element; constructor(options: TableViewOptions = {}) { super(options); // warn (but don't throw) on invalid options so that unsupported values // simply fall back to their defaults, keeping behavior non-breaking. if (!isValidTableViewOptions(options)) { console?.error?.( "[billboard.js] TableView plugin received invalid options; unsupported values will be ignored.", options ); } this.config = new Options(); return this; } $beforeInit(): void { this.loadConfig(); } $init(): void { const {class: className, selector, style} = this.config; let element = document.querySelector( selector || `.${className || defaultStyle.class}` ); if (!element) { const chart = this.$$.$el.chart.node(); element = document.createElement("table"); chart.parentNode.insertBefore(element, chart.nextSibling); } if (element.tagName !== "TABLE") { const table = document.createElement("table"); element.appendChild(table); element = table; } // append default css style if (style && !document.getElementById(defaultStyle.id)) { const s = document.createElement("style"); s.id = defaultStyle.id; s.innerHTML = defaultStyle.rule; (document.head || document.getElementsByTagName("head")[0]) .appendChild(s); } element.classList.add(...[style && defaultStyle.class, className].filter(Boolean)); this.element = element; } /** * Generate table * @private */ generateTable(): void { const {$$, config, element} = this; const dataToShow = $$.filterTargetsToShow($$.data.targets); let thead = tplProcess(tpl.thead, { title: dataToShow.length ? this.config.categoryTitle : "" }); let tbody = ""; const rows: (number | string)[][] = []; dataToShow.forEach(v => { thead += tplProcess(tpl.thead, {title: v.id}); // make up value rows v.values.forEach((d, i: number) => { if (!rows[i]) { rows[i] = [d.x]; } rows[i].push(d.value); }); }); rows.forEach(v => { tbody += `${ v.map((d, i) => tplProcess(i ? tpl.tbody : tpl.tbodyHeader, { value: i === 0 ? config.categoryFormat.bind(this)(d) : (isNumber(d) ? config.numberFormat.bind(this)(d) : config.nullString) }) ).join("") }`; }); element.innerHTML = tplProcess(tpl.body, { ...config, title: config.title || $$.config.title_text || "", thead, tbody }); } $redraw(): void { const {state} = this.$$; const doNotUpdate = state.resizing || (!this.config.updateOnToggle && state.toggling); !doNotUpdate && this.generateTable(); } $willDestroy(): void { this.element.parentNode?.removeChild(this.element); // remove default css style when left one chart instance if (this.$$.charts.length === 1) { const s = document.getElementById(defaultStyle.id); s?.parentNode?.removeChild(s); } } }