From 215b611fe68ea37360e8939e9902d489aa502b75 Mon Sep 17 00:00:00 2001 From: Jae Sung Park Date: Thu, 15 Jul 2021 17:51:40 +0900 Subject: [PATCH] feat(plugin): Intent to ship TableView plugin Implement new TableView plugin Close #1873 --- config/webpack/plugin.cjs | 4 +- demo/demo.js | 33 ++++ demo/index.html | 3 + src/Plugin/tableview/Options.ts | 107 +++++++++++ src/Plugin/tableview/const.ts | 52 ++++++ src/Plugin/tableview/index.ts | 171 ++++++++++++++++++ .../bubble-compare/bubble-compare-spec.ts | 4 + test/plugin/tableview/tableview-spec.ts | 129 +++++++++++++ types/plugin/tableview/index.d.ts | 13 ++ types/plugin/tableview/options.d.ts | 38 ++++ 10 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 src/Plugin/tableview/Options.ts create mode 100644 src/Plugin/tableview/const.ts create mode 100644 src/Plugin/tableview/index.ts create mode 100644 test/plugin/tableview/tableview-spec.ts create mode 100644 types/plugin/tableview/index.d.ts create mode 100644 types/plugin/tableview/options.d.ts diff --git a/config/webpack/plugin.cjs b/config/webpack/plugin.cjs index c4aacfbd2..cf494d675 100644 --- a/config/webpack/plugin.cjs +++ b/config/webpack/plugin.cjs @@ -30,9 +30,7 @@ const config = { filename: `billboardjs-plugin-[name].js`, library: ["bb", "plugin", "[name]"], libraryExport: "default", - libraryTarget: "umd", - umdNamedDefine: true, - globalObject: "this" + publicPath: "/dist/plugin" }, plugins: [ new webpack.BannerPlugin({ diff --git a/demo/demo.js b/demo/demo.js index 5e2b88455..15229ae75 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -3045,6 +3045,39 @@ d3.select(".chart_area") bubblecompare: {minR: 11, maxR: 74, expandScale: 1.1} }] } + }, + TableView: { + description: "Generates table view for bound dataset.
Must load or import plugin before the use.", + options: { + data: { + x: "x", + columns: [ + ["x", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021"], + ["data1", 1230, 2380, 1200, 1238, 1500, 2500, 2540, 1265, 550, 240], + ["data2", 500, 120, 100, 200, 840, 935, 825, 1123, 385, 980], + ["data3", 1223, 153, 850, 300, 250, 3120, 1205, 840, 999, 1280], + ["data4", 1130, 2135, 1020, 1138, 2119, 1228, 3256, 138, 2355, 220], + ["data5", 1223, 2310, 1210, 2220, 1238, 1205, 2120, 2113, 1185, 1098] + ], + types: { + data3: "area", + data4: "step", + data5: "bar" + } + }, + axis: { + x: { + type: "category" + } + }, + _plugins: [{ + tableview: { + title: "My Yearly Data List", + categoryTitle: "Year", + style: true + } + }] + } } }, Point: { diff --git a/demo/index.html b/demo/index.html index 8308ea762..95cac54e9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -181,6 +181,9 @@

Sample code

}), plugins_bubblecompare: path.map(function(p) { return p + "plugin/billboardjs-plugin-bubblecompare.js" + }), + plugins_tableview: path.map(function(p) { + return p + "plugin/billboardjs-plugin-tableview.js" }) }); diff --git a/src/Plugin/tableview/Options.ts b/src/Plugin/tableview/Options.ts new file mode 100644 index 000000000..4d48845d0 --- /dev/null +++ b/src/Plugin/tableview/Options.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2021 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +/** + * TableView plugin option class + * @class TableviewOptions + * @param {Options} options TableView plugin options + * @augments Plugin + * @returns {TableviewOptions} + * @private + */ +export default class Options { + constructor() { + return { + /** + * Set tableview holder selector. + * - **NOTE:** If not set, will append new holder element dynamically right after chart element. + * @name selector + * @memberof plugin-tableview + * @type {string} + * @default undefined + * @example + * selector: "#table-holder" + */ + selector: undefined, + + /** + * Set category title text + * @name categoryTitle + * @memberof plugin-tableview + * @type {string} + * @default "Category" + * @example + * categoryTitle: "#table-holder" + */ + categoryTitle: "Category", + + /** + * Set category text format function. + * @name categoryFormat + * @memberof plugin-tableview + * @type {Function} + * @returns {string} + * @default function(v) { // will return formatted value according x Axis type }} + * @example + * categoryFormat: "#table-holder" + */ + categoryFormat: function(v: Date|number|string): string { + let category = v; + + if (this.$$.axis.isCategorized()) { + category = this.$$.categoryName(v); + } else if (this.$$.axis.isTimeSeries()) { + category = (v as Date).toLocaleDateString(); + } + + return category as string; + }, + + /** + * Set tableview holder class name. + * @name class + * @memberof plugin-tableview + * @type {string} + * @default undefined + * @example + * class: "table-class-name" + */ + class: undefined, + + /** + * Set to apply default style(`.bb-tableview`) to tableview element. + * @name style + * @memberof plugin-tableview + * @type {boolean} + * @default true + * @example + * style: false + */ + style: true, + + /** + * Set tableview title text. + * - **NOTE:** If set [title.text](https://naver.github.io/billboard.js/release/latest/doc/Options.html#.title), will be used when this option value is empty. + * @name title + * @memberof plugin-tableview + * @type {string} + * @default undefined + * @example + * title: "Table Title Text" + */ + title: undefined, + + /** + * Update tableview from data visibility update(ex. legend toggle). + * @name updateOnToggle + * @memberof plugin-tableview + * @type {boolean} + * @default true + * @example + * legendToggleUpdate: false + */ + updateOnToggle: true + }; + } +} diff --git a/src/Plugin/tableview/const.ts b/src/Plugin/tableview/const.ts new file mode 100644 index 000000000..5463eeb5e --- /dev/null +++ b/src/Plugin/tableview/const.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2021 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +/** + * Constants values for plugin option + * @ignore + */ +const defaultStyle = { + id: "__tableview-style__", + class: "bb-tableview", + rule: `.bb-tableview { + border-collapse:collapse; + border-spacing:0; + background:#fff; + min-width:100%; + margin-top:10px; + font-family:sans-serif; + font-size:.9em; + } + .bb-tableview tr:hover { + background:#eef7ff; + } + .bb-tableview thead tr { + background:#f8f8f8; + } + .bb-tableview caption,.bb-tableview td,.bb-tableview th { + text-align: center; + border:1px solid silver; + padding:.5em; + } + .bb-tableview caption { + font-size:1.1em; + font-weight:700; + margin-bottom: -1px; + }` +}; + +// template +const tpl = { + body: `{=title} + {=thead} + {=tbody}`, + thead: `{=title}`, + tbodyHeader: `{=value}`, + tbody: `{=value}` +}; + +export { + defaultStyle, + tpl +}; diff --git a/src/Plugin/tableview/index.ts b/src/Plugin/tableview/index.ts new file mode 100644 index 000000000..871413e2f --- /dev/null +++ b/src/Plugin/tableview/index.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2021 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +import Plugin from "../Plugin"; +import Options from "./Options"; +import {defaultStyle, tpl} from "./const"; +import {loadConfig} from "../../config/config"; +import {isNumber, tplProcess} from "../../module/util"; + +/** + * 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 + * }), + * ] + * }); + * @example + * import {bb} from "billboard.js"; + * import TableView from "billboard.js/dist/billboardjs-plugin-tableview.esm"; + * + * bb.generate({ + * ... + * plugins: [ + * new TableView({ ... }) + * ] + * }) + */ +export default class TableView extends Plugin { + static version = `0.0.1`; + private config; + private element; + + constructor(options) { + super(options); + this.config = new Options(); + + return this; + } + + $beforeInit(): void { + loadConfig.call(this, this.options); + } + + $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) ? d.toLocaleString() : "") + })).join("") + }`; + }); + + const rx = /<[^>]+><\/[^>]+>/g; + const r = tplProcess(tpl.body, { + ...config, + title: config.title || $$.config.title_text || "", + thead, + tbody + }).replace(rx, ""); + + element.innerHTML = r; + } + + $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); + } + } +} diff --git a/test/plugin/bubble-compare/bubble-compare-spec.ts b/test/plugin/bubble-compare/bubble-compare-spec.ts index ae77aa9cf..eff781861 100644 --- a/test/plugin/bubble-compare/bubble-compare-spec.ts +++ b/test/plugin/bubble-compare/bubble-compare-spec.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) 2017 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ /* eslint-disable */ import {expect} from "chai"; import {select as d3Select} from "d3-selection"; diff --git a/test/plugin/tableview/tableview-spec.ts b/test/plugin/tableview/tableview-spec.ts new file mode 100644 index 000000000..2053e7fec --- /dev/null +++ b/test/plugin/tableview/tableview-spec.ts @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2021 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +/* eslint-disable */ +import {expect} from "chai"; +import sinon from "sinon"; +import TableView from "../../../src/Plugin/tableview"; +import util from "../../assets/util"; +import {toArray} from "../../../src/module/util"; + +describe("PLUGIN: TABLE-VIEW", () => { + let chart; + const pluginArgs = { + selector: undefined, + title: "어쩌고 저쩌고222", + categoryTitle: undefined, + categoryFormat: undefined, + class: undefined, + style: true, + updateOnToggle: true + }; + const args = { + data: { + columns: [ + ["data1", 30, 20, 50], + ["data2", 200, 130, 90], + ["data3", 300, 200, 160] + ], + }, + plugins: [ + new TableView(pluginArgs) + ] + }; + let spy; + + beforeEach(() => { + chart = util.generate(args); + }); + + function checkDataRows(table) { + const rows = []; + + toArray(table.querySelectorAll("tbody tr")).forEach(v => { + toArray(v.querySelectorAll("td")).forEach((d, j) => { + if (!rows[j]) { + rows[j] = []; + } + + rows[j].push(+d.innerHTML); + }); + }); + + rows.forEach((v, i) => { + expect(v).to.deep.equal(chart.data.values(`data${i + 1}`)); + }); + } + + it("Table view generated correctly?", () => { + const table = document.querySelector(`table.bb-tableview`); + const rows = []; + + expect(table).to.be.ok; + expect(table.querySelector("caption").innerHTML).to.be.equal(pluginArgs.title); + + // check if values are shown correctly + checkDataRows(table); + }); + + it("should update on data toggle", ()=> { + const table = document.querySelector(`table.bb-tableview`); + const len = table.querySelectorAll("tbody td").length; + + // when + chart.hide("data3"); + + expect(table.querySelectorAll("tbody td").length).to.be.below(len); + + // check if values are shown correctly + checkDataRows(table); + }); + + it("set options", () => { + pluginArgs.updateOnToggle = false; + }); + + it("shouldn't update on data toggle", ()=> { + const table = document.querySelector(`table.bb-tableview`); + const len = table.querySelectorAll("tbody td").length; + + // when + chart.hide("data3"); + + expect(table.querySelectorAll("tbody td").length).to.be.equal(len); + + // check if values are shown correctly + checkDataRows(table); + }); + + it("set options", () => { + pluginArgs.style = false; + pluginArgs.class = "test-abcd"; + pluginArgs.categoryTitle = "MyCategory"; + }); + + it("check if used defined options are applied", () => { + const table = document.querySelector(`table.${pluginArgs.class}`); + + expect(table).to.be.ok; + expect(table.querySelector("thead th").innerHTML).to.be.equal(pluginArgs.categoryTitle); + }); + + it("set options: categoryFormat", () => { + spy = sinon.spy(v => `ab${v}`); + pluginArgs.categoryFormat = spy; + }); + + it("should apply categoryFormat function value", () => { + const table = document.querySelector(`table.${pluginArgs.class}`); + + // categoryFormat callback should be called as the data length's times + expect(spy.callCount).to.be.equal(args.data.columns.length); + + toArray(table.querySelectorAll("tbody th")).forEach((v, i) => { + expect(v.innerHTML).to.be.equal(pluginArgs.categoryFormat(i)); + }); + + }); +}); diff --git a/types/plugin/tableview/index.d.ts b/types/plugin/tableview/index.d.ts new file mode 100644 index 000000000..2d645be44 --- /dev/null +++ b/types/plugin/tableview/index.d.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2017 ~ present NAVER Corp. + * billboard.js project is licensed under the MIT license + */ +import {Plugin} from "../plugin"; +import {TabieViewOptions} from "./options"; + +export default class TabieView extends Plugin { + /** + * Generate stanford diagram + */ + constructor(options: TabieViewOptions); +} diff --git a/types/plugin/tableview/options.d.ts b/types/plugin/tableview/options.d.ts new file mode 100644 index 000000000..01c8ffd8f --- /dev/null +++ b/types/plugin/tableview/options.d.ts @@ -0,0 +1,38 @@ +export interface TabieViewOptions { + /** + * Set tableview holder selector. + * - **NOTE:** If not set, will append new holder element dynamically. + */ + selector?: string; + + /** + * Set category title text + */ + categoryTitle?: string; + + /** + * Set category text format function. + */ + categoryFormat?: (v: Date|number|string) => string; + + /** + * Set tableview holder class name. + */ + class?: string; + + /** + * Set to apply default style to tableview element. + */ + style?: boolean; + + /** + * Set tableview title text. + * - **NOTE:** If set [title.text](https://naver.github.io/billboard.js/release/latest/doc/Options.html#.title), will be used when this option value is empty. + */ + title?: string; + + /** + * Update tableview from data visibility update(ex. legend toggle). + */ + updateOnToggle?: boolean; +}