diff --git a/docz/docs/02-getting-started/01-installation/01-standalone.mdx b/docz/docs/02-getting-started/01-installation/01-standalone.mdx index 3195f50..e319e12 100644 --- a/docz/docs/02-getting-started/01-installation/01-standalone.mdx +++ b/docz/docs/02-getting-started/01-installation/01-standalone.mdx @@ -20,7 +20,7 @@ Each standalone release script is available at . `} -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/02-frameworks.md b/docz/docs/02-getting-started/01-installation/02-frameworks.md index 5a8f594..7a4fa31 100644 --- a/docz/docs/02-getting-started/01-installation/02-frameworks.md +++ b/docz/docs/02-getting-started/01-installation/02-frameworks.md @@ -49,7 +49,7 @@ import { read, writeFileXLSX } from "xlsx"; The ["Bundlers" demo](/docs/demos/bundler) includes examples for specific tools. -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/03-nodejs.md b/docz/docs/02-getting-started/01-installation/03-nodejs.md index 5d60ab7..69a3f50 100644 --- a/docz/docs/02-getting-started/01-installation/03-nodejs.md +++ b/docz/docs/02-getting-started/01-installation/03-nodejs.md @@ -39,7 +39,7 @@ yarn add https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`} -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/04-amd.md b/docz/docs/02-getting-started/01-installation/04-amd.md index 5bc9d54..83c4efd 100644 --- a/docz/docs/02-getting-started/01-installation/04-amd.md +++ b/docz/docs/02-getting-started/01-installation/04-amd.md @@ -26,7 +26,7 @@ script as `xlsx.full.min`. ::: -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/05-extendscript.md b/docz/docs/02-getting-started/01-installation/05-extendscript.md index 3ba5c49..84445e7 100644 --- a/docz/docs/02-getting-started/01-installation/05-extendscript.md +++ b/docz/docs/02-getting-started/01-installation/05-extendscript.md @@ -22,7 +22,7 @@ After downloading the script, it can be directly referenced with `#include`: #include "xlsx.extendscript.js" ``` -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/06-deno.md b/docz/docs/02-getting-started/01-installation/06-deno.md index d9f68a7..1d6ba1d 100644 --- a/docz/docs/02-getting-started/01-installation/06-deno.md +++ b/docz/docs/02-getting-started/01-installation/06-deno.md @@ -31,7 +31,7 @@ be reported to the Deno project for further diagnosis. ::: -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/07-bun.md b/docz/docs/02-getting-started/01-installation/07-bun.md index 0122d6b..53e1482 100644 --- a/docz/docs/02-getting-started/01-installation/07-bun.md +++ b/docz/docs/02-getting-started/01-installation/07-bun.md @@ -29,7 +29,7 @@ be reported to the Bun project for further diagnosis. ::: -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/01-installation/index.md b/docz/docs/02-getting-started/01-installation/index.md index ca685dd..646f79f 100644 --- a/docz/docs/02-getting-started/01-installation/index.md +++ b/docz/docs/02-getting-started/01-installation/index.md @@ -20,7 +20,7 @@ read the installation instructions for your use case: ); })} -:::info +:::tip pass [Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the [RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when diff --git a/docz/docs/02-getting-started/02-example.mdx b/docz/docs/02-getting-started/02-example.mdx index c5d4bfe..97fcc3e 100644 --- a/docz/docs/02-getting-started/02-example.mdx +++ b/docz/docs/02-getting-started/02-example.mdx @@ -18,8 +18,15 @@ The discussion focuses on the problem solving mindset. API details are covered in other parts of the documentation. The goal of this example is to generate a XLSX workbook of US President names -and birthdays. [Click here](#live-demo) to jump to the live demo. The sequence -diagram below shows the process: +and birthdates. We will download and wrangle a JSON dataset using standard +JavaScript functions. Once we have a simple list of names and birthdates, we +will use SheetJS API functions to build a workbook object and export to XLSX. + +The ["Live Demo"](#live-demo) section includes a working demo in this page! +["Run the Demo Locally"](#run-the-demo-locally) shows how to run the workflow in +iOS / Android apps, desktop apps, NodeJS scripts and other environments. + +The follow sequence diagram shows the process: ```mermaid sequenceDiagram @@ -31,17 +38,17 @@ sequenceDiagram A->>P: raw data Note over P: process data Note over P: make workbook - Note over P: setup download + Note over P: export file P->>U: download workbook ``` ## Acquire Data -### Raw Data +The raw data is available in JSON form[^1]. It has been mirrored at + -[The raw data is available in JSON form](https://theunitedstates.io/congress-legislators/executive.json). -For convenience, it has been [mirrored here](https://sheetjs.com/data/executive.json) +### Raw Data Acquiring the data is straightforward with `fetch`: @@ -50,23 +57,105 @@ const url = "https://sheetjs.com/data/executive.json"; const raw_data = await (await fetch(url)).json(); ``` -The raw data is an Array of objects. This is the data for John Adams: +
Code Explanation (click to show) + +`fetch` is a low-level API for downloading data from an endpoint. It separates +the network step from the response parsing step. + +**Network Step** + +`fetch(url)` returns a `Promise` representing the network request. The browser +will attempt to download data from the URL. If the network request succeeded, +the `Promise` will "return" with a `Response` object. + +Using modern syntax, inside an `async` function, code should `await` the fetch: + +```js +const response = await fetch(url); +``` + +**Checking Status Code** + +If the file is not available, the `fetch` will still succeed. + +The status code, stored in the `status` property of the `Response` object, is a +standard HTTP status code number. Code should check the result. + +Typically servers will return status `404` "File not Found" if the file is not +available. A successful request should have status `200` "OK". + +**Extracting Data** + +`Response#json` will try to parse the data using `JSON.parse`. Like `fetch`, the +`json` method returns a `Promise` that must be `await`-ed: + +```js +const raw_data = await response.json(); +``` + +:::note pass + +The `Response` object has other useful methods. `Response#arrayBuffer` will +return the raw data as an `ArrayBuffer`, suitable for parsing workbook files. + +::: + +**Production Use** + +Functions can test each part independently and report different errors: + +```js +async function get_data_from_endpoint(url) { + /* perform network request */ + let response; + try { + response = await fetch(url); + } catch(e) { + /* network error */ + throw new Error(`Network Error: ${e.message}`); + } + + /* check status code */ + if(response.status == 404) { + /* server 404 error -- file not found */ + throw new Error("File not found"); + } + if(response.status != 200) { + /* for most servers, a successful response will have status 200 */ + throw new Error(`Server status ${response.status}: ${response.statusText}`); + } + + /* parse JSON */ + let data; + try { + data = await response.json(); + } catch(e) { + /* parsing error */ + throw new Error(`Parsing Error: ${e.message}`); + } + + return data; +} +``` + +
+ +The raw data is an Array of objects[^2]. For this discussion, the relevant data +for John Adams is shown below: ```js { - "id": { /* (data omitted) */ }, "name": { "first": "John", // <-- first name "last": "Adams" // <-- last name }, "bio": { "birthday": "1735-10-19", // <-- birthday - "gender": "M" }, - "terms": [ - { "type": "viceprez", "start": "1789-04-21", /* (other fields omitted) */ }, - { "type": "viceprez", "start": "1793-03-04", /* (other fields omitted) */ }, - { "type": "prez", "start": "1797-03-04", /* (other fields omitted) */ } + "terms": [ // <-- array of presidential terms + { "type": "viceprez", "start": "1789-04-21", }, + { "type": "viceprez", "start": "1793-03-04", }, + { "type": "prez", "start": "1797-03-04", } // <-- presidential term ] } ``` @@ -75,15 +164,62 @@ The raw data is an Array of objects. This is the data for John Adams: The dataset includes Aaron Burr, a Vice President who was never President! -`Array#filter` creates a new array with the desired rows. A President served -at least one term with `type` set to `"prez"`. To test if a particular row has -at least one `"prez"` term, `Array#some` is another native JS function. The -complete filter would be: +The `terms` field of each object is an array of terms. A term is a Presidential +term if the `type` property is `"prez"`. We are interested in Presidents that +served at least one term. The following line creates an array of Presidents: ```js const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez")); ``` +:::caution pass + +JavaScript code can be extremely concise. The "Code Explanation" blocks explain +the code in more detail. + +::: + +
Code Explanation (click to show) + +**Verifying if a person was a US President** + +`Array#some` takes a function and calls it on each element of an array in order. +If the function ever returns `true`, `Array#some` returns `true`. If each call +returns `false`, `Array#some` returns `false`. + +The following function tests if a term is presidential: + +```js +const term_is_presidential = term => term.type == "prez"; +``` + +To test if a person was a President, that function should be tested against +every term in the `terms` array: + +```js +const person_was_president = person => person.terms.some(term => term.type == "prez"); +``` + +**Creating a list of US Presidents** + +`Array#filter` takes a function and returns an array. The function is called on +each element in order. If the function returns `true`, the element is added to +the final array. If the function returns false, the element is not added. + +Using the previous function, this line filters the dataset for Presidents: + +```js +const prez = raw_data.filter(row => person_was_president(row)); +``` + +Placing the `person_was_president` function in-line, the final code is: + +```js +const prez = raw_data.filter(row => row.terms.some(term => term.type == "prez")); +``` + +
+ ### Sorting by First Term The dataset is sorted in chronological order by the first presidential or vice @@ -94,21 +230,136 @@ data point appears first. The goal is to sort the presidents in order of their presidential term. The first step is adding the first presidential term start date to the dataset. -`Array#find` will find the first value in an array that matches a criterion. -The following code looks at each president and creates a `"start"` property that +The following code looks at each president and creates a `start` property that represents the start of the first presidential term. ```js -prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start); +prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start); ``` -`Array#sort` will sort the array. Since the `start` properties are strings, the -recommended approach is to use `String#localeCompare` to compare strings: +
Code Explanation (click to show) + +**Finding the first presidential term** + +`Array#find` will find the first value in an array that matches a criterion. +The first presidential term can be found with the following function: + +```js +const first_prez_term = prez => prez.terms.find(term => term.type === "prez"); +``` + +:::note + +If no element in the array matches the criterion, `Array#find` does not return +a value. In this case, since `prez` was created by filtering for people that +served at least one presidential term, the code assumes a term exists. + +::: + +The start of a President's first Presidential term is therefore + +```js +const first_prez_term_start = prez => first_prez_term(prez).start; +``` + +**Adding the first start date to one row** + +The following function creates the desired `start` property: + +```js +const prez_add_start = prez => prez.start = first_prez_term_start(prez); +``` + +**Adding the first start date to each row** + +`Array#forEach` takes a function and calls it for every element in the array. +Any modifications to objects affect the objects in the original array. + +The previous function can be used directly: + +```js +prez.forEach(row => prez_add_start(row)); +``` + +Working in reverse, each partial function can be inserted in place. These lines +of code are equivalent: + +```js +/* start */ +prez.forEach(row => prez_add_start(row)); + +/* put `prez_add_start` definition into the line */ +prez.forEach(row => row.start = first_prez_term_start(row)); + +/* put `first_prez_term_start` definition into the line */ +prez.forEach(row => row.start = first_prez_term(row).start); + +/* put `first_prez_term` definition into the line */ +prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start); +``` + +
+ +At this point, each row in the `prez` array has a `start` property. Since the +`start` properties are strings, the following line sorts the array: ```js prez.sort((l,r) => l.start.localeCompare(r.start)); ``` +
Code Explanation (click to show) + +**Comparator Functions and Relative Ordering in JavaScript** + +A comparator takes two arguments and returns a number that represents the +relative ordering. `comparator(a,b)` should return a negative number if `a` +should be placed before `b`. If `b` should be placed before `a`, the comparator +should return a positive number. + +If the `start` properties were numbers, the following comparator would suffice: + +```js +const comparator_numbers = (a,b) => a - b; +``` + +For strings, JavaScript comparison operators can work: + +```js +const comparator_string_simple = (a,b) => a == b ? 0 : a < b ? -1 : 1; +``` + +However, that comparator does not handle diacritics. For example, `"z" < "é"`. +It is strongly recommended to use `String#localeCompare` to compare strings: + +```js +const comparator_string = (a,b) => a.localeCompare(b); +``` + +**Comparing two Presidents** + +The `start` properties of the Presidents should be compared: + +```js +const compare_prez = (a,b) => (a.start).localeCompare(b.start); +``` + +**Sorting the Array** + +`Array#sort` takes a comparator function and sorts the array in place. Using +the Presidential comparator: + +```js +prez.sort((l,r) => compare_prez(l,r)); +``` + +Placing the `compare_prez` function in the body: + +```js +prez.sort((l,r) => l.start.localeCompare(r.start)); +``` + +
+ ### Reshaping the Array For this example, the name will be the first name combined with the last name @@ -122,6 +373,86 @@ const rows = prez.map(row => ({ })); ``` +
Code Explanation (click to show) + +**Wrangling One Data Row** + +The key fields for John Adams are shown below: + +```js +{ + "name": { + "first": "John", // <-- first name + "last": "Adams" // <-- last name + }, + "bio": { + "birthday": "1735-10-19", // <-- birthday + } +} +``` + +If `row` is the object, then +- `row.name.first` is the first name ("John") +- `row.name.last` is the last name ("Adams") +- `row.bio.birthday` is the birthday ("1735-10-19") + +The desired object has a `name` and `birthday` field: + +```js +function get_data(row) { + var name = row.name.first + " " + row.name.last; + var birthday = row.bio.birthday; + return ({ + name: name, + birthday: birthday + }); +} +``` + +This can be shortened by adding the fields to the object directly: + +```js +function get_data(row) { + return ({ + name: row.name.first + " " + row.name.last, + birthday: row.bio.birthday + }); +} +``` + +When writing an arrow function that returns an object, parentheses are required: + +```js +// open paren required --V + const get_data = row => ({ + name: row.name.first + " " + row.name.last, + birthday: row.bio.birthday + }); +// ^-- close paren required +``` + +**Wrangling the entire dataset** + +`Array#map` calls a function on each element of an array and returns a new array +with the return values of each function. + +Using the previous method: + +```js +const rows = prez.map(row => get_data(row)); +``` + +The `get_data` function can be added in place: + +```js +const rows = prez.map(row => ({ + name: row.name.first + " " + row.name.last, + birthday: row.bio.birthday +})); +``` + +
+ The result is an array of "simple" objects with no nesting: ```js @@ -134,13 +465,13 @@ The result is an array of "simple" objects with no nesting: ## Create a Workbook -With the cleaned dataset, `XLSX.utils.json_to_sheet` generates a worksheet: +With the cleaned dataset, `XLSX.utils.json_to_sheet`[^3] generates a worksheet: ```js const worksheet = XLSX.utils.json_to_sheet(rows); ``` -`XLSX.utils.book_new` creates a new workbook and `XLSX.utils.book_append_sheet` +`XLSX.utils.book_new`[^4] creates a new workbook and `XLSX.utils.book_append_sheet`[^5] appends a worksheet to the workbook. The new worksheet will be called "Dates": ```js @@ -163,7 +494,7 @@ additional styling options like cell styling and frozen rows. By default, `json_to_sheet` creates a worksheet with a header row. In this case, the headers come from the JS object keys: "name" and "birthday". -The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa` can write +The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa`[^6] can write text values to the existing worksheet starting at cell `A1`: ```js @@ -175,7 +506,7 @@ XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
Changing Column Widths (click to show) Some of the names are longer than the default column width. Column widths are -set by setting the `"!cols"` worksheet property. +set by setting the `"!cols"` worksheet property.[^7] The following line sets the width of column A to approximately 10 characters: @@ -198,9 +529,9 @@ After cleanup, the generated workbook looks like the screenshot below: ## Export a File -`XLSX.writeFile` creates a spreadsheet file and tries to write it to the system. -In the browser, it will try to prompt the user to download the file. In NodeJS, -it will write to the local directory. +`XLSX.writeFile`[^8] creates a spreadsheet file and tries to write it to the +system. In the browser, it will try to prompt the user to download the file. In +NodeJS, it will write to the local directory. ```js XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true }); @@ -221,7 +552,7 @@ function Presidents() { return (