{"id":22326035,"url":"https://github.com/solarnetwork/solarnetwork-example-ts-chart-billboard","last_synced_at":"2025-03-26T06:11:41.030Z","repository":{"id":239225544,"uuid":"798926153","full_name":"SolarNetwork/solarnetwork-example-ts-chart-billboard","owner":"SolarNetwork","description":"Example web app using the solarnetwork-api-core package to query SolarNetwork and render a chart, using TypeScript with billboard.js.","archived":false,"fork":false,"pushed_at":"2024-05-10T19:56:03.000Z","size":157,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-31T07:32:05.488Z","etag":null,"topics":["example","solarnetwork","typescript","webapp"],"latest_commit_sha":null,"homepage":"https://go.solarnetwork.net/dev/example/typescript-chart-billboard/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SolarNetwork.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-10T19:18:27.000Z","updated_at":"2024-05-10T19:53:43.000Z","dependencies_parsed_at":"2024-05-10T20:47:09.391Z","dependency_job_id":null,"html_url":"https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard","commit_stats":null,"previous_names":["solarnetwork/solarnetwork-example-ts-chart-billboard"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SolarNetwork%2Fsolarnetwork-example-ts-chart-billboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SolarNetwork%2Fsolarnetwork-example-ts-chart-billboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SolarNetwork%2Fsolarnetwork-example-ts-chart-billboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SolarNetwork%2Fsolarnetwork-example-ts-chart-billboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SolarNetwork","download_url":"https://codeload.github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245598320,"owners_count":20641884,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["example","solarnetwork","typescript","webapp"],"created_at":"2024-12-04T02:15:09.394Z","updated_at":"2025-03-26T06:11:41.010Z","avatar_url":"https://github.com/SolarNetwork.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SolarNetwork Example: TypeScript Chart with billboard.js\n\nThis project is a little web app to show the basics of using the SolarNetwork API\nin a TypeScript project to render an accumulating meter reading datum stream in a chart with the [billboard.js][billboard] project to generate a chart out of a SolarNetwork datum stream.\n\nYou can see the example in action here:\n\n\u003chttps://go.solarnetwork.net/dev/example/typescript-chart-billboard/\u003e\n\n\u003cimg alt=\"Screenshot of the TypeScript Chart with billboard.js app\" src=\"docs/chart-billboard-screenshot@2x.png\" width=\"1155\"\u003e\n\n# Key aspects\n\nThere are a few key aspects of this example worth pointing out.\n\n## SolarNetwork API\n\nThe [solarnetwork-api-core][sn-api-core] package is [included in the project][sn-api-core-dep], which provides many helpful utilities in both TypeScript and JavaScript for working with the SolarNetwork API.\n\n```json\n{\n\t\"dependencies\": {\n\t\t\"solarnetwork-api-core\": \"^2.0.1\"\n\t}\n}\n```\n\n## Token authentication with Fetch API\n\nThe example demonstrates using [SolarNetwork token authentication][sn-api-auth] with the browser [Fetch API][fetch].\n\nFirst the demo [imports the AuthorizationV2Builder][import-sn-auth] class and [creates a reusable instance][auth-instance] in an `auth` variable:\n\n```ts\nimport { AuthorizationV2Builder } from \"solarnetwork-api-core/lib/net\";\n\nconst auth = new AuthorizationV2Builder();\n```\n\nA `change` form event handler listens for changes to the form's token and secret fields, and [saves the credentials][save-creds] for future API calls:\n\n```ts\n// save credentials\nauth.tokenId = settingsForm.snToken.value;\nauth.saveSigningKey(settingsForm.snTokenSecret.value);\n```\n\nWhen it comes time to make a SolarNetwork API request, the app [generates a `Headers` object][auth-headers] for the API URL that includes the necessary `Authorization`, `X-SN-Date`, and `Accept` header values and initiates the `fetch()` call:\n\n```ts\nfunction authorizeUrl(url: string): Headers {\n\tconst authHeader = auth.reset().snDate(true).url(url).buildWithSavedKey();\n\treturn new Headers({\n\t\tAuthorization: authHeader,\n\t\t\"X-SN-Date\": auth.requestDateHeaderValue!,\n\t\tAccept: \"application/json\",\n\t});\n}\n\nconst headers = authorizeUrl(findSourcesUrl);\nconst res = await fetch(findSourcesUrl, {\n\tmethod: \"GET\",\n\theaders: headers,\n});\n```\n\n## URL helper\n\nThe `SolarQueryApi` class is also imported, which provides methods to help generate SolarNetwork API URLs:\n\n```ts\nimport { SolarQueryApi } from \"solarnetwork-api-core/lib/net\";\n\nconst urlHelper = new SolarQueryApi();\n```\n\nFor example, to discover the available sources to populate the **Source ID** menu, the application [creates a `DatumFilter` object][create-datum-filter] and populates the **Node ID**, **Start Date**, and **End Date** values from the form, and then [uses the `findSourcesUrl(filter)` method][find-sources-url] to generate the API URL:\n\n```ts\nimport { DatumFilter } from \"solarnetwork-api-core/lib/domain\";\n\n// create a filter object with the form's node ID, start date, and end date values\nconst filter = new DatumFilter();\nfilter.nodeId = Number(nodeId);\nif (startDate) {\n\tstartDate.setHours(0, 0, 0, 0);\n\tfilter.localStartDate = startDate;\n}\nif (endDate) {\n\tendDate.setHours(0, 0, 0, 0);\n\tfilter.localEndDate = endDate;\n}\n\n// use the findSourcesUrl() method to generate the API URL\nconst findSourcesUrl = urlHelper.findSourcesUrl(filter);\n```\n\n## Discover the available sources for the given node\n\nThe [the previous](#url-helper) section actually showed how the app takes the **Node ID**, **Start Date**, and **End Date** values from the form and uses the `SolarQueryApi` helper's `findSourcesUrl(filter)` method to [query SolarNetwork for the source IDs available matching that criteria][discover-sources]. The API used here is the [/nodes/sources][reportable-node-sources] method, that [returns a list of node/source objects][reportable-node-sources-res] like this:\n\n```json\n{\n\t\"success\": true,\n\t\"data\": [\n\t\t{ \"nodeId\": 1, \"sourceId\": \"Main\" },\n\t\t{ \"nodeId\": 1, \"sourceId\": \"Main1\" }\n\t]\n}\n```\n\nThus the results are processed and all the available `sourceId` values are populated in the **Source ID** form menu:\n\n```ts\n// make API request using Fetch API\nconst res = await fetch(findSourcesUrl, {\n\tmethod: \"GET\",\n\theaders: headers,\n});\n\n// wait for response\nconst json = await res.json();\nif (json \u0026\u0026 Array.isArray(json.data)) {\n\t// clear out and re-populate the source IDs menu\n\twhile (settingsForm.snSourceId.length) {\n\t\tsettingsForm.snSourceId.remove(0);\n\t}\n\tif (json.data.length) {\n\t\tsettingsForm.snSourceId.add(new Option(\"Choose...\", \"\"));\n\t\t// for each response object, add a menu option for that source ID\n\t\tfor (const src of json.data) {\n\t\t\tconst opt = new Option(src.sourceId, src.sourceId);\n\t\t\tsettingsForm.snSourceId.add(opt);\n\t\t}\n\t}\n}\n```\n\n## Discover the available properties for a datum stream\n\nIn order to populate the **Datum Property** form menu with a list of the available meter-style properties of the selected **Source ID** the app queries the [/datum/stream/meta/node][datum-stream-meta-node] method using the **Node ID** and **Source ID** values in the form. This method [returns a list of datum stream metadata objects][datum-stream-meta-node-res] and looks like this:\n\n```json\n{\n\t\"success\": true,\n\t\"data\": [\n\t\t{\n\t\t\t\"streamId\": \"9458020e-789b-49d5-8a29-d9b53fde622f\",\n\t\t\t\"zone\": \"Pacific/Auckland\",\n\t\t\t\"kind\": \"n\",\n\t\t\t\"objectId\": 123,\n\t\t\t\"sourceId\": \"/meter/1\",\n\t\t\t\"i\": [\n\t\t\t\t\"watts\",\n\t\t\t\t\"current\",\n\t\t\t\t\"voltage\",\n\t\t\t\t\"frequency\",\n\t\t\t\t\"apparentPower\",\n\t\t\t\t\"reactivePower\"\n\t\t\t],\n\t\t\t\"a\": [\"wattHours\"]\n\t\t}\n\t]\n}\n```\n\nSince the app only wants to display meter reading values, the [_accumulating_][datum-prop-class] properties in the metadata objects's `a` property are then populated as options in the **Datum Property** form menu:\n\n```ts\n// create a filter with the node and source ID values from the form\nconst filter = new DatumFilter();\nfilter.nodeId = Number(nodeId);\nfilter.sourceId = sourceId;\n\n// construct the API URL to call\nconst streamMetaUrl =\n\turlHelper.baseUrl() + \"/datum/stream/meta/node?\" + filter.toUriEncoding();\n\n// generate authorization headers using the token credentials in the form\nconst headers = authorizeUrl(streamMetaUrl);\n\n// make API request using Fetch API\nconst res = await fetch(streamMetaUrl, {\n\tmethod: \"GET\",\n\theaders: headers,\n});\n\n// wait for response\nconst json = await res.json();\n\n// clear out and re-populate the property names menu\nwhile (settingsForm.snDatumProperty.length) {\n\tsettingsForm.snDatumProperty.remove(0);\n}\nif (json.data.length) {\n\tsettingsForm.snDatumProperty.add(new Option(\"Choose...\", \"\"));\n\tfor (const meta of json.data) {\n\t\t// add all accumulating properties to menu\n\t\tif (Array.isArray(meta.a)) {\n\t\t\tfor (const p of meta.a) {\n\t\t\t\tsettingsForm.snDatumProperty.add(new Option(p, p));\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n## Query datum stream\n\nOnce the date range, node, source, and property are all configured, the app can query SolarNetwork for the actual datum stream. It uses the [/datum/stream/reading][datum-stream-reading-list] method to query for hourly meter readings over the given date range.\n\n\u003e **Note** there are other query methods that could be used, such as [/datum/stream/datum][datum-stream-datum-list] or [/datum/list][datum-list] or [/datum/reading][datum-reading]. The choice on which to use is up to the needs of your app.\n\nThe `/datum/stream/reading` API returns a result like this:\n\n```json\n{\n\t\"success\": true,\n\t\"meta\": [\n\t\t{\n\t\t\t\"streamId\": \"7714f762-2361-4ec2-98ab-7e96807b32a6\",\n\t\t\t\"zone\": \"Pacific/Auckland\",\n\t\t\t\"kind\": \"n\",\n\t\t\t\"objectId\": 123,\n\t\t\t\"sourceId\": \"/power/1\",\n\t\t\t\"i\": [\"watts\", \"current\", \"voltage\", \"frequency\"],\n\t\t\t\"a\": [\"wattHours\"]\n\t\t}\n\t],\n\t\"data\": [\n\t\t[\n\t\t\t0,\n\t\t\t[1650667326308, null],\n\t\t\t[12326, 600, 8290, 14222],\n\t\t\tnull,\n\t\t\t[230.19719, 600, 228.2922, 233.12324],\n\t\t\t[50.19501, 600, 49.94322, 50.20012],\n\t\t\t[6472722, 2819093834849, 2819100307571]\n\t\t]\n\t]\n}\n```\n\nThe app converts each stream result object into a `GeneralDatum` object that looks like this:\n\n```js\n{\n\tnodeId: 123,\n\tsourceId: \"/power/1\",\n\tdate: Date(1650667326308),\n\twatts:12326,\n\tvoltage:230.19719,\n\tfrequency:50.19501,\n\twattHours:6472722\n}\n```\n\nThe code involved looks like this:\n\n```ts\n// create filter with Hour aggregation, node/source IDs, and date range from the form\nconst filter = new DatumFilter();\nfilter.aggregation = Aggregations.Hour;\nfilter.nodeId = Number(nodeId);\nfilter.sourceId = sourceId;\nif (startDate) {\n\tstartDate.setHours(0, 0, 0, 0);\n\tfilter.localStartDate = startDate;\n}\nif (endDate) {\n\tendDate.setHours(0, 0, 0, 0);\n\tfilter.localEndDate = endDate;\n}\n\n// construct the API URL to call\nconst streamDataUrl =\n\turlHelper.baseUrl() +\n\t\"/datum/stream/reading?readingType=\" +\n\tDatumReadingTypes.Difference.name +\n\t\"\u0026\" +\n\tfilter.toUriEncoding();\n\n// generate authorization headers using the token credentials in the form\nconst headers = authorizeUrl(streamDataUrl);\n\n// make API request using Fetch API\nconst res = await fetch(streamDataUrl, {\n\tmethod: \"GET\",\n\theaders: headers,\n});\n\n// wait for response\nconst json = await res.json();\nif (\n\t!(\n\t\tjson \u0026\u0026\n\t\tArray.isArray(json.data) \u0026\u0026\n\t\tArray.isArray(json.meta) \u0026\u0026\n\t\tjson.meta.length\n\t)\n) {\n\treturn Promise.reject(\"No data available.\");\n}\n\n// convert stream results into GeneralDatum to more easily use in charts\nconst result: GeneralDatum[] = [];\n\n// create a DatumStreamMetadataRegistry to associate result objects with stream metadata\nconst reg = DatumStreamMetadataRegistry.fromJsonObject(json.meta);\nif (!reg) {\n\treturn Promise.reject(\"JSON could not be parsed.\");\n}\nfor (const data of json.data) {\n\t// get the stream metadata for this result\n\tconst meta = reg.metadataAt(data[0]);\n\tif (!meta) {\n\t\tcontinue;\n\t}\n\n\t// convert stream result object into GeneralDatum object\n\tconst d = datumForStreamData(data, meta)?.toObject();\n\tif (d) {\n\t\tresult.push(d as GeneralDatum);\n\t}\n}\nreturn Promise.resolve(result);\n```\n\n## Render chart with billboard.js\n\nOnce the list of `GeneralDatum` has been obtained, an area [chart is rendered][render-chart] using time on the x-axis and **Property Name** values on the y-axis. This is done using [billboard.js][billboard] and looks like this:\n\n```ts\n// c looks like {propName: \"foo\", displayName: \"kWh\", scale: 1}\nconst c = seriesConfig(config);\nbb.generate({\n\tdata: {\n\t\tjson: datum,\n\t\tkeys: {\n\t\t\tx: \"date\",\n\t\t\t// render the \"Datum Property\" form value\n\t\t\tvalue: [c.propName],\n\t\t},\n\t\ttype: area(),\n\t},\n\taxis: {\n\t\tx: {\n\t\t\ttype: \"timeseries\",\n\t\t\ttick: {\n\t\t\t\tcount: 6,\n\t\t\t\tfit: false,\n\t\t\t\tformat: \"%Y-%m-%d %H:%M\",\n\t\t\t},\n\t\t\tpadding: {\n\t\t\t\tleft: 20,\n\t\t\t\tright: 10,\n\t\t\t\tunit: \"px\",\n\t\t\t},\n\t\t},\n\t\ty: {\n\t\t\tlabel: c.displayName,\n\t\t\ttick: {\n\t\t\t\t// scale the value using the \"Unit scale\" form value\n\t\t\t\tformat: function (v: number) {\n\t\t\t\t\treturn v / c.scale;\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tlegend: {\n\t\thide: true,\n\t},\n\tzoom: {\n\t\tenabled: zoom(),\n\t\ttype: \"drag\",\n\t},\n\ttooltip: {\n\t\tformat: {\n\t\t\ttitle: tooltipDateFormat,\n\t\t\tname: () =\u003e \"Example Chart\",\n\t\t},\n\t},\n\tpoint: {\n\t\tfocus: {\n\t\t\tonly: true,\n\t\t},\n\t},\n\tbindto: \"#chart\",\n});\n```\n\n# Building from source\n\nTo build yourself, clone or download this repository. You need to have\nNode 16+ installed. Then:\n\n```sh\n# initialize dependencies\nnpm ci\n\n# run development live server on http://localhost:8080\nnpm run dev\n\n# build for production\nnpm run build\n```\n\nRunning the `build` script will generate the application into the `dist/` directory.\n\n[auth-instance]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L17\n[auth-headers]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L42-L55\n[billboard]: https://naver.github.io/billboard.js/\n[create-datum-filter]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L85-L94\n[datum-list]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#datum-list\n[datum-prop-class]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarNet-API-global-objects#datum-property-classifications\n[datum-reading]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#datum-reading\n[datum-stream-meta-node]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#datum-stream-metadata-list\n[datum-stream-meta-node-res]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#datum-stream-metadata-list-response\n[datum-stream-datum-list]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-Stream-API#datum-stream-datum-list\n[datum-stream-reading-list]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-Stream-API#datum-stream-reading-list\n[datum-stream-reading-list-res]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-Stream-API#datum-stream-reading-list-response\n[discover-sources]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L96-L119\n[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API\n[sn-api-auth]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarNet-API-authentication-scheme-V2\n[find-sources-url]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L96\n[render-chart]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/main.ts#L30-L33\n[reportable-node-sources]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#reportable-node-sources\n[reportable-node-sources-res]: https://github.com/SolarNetwork/solarnetwork/wiki/SolarQuery-API#reportable-node-sources-response\n[sn-api-core]: https://www.npmjs.com/package/solarnetwork-api-core\n[sn-api-core-dep]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/package.json#L44\n[import-sn-auth]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L6-L9\n[save-creds]: https://github.com/SolarNetwork/solarnetwork-example-ts-chart-billboard/blob/1.0.0/src/main/ts/sn.ts#L77-L78\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolarnetwork%2Fsolarnetwork-example-ts-chart-billboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsolarnetwork%2Fsolarnetwork-example-ts-chart-billboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolarnetwork%2Fsolarnetwork-example-ts-chart-billboard/lists"}