├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── babel.config.js ├── example.png ├── examples ├── custom-font │ ├── README.md │ ├── custom-font.png │ ├── fonts │ │ ├── Spoqa-Han-Sans-Bold.ttf │ │ └── Spoqa-Han-Sans-Regular.ttf │ └── index.js ├── lines │ ├── README.md │ ├── index.js │ └── lines.png ├── multiple-tables │ ├── README.md │ ├── index.js │ └── multiple-tables.png ├── padding │ ├── README.md │ ├── index.js │ └── padding.png ├── single-table │ ├── README.md │ ├── index.js │ └── single-table.png └── styling-title │ ├── README.md │ ├── index.js │ └── styling-title.png ├── index.d.ts ├── package-lock.json ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": true, 4 | "singleQuote": true, 5 | "printWidth": 200 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.exclude": { 4 | "**/node_modules": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # table-renderer 2 | 3 | > convert table or spreadsheet data into an image 4 | 5 | ## Background 6 | 7 | One day, I had to build a slack slash command which reports marketing reports to our company slack channel. I wanted to format the command results look like table, but I could not find a simple way to do that. I decided to build a table-like view using markdown text, and struggled to do that. However, the layout was broken with small windows or with CJK charaters. So I decided to build the report as an image. 8 | 9 | ![table-renderer](example.png) 10 | 11 | I hope this module will help someone who wants to convert a simple spreadsheet data into an image, 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install table-renderer canvas 17 | ``` 18 | 19 | [node-canvas](https://github.com/Automattic/node-canvas) module is peer-dependency. You have to install it, too. 20 | 21 | ## Examples 22 | 23 | - [Single Table](./examples/single-table) 24 | - [Multiple Tables](./examples/multiple-tables) 25 | - [Styling Title](./examples/styling-title) 26 | - [Horizontal and Vertical Lines](./examples/lines) 27 | - [Horizontal and Vertical Padding](./examples/padding) 28 | - [Custom Font](./examples/custom-font) 29 | 30 | ## Usage 31 | 32 | ```javascript 33 | import path from 'path'; 34 | import TableRenderer, { saveImage } from 'table-renderer'; 35 | 36 | const renderTable = TableRenderer().render; 37 | 38 | const canvas = renderTable({ 39 | title: 'Marketing Summary', 40 | columns: [ 41 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 42 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 43 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 44 | ], 45 | dataSource: [ 46 | '-', 47 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 48 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 49 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 50 | '-', 51 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 52 | ], 53 | }); 54 | 55 | saveImage(canvas, path.join(__dirname, 'example.png')); 56 | ``` 57 | 58 | ![single table](example.png) 59 | 60 | ## API 61 | 62 | - [TableRenderer()](#tablerenderer) 63 | - [TableRenderer#render()](#tablerendererrender) 64 | - [saveImage()](#saveimage) 65 | 66 | ### TableRenderer 67 | 68 | ```javascript 69 | TableRenderer(options = {}) => ({ render: function }); 70 | ``` 71 | 72 | #### options 73 | 74 | - `cellWidth` {number} default width for a table cell. default = 100 75 | - `cellHeight` {number} default height for a table cell. default = 40 76 | - `offsetLeft` {number} default text offset from left border of table cell. default = 8 77 | - `offsetTop` {number} default text offset from top border of table cell. default = 26 78 | - `spacing` {number} spacing between tables. default = 20 79 | - `titleSpacing` {number} spacing between title and a table. default = 10 80 | - `fontFamily` {string} default = 'sans-serif' 81 | - `paddingVertical` {number} vertical padding of a page. default = 0 82 | - `paddingHorizontal` {number} horizontal padding of a page. default = 0 83 | - `backgroundColor` {string} page background color. default = '#ffffff' 84 | 85 | ### TableRenderer#render 86 | 87 | ```javascript 88 | render((tables: Object | Array)) => Canvas; 89 | ``` 90 | 91 | tables parameter is either Object or Array. Single table is comprised of title, columns, and dataSource, where title is optional. Parameters of render function resembles ant-design Table paramters. 92 | 93 | The function returns Canvas object, which is an instance of [node-canvas](https://github.com/Automattic/node-canvas). So, you can add canvas operations to it. 94 | 95 | ```javascript 96 | render({ 97 | title: 'Marketing Summary', 98 | columns: [...], 99 | dataSource: [...] 100 | }); 101 | ``` 102 | 103 | ### saveImage 104 | 105 | ```javascript 106 | saveImage((canvas: Canvas), (filepath: String)) => Promise; 107 | ``` 108 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: '10.0' } }]], 3 | plugins: [], 4 | }; 5 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/example.png -------------------------------------------------------------------------------- /examples/custom-font/README.md: -------------------------------------------------------------------------------- 1 | # Custom Font 2 | 3 | This module uses 'sans-serif' font by default. However, in some environments where fonts are not installed, you have to register fonts by yourself. In addition, CJK users may want to change the default font. 4 | You can register fonts using `registerFont` from node-canvas which is a peer dependency of table-renderer. 5 | 6 | ![custom font](./custom-font.png) 7 | 8 | ```javascript 9 | import path from 'path'; 10 | import TableRenderer, { saveImage } from '../../src'; 11 | import { registerFont } from 'canvas'; 12 | 13 | registerFont(path.join(__dirname, 'fonts/Spoqa-Han-Sans-Regular.ttf'), { family: 'spoqa', weight: 'normal' }); 14 | registerFont(path.join(__dirname, 'fonts/Spoqa-Han-Sans-Bold.ttf'), { family: 'spoqa', weight: 'bold' }); 15 | 16 | const renderTable = TableRenderer({ fontFamily: 'spoqa' }).render; 17 | 18 | const canvas = renderTable({ 19 | title: 'Marketing Summary', 20 | columns: [ 21 | { width: 200, title: '캠페인', dataIndex: 'campaign' }, 22 | { width: 100, title: '설치수', dataIndex: 'install', align: 'right' }, 23 | { width: 100, title: '비용', dataIndex: 'cost', align: 'right' }, 24 | ], 25 | dataSource: [ 26 | '-', 27 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 28 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 29 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 30 | '-', 31 | { campaign: '합계', install: '146', cost: '$ 1,690' }, 32 | ], 33 | }); 34 | 35 | saveImage(canvas, path.join(__dirname, 'custom-font.png')); 36 | ``` 37 | -------------------------------------------------------------------------------- /examples/custom-font/custom-font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/custom-font/custom-font.png -------------------------------------------------------------------------------- /examples/custom-font/fonts/Spoqa-Han-Sans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/custom-font/fonts/Spoqa-Han-Sans-Bold.ttf -------------------------------------------------------------------------------- /examples/custom-font/fonts/Spoqa-Han-Sans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/custom-font/fonts/Spoqa-Han-Sans-Regular.ttf -------------------------------------------------------------------------------- /examples/custom-font/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | import { registerFont } from 'canvas'; 4 | 5 | registerFont(path.join(__dirname, 'fonts/Spoqa-Han-Sans-Regular.ttf'), { family: 'spoqa', weight: 'normal' }); 6 | registerFont(path.join(__dirname, 'fonts/Spoqa-Han-Sans-Bold.ttf'), { family: 'spoqa', weight: 'bold' }); 7 | 8 | const renderTable = TableRenderer({ fontFamily: 'spoqa' }).render; 9 | 10 | const canvas = renderTable({ 11 | title: 'Marketing Summary', 12 | columns: [ 13 | { width: 200, title: '캠페인', dataIndex: 'campaign' }, 14 | { width: 100, title: '설치수', dataIndex: 'install', align: 'right' }, 15 | { width: 100, title: '비용', dataIndex: 'cost', align: 'right' }, 16 | ], 17 | dataSource: [ 18 | '-', 19 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 20 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 21 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 22 | '-', 23 | { campaign: '합계', install: '146', cost: '$ 1,690' }, 24 | ], 25 | }); 26 | 27 | saveImage(canvas, path.join(__dirname, 'custom-font.png')); 28 | -------------------------------------------------------------------------------- /examples/lines/README.md: -------------------------------------------------------------------------------- 1 | # Adding Vertical and Horizontal Lines 2 | 3 | To add a vertical line add '|' to the columns. To add a horizontal line add '-' to the dataSource. 4 | 5 | ![vertical and horizontal lines](./lines.png) 6 | 7 | ```javascript 8 | import path from 'path'; 9 | import TableRenderer, { saveImage } from '../../src'; 10 | 11 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 12 | 13 | const canvas = renderTable({ 14 | title: 'Marketing Summary', 15 | columns: [ 16 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 17 | '|', 18 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 19 | '|', 20 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 21 | ], 22 | dataSource: [ 23 | '-', 24 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 25 | '-', 26 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 27 | '-', 28 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 29 | '-', 30 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 31 | ], 32 | }); 33 | 34 | saveImage(canvas, path.join(__dirname, 'lines.png')); 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/lines/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | 4 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 5 | 6 | const canvas = renderTable({ 7 | title: 'Marketing Summary', 8 | columns: [ 9 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 10 | '|', 11 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 12 | '|', 13 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 14 | ], 15 | dataSource: [ 16 | '-', 17 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 18 | '-', 19 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 20 | '-', 21 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 22 | '-', 23 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 24 | ], 25 | }); 26 | 27 | saveImage(canvas, path.join(__dirname, 'lines.png')); 28 | -------------------------------------------------------------------------------- /examples/lines/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/lines/lines.png -------------------------------------------------------------------------------- /examples/multiple-tables/README.md: -------------------------------------------------------------------------------- 1 | # Multiple Tables 2 | 3 | To render multiple tables, pass an array of table objects to the renderTable function. 4 | 5 | ![multiple tables](./multiple-tables.png) 6 | 7 | ```javascript 8 | import path from 'path'; 9 | import TableRenderer, { saveImage } from '../../src'; 10 | 11 | const canvas = renderTable([ 12 | { 13 | title: 'Revenue', 14 | columns: [ 15 | { width: 140, title: 'Country', dataIndex: 'country' }, 16 | { width: 100, title: 'Amount', dataIndex: 'amount', align: 'right' }, 17 | { width: 100, title: 'Revenue', dataIndex: 'revenue', align: 'right' }, 18 | ], 19 | dataSource: [ 20 | '-', 21 | { country: 'United States', amount: '12', revenue: '$ 400' }, 22 | { country: 'France', amount: '3', revenue: '$ 60' }, 23 | { country: 'Japan', amount: '131', revenue: '$ 1,230' }, 24 | '-', 25 | { country: 'Total', amount: '146', revenue: '$ 1,690' }, 26 | ], 27 | }, 28 | { 29 | title: 'Marketing Cost', 30 | columns: [ 31 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 32 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 33 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 34 | ], 35 | dataSource: [ 36 | '-', 37 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 38 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 39 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 40 | '-', 41 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 42 | ], 43 | }, 44 | ]); 45 | 46 | saveImage(canvas, path.join(__dirname, 'multiple-tables.png')); 47 | ``` 48 | -------------------------------------------------------------------------------- /examples/multiple-tables/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | 4 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 5 | 6 | const canvas = renderTable([ 7 | { 8 | title: 'Revenue', 9 | columns: [ 10 | { width: 140, title: 'Country', dataIndex: 'country' }, 11 | { width: 100, title: 'Amount', dataIndex: 'amount', align: 'right' }, 12 | { width: 100, title: 'Revenue', dataIndex: 'revenue', align: 'right' }, 13 | ], 14 | dataSource: [ 15 | '-', 16 | { country: 'United States', amount: '12', revenue: '$ 400' }, 17 | { country: 'France', amount: '3', revenue: '$ 60' }, 18 | { country: 'Japan', amount: '131', revenue: '$ 1,230' }, 19 | '-', 20 | { country: 'Total', amount: '146', revenue: '$ 1,690' }, 21 | ], 22 | }, 23 | { 24 | title: 'Marketing Cost', 25 | columns: [ 26 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 27 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 28 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 29 | ], 30 | dataSource: [ 31 | '-', 32 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 33 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 34 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 35 | '-', 36 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 37 | ], 38 | }, 39 | ]); 40 | 41 | saveImage(canvas, path.join(__dirname, 'multiple-tables.png')); 42 | -------------------------------------------------------------------------------- /examples/multiple-tables/multiple-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/multiple-tables/multiple-tables.png -------------------------------------------------------------------------------- /examples/padding/README.md: -------------------------------------------------------------------------------- 1 | # Adding Vertical and Horizontal Padding 2 | 3 | To add a vertical padding, pass verticalPadding to TableRenderer options. To add a horizontal padding, pass horizontalPadding to TableRenderer options. 4 | 5 | ![vertical and horizontal padding](./padding.png) 6 | 7 | ```javascript 8 | import path from 'path'; 9 | import TableRenderer, { saveImage } from '../../src'; 10 | 11 | const renderTable = TableRenderer({ paddingHorizontal: 20, paddingVertical: 20, backgroundColor: '#efefef' }).render; 12 | 13 | const canvas = renderTable({ 14 | title: 'Marketing Summary', 15 | columns: [ 16 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 17 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 18 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 19 | ], 20 | dataSource: [ 21 | '-', 22 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 23 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 24 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 25 | '-', 26 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 27 | ], 28 | }); 29 | 30 | saveImage(canvas, path.join(__dirname, 'padding.png')); 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/padding/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | 4 | const renderTable = TableRenderer({ paddingHorizontal: 20, paddingVertical: 20, backgroundColor: '#efefef' }).render; 5 | 6 | const canvas = renderTable({ 7 | title: 'Marketing Summary', 8 | columns: [ 9 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 10 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 11 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 12 | ], 13 | dataSource: [ 14 | '-', 15 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 16 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 17 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 18 | '-', 19 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 20 | ], 21 | }); 22 | 23 | saveImage(canvas, path.join(__dirname, 'padding.png')); 24 | -------------------------------------------------------------------------------- /examples/padding/padding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/padding/padding.png -------------------------------------------------------------------------------- /examples/single-table/README.md: -------------------------------------------------------------------------------- 1 | # Single Table 2 | 3 | To render a single table, just pass a table object to the renderTable function. 4 | 5 | ![single table](./single-table.png) 6 | 7 | ```javascript 8 | import path from 'path'; 9 | import TableRenderer, { saveImage } from '../../src'; 10 | 11 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 12 | 13 | const canvas = renderTable({ 14 | title: 'Marketing Summary', 15 | columns: [ 16 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 17 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 18 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 19 | ], 20 | dataSource: [ 21 | '-', 22 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 23 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 24 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 25 | '-', 26 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 27 | ], 28 | }); 29 | 30 | saveImage(canvas, path.join(__dirname, 'single-table.png')); 31 | ``` 32 | -------------------------------------------------------------------------------- /examples/single-table/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | 4 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 5 | 6 | const canvas = renderTable({ 7 | title: 'Marketing Summary', 8 | columns: [ 9 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 10 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 11 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 12 | ], 13 | dataSource: [ 14 | '-', 15 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 16 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 17 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 18 | '-', 19 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 20 | ], 21 | }); 22 | 23 | saveImage(canvas, path.join(__dirname, 'single-table.png')); 24 | -------------------------------------------------------------------------------- /examples/single-table/single-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/single-table/single-table.png -------------------------------------------------------------------------------- /examples/styling-title/README.md: -------------------------------------------------------------------------------- 1 | # Styling Title 2 | 3 | You can customize title using titleStyle object. 4 | 5 | ![styling title](./styling-title.png) 6 | 7 | ```javascript 8 | import path from 'path'; 9 | import TableRenderer, { saveImage } from '../../src'; 10 | 11 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 12 | 13 | const canvas = renderTable({ 14 | title: 'Marketing Summary', 15 | titleStyle: { 16 | font: 'normal 24px sans-serif', 17 | fillStyle: '#ff0000', 18 | }, 19 | columns: [ 20 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 21 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 22 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 23 | ], 24 | dataSource: [ 25 | '-', 26 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 27 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 28 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 29 | '-', 30 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 31 | ], 32 | }); 33 | 34 | saveImage(canvas, path.join(__dirname, 'styling-title.png')); 35 | ``` 36 | -------------------------------------------------------------------------------- /examples/styling-title/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import TableRenderer, { saveImage } from '../../src'; 3 | 4 | const renderTable = TableRenderer({ titleSpacing: 10 }).render; 5 | 6 | const canvas = renderTable({ 7 | title: 'Marketing Summary', 8 | titleStyle: { 9 | font: 'normal 24px sans-serif', 10 | fillStyle: '#ff0000', 11 | }, 12 | columns: [ 13 | { width: 200, title: 'Campaign', dataIndex: 'campaign' }, 14 | { width: 100, title: 'Install', dataIndex: 'install', align: 'right' }, 15 | { width: 100, title: 'Cost', dataIndex: 'cost', align: 'right' }, 16 | ], 17 | dataSource: [ 18 | '-', 19 | { campaign: 'Google CPC', install: '12', cost: '$ 400' }, 20 | { campaign: 'Facebook CPC', install: '3', cost: '$ 60' }, 21 | { campaign: 'Youtube Video', install: '131', cost: '$ 1,230' }, 22 | '-', 23 | { campaign: 'Total', install: '146', cost: '$ 1,690' }, 24 | ], 25 | }); 26 | 27 | saveImage(canvas, path.join(__dirname, 'single-table.png')); 28 | -------------------------------------------------------------------------------- /examples/styling-title/styling-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idw111/table-renderer/b2874e608df2e3270546f2514ea1dc882915f1a9/examples/styling-title/styling-title.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from 'canvas'; 2 | 3 | export interface TableRendererOptions { 4 | cellWidth ?: number; 5 | cellHeight ?: number; 6 | offsetLeft ?: number; 7 | offsetTop ?: number; 8 | spacing ?: number; 9 | titleSpacing ?: number; 10 | fontFamily ?: string; 11 | paddingVertical ?: number; 12 | paddingHorizontal?: number; 13 | backgroundColor ?: string | CanvasGradient | CanvasPattern; 14 | } 15 | 16 | export type Column = '|' | { 17 | width ?: number, 18 | title : string, 19 | dataIndex: string, 20 | align ?: CanvasTextAlign, 21 | }; 22 | 23 | export type Row = '-' | { 24 | [key: string]: string, 25 | }; 26 | 27 | export interface TitleStyle { 28 | font ?: string; 29 | fillStyle?: string | CanvasGradient | CanvasPattern; 30 | textAlign?: CanvasTextAlign; 31 | offsetTop?: number; 32 | } 33 | 34 | export interface Table { 35 | title ?: string; 36 | titleStyle?: TitleStyle; 37 | columns : Column[]; 38 | dataSource : Row[]; 39 | } 40 | 41 | export type RenderFunction = (tables: Table | Table[]) => Canvas; 42 | 43 | declare function TableRenderer (options?: TableRendererOptions): { render: RenderFunction }; 44 | declare function saveImage (canvas: Canvas, filepath: string): Promise; 45 | 46 | export default TableRenderer; 47 | export { saveImage }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "table-renderer", 3 | "version": "0.1.29", 4 | "description": "convert table or spreadsheet data into an image", 5 | "main": "./lib/index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "/lib", 9 | "index.d.ts" 10 | ], 11 | "scripts": { 12 | "clean": "rimraf lib", 13 | "build": "babel src --out-dir lib", 14 | "prepublish": "npm run clean && npm run build", 15 | "script": "node -r @babel/register -r @babel/polyfill", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/idw111/table-renderer" 21 | }, 22 | "keywords": [ 23 | "table", 24 | "image", 25 | "convert", 26 | "spread", 27 | "spreadsheet" 28 | ], 29 | "author": "Dongwon Lim", 30 | "license": "MIT", 31 | "peerDependencies": { 32 | "canvas": "^2.9.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/cli": "^7.10.4", 36 | "@babel/core": "^7.10.4", 37 | "@babel/polyfill": "^7.10.4", 38 | "@babel/preset-env": "^7.10.4", 39 | "@babel/register": "^7.10.4", 40 | "canvas": "^2.9.0", 41 | "rimraf": "^3.0.2", 42 | "webpack": "^4.43.0", 43 | "webpack-cli": "^4.9.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { createCanvas } from 'canvas'; 4 | 5 | const defaultOptions = { 6 | cellWidth: 100, 7 | cellHeight: 40, 8 | offsetLeft: 8, 9 | offsetTop: 26, 10 | spacing: 20, 11 | titleSpacing: 10, 12 | fontFamily: 'sans-serif', 13 | paddingVertical: 0, 14 | paddingHorizontal: 0, 15 | backgroundColor: '#ffffff', 16 | }; 17 | 18 | const TableRenderer = (options = {}) => { 19 | const { cellWidth, cellHeight, offsetLeft, offsetTop, spacing, titleSpacing, fontFamily, paddingHorizontal, paddingVertical, backgroundColor } = Object.assign(defaultOptions, options); 20 | 21 | const getTableWidth = (columns) => { 22 | return columns?.reduce((sum, col) => sum + (col === '|' ? 1 : col.width ?? cellWidth), 0) ?? cellWidth; 23 | }; 24 | 25 | const getTableHeight = (title, columns, dataSource) => { 26 | const titleHeight = title ? cellHeight + titleSpacing : 0; 27 | const headerHeight = !columns?.length || columns.every((col) => !col.title) ? 0 : cellHeight; 28 | const bodyHeight = dataSource?.reduce((height, row) => height + (row === '-' ? 1 : cellHeight), 0) ?? 0; 29 | return titleHeight + headerHeight + bodyHeight; 30 | }; 31 | 32 | const renderBackground = (ctx, width, height) => { 33 | ctx.fillStyle = backgroundColor; 34 | ctx.strokeStyle = backgroundColor; 35 | ctx.fillRect(-10, -10, width + 10, height + 10); 36 | }; 37 | 38 | const renderHorizontalLines = 39 | (ctx) => 40 | (dataSource, { x, y, width }) => { 41 | ctx.strokeStyle = '#000000'; 42 | dataSource?.forEach((row, i) => { 43 | if (row !== '-') return; 44 | ctx.moveTo(paddingHorizontal, y[i]); 45 | ctx.lineTo(paddingHorizontal + width, y[i]); 46 | ctx.stroke(); 47 | }); 48 | }; 49 | 50 | const renderVerticalLines = 51 | (ctx) => 52 | (title, columns, { x, y, height }) => { 53 | ctx.strokeStyle = '#000000'; 54 | const titleHeight = title ? cellHeight + titleSpacing : 0; 55 | const headerHeight = !columns?.length || columns.every((col) => !col.title) ? 0 : cellHeight; 56 | columns?.forEach((col, i) => { 57 | if (col !== '|') return; 58 | ctx.moveTo(x[i], y[0] - headerHeight); 59 | ctx.lineTo(x[i], y[0] - headerHeight + height - titleHeight); 60 | ctx.stroke(); 61 | }); 62 | }; 63 | 64 | const renderTitle = 65 | (ctx) => 66 | (title, titleStyle = {}, { top, x }) => { 67 | if (!title) return; 68 | ctx.font = titleStyle.font ?? `bold 24px ${fontFamily}`; 69 | ctx.fillStyle = titleStyle?.fillStyle ?? '#000000'; 70 | ctx.textAlign = titleStyle?.textAlign ?? 'left'; 71 | ctx.fillText(title, paddingHorizontal + offsetLeft, top + offsetTop + (titleStyle?.offsetTop ?? 0)); 72 | }; 73 | 74 | const renderHeader = 75 | (ctx) => 76 | (columns, { x, y }) => { 77 | ctx.font = `normal 16px ${fontFamily}`; 78 | ctx.fillStyle = '#333333'; 79 | columns?.forEach((col, i) => { 80 | if (typeof col != 'object' || !col.title) return; 81 | const { title, width = cellWidth, align = 'left' } = col; 82 | ctx.textAlign = align; 83 | ctx.fillText(title, x[i] + (align === 'right' ? width - offsetLeft : offsetLeft), y[0] - cellHeight + offsetTop); 84 | }); 85 | }; 86 | 87 | const renderRows = 88 | (ctx) => 89 | (columns, dataSource, { x, y }) => { 90 | dataSource?.forEach((row, i) => { 91 | if (row === '-') return; 92 | columns?.forEach(({ width = cellWidth, dataIndex, align = 'left', prefix = '', suffix = '' }, j) => { 93 | if (!row[dataIndex]) return; 94 | const content = prefix + row[dataIndex] + suffix; 95 | ctx.textAlign = align; 96 | ctx.fillText(content, x[j] + (align === 'right' ? width - offsetLeft : offsetLeft), y[i] + offsetTop, width - 2 * offsetLeft); 97 | }); 98 | }); 99 | }; 100 | 101 | const renderTable = ({ title, titleStyle = {}, columns, dataSource }, { ctx, width, height, top = paddingVertical }) => { 102 | const info = { 103 | width, 104 | height, 105 | top, 106 | x: new Array(columns?.length ?? 0).fill().map((_, i) => paddingHorizontal + columns?.reduce((x, col, j) => x + (j >= i ? 0 : col === '|' ? 1 : col.width ?? cellWidth), 0)), 107 | y: new Array(dataSource?.length ?? 0).fill().map((_, i) => { 108 | const titleHeight = title ? cellHeight + titleSpacing : 0; 109 | const headerHeight = !columns?.length || columns.every((col) => !col.title) ? 0 : cellHeight; 110 | return top + titleHeight + headerHeight + dataSource?.reduce((y, row, j) => y + (j >= i ? 0 : row === '-' ? 1 : cellHeight), 0); 111 | }), 112 | }; 113 | renderHorizontalLines(ctx)(dataSource, info); 114 | renderVerticalLines(ctx)(title, columns, info); 115 | renderTitle(ctx)(title, titleStyle, info); 116 | renderHeader(ctx)(columns, info); 117 | renderRows(ctx)(columns, dataSource, info); 118 | }; 119 | 120 | const renderTables = (tables, { ctx, width, height }) => { 121 | tables.forEach((table, i) => { 122 | const { title, columns, dataSource } = table; 123 | const top = tables.reduce((top, { title, columns, dataSource }, j) => top + (j >= i ? 0 : getTableHeight(title, columns, dataSource)), 0) + i * spacing + paddingVertical; 124 | renderTable(table, { ctx, width: getTableWidth(columns), height: getTableHeight(title, columns, dataSource), top }); 125 | }); 126 | }; 127 | 128 | const render = (tables) => { 129 | if (!Array.isArray(tables)) { 130 | const { title, columns, dataSource } = tables; 131 | const width = getTableWidth(columns); 132 | const height = getTableHeight(title, columns, dataSource); 133 | const canvas = createCanvas(width + 2 * paddingHorizontal, height + 2 * paddingVertical); 134 | const ctx = canvas.getContext('2d'); 135 | renderBackground(ctx, width + 2 * paddingHorizontal, height + 2 * paddingVertical); 136 | renderTable(tables, { ctx, width, height }); 137 | return canvas; 138 | } else { 139 | const width = tables.reduce((maxWidth, { columns }) => Math.max(getTableWidth(columns), maxWidth), 0) + 2 * paddingHorizontal; 140 | const height = tables.reduce((height, { title, columns, dataSource }) => height + getTableHeight(title, columns, dataSource), 0) + (tables.length - 1) * spacing + 2 * paddingVertical; 141 | const canvas = createCanvas(width, height); 142 | const ctx = canvas.getContext('2d'); 143 | renderBackground(ctx, width, height); 144 | renderTables(tables, { ctx, width, height }); 145 | return canvas; 146 | } 147 | }; 148 | 149 | return { render }; 150 | }; 151 | 152 | export default TableRenderer; 153 | 154 | export const saveImage = async (canvas, filepath) => { 155 | await new Promise((resolve, reject) => { 156 | const ws = fs.createWriteStream(filepath); 157 | ws.on('finish', resolve); 158 | ws.on('error', reject); 159 | canvas.createPNGStream().pipe(ws); 160 | }); 161 | }; 162 | --------------------------------------------------------------------------------