{"id":25596853,"url":"https://github.com/nanoporetech/cronkite","last_synced_at":"2025-04-13T02:31:13.341Z","repository":{"id":42934734,"uuid":"236981939","full_name":"nanoporetech/cronkite","owner":"nanoporetech","description":"One **hell** of a reporter","archived":false,"fork":false,"pushed_at":"2023-01-07T04:31:00.000Z","size":4392,"stargazers_count":8,"open_issues_count":6,"forks_count":0,"subscribers_count":20,"default_branch":"master","last_synced_at":"2025-04-06T08:02:14.504Z","etag":null,"topics":["dashboard-application","dashboard-framework","dashboard-widget","dashboards","stencil-components","stenciljs","web-components"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nanoporetech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-01-29T12:55:42.000Z","updated_at":"2022-06-04T08:34:09.000Z","dependencies_parsed_at":"2023-02-06T11:46:23.895Z","dependency_job_id":null,"html_url":"https://github.com/nanoporetech/cronkite","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoporetech%2Fcronkite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoporetech%2Fcronkite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoporetech%2Fcronkite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nanoporetech%2Fcronkite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nanoporetech","download_url":"https://codeload.github.com/nanoporetech/cronkite/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248594190,"owners_count":21130316,"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":["dashboard-application","dashboard-framework","dashboard-widget","dashboards","stencil-components","stenciljs","web-components"],"created_at":"2025-02-21T12:34:50.069Z","updated_at":"2025-04-13T02:31:13.313Z","avatar_url":"https://github.com/nanoporetech.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cronkite - One __*helluva*__ reporter\n\n## INTRODUCTION\n\nConkite is web component for rendering UI interfaces (`dashboards`) that have been defined in a JSON schema. Given a valid schema, cronkite will manage the rendering of UI and non-UI web components in a web page. These web components include:\n\n1. Event emitters (sources of data)\n2. Event consumers (listeners)\n3. Web page tags to render including\n    - style\n    - layout\n4. Event payload transforms\n5. Mappings between payload transforms and tag attributes/props\n\n## DEMO\n\nhttps://nanoporetech.github.io/cronkite/\n\n## [INSTALLING CRONKITE](./INSTALLING.md)\n\n## COMPONENTS\n\n### 1. ADDING HTML TAGS/ELEMENTS/WEB COMPONENTS\n\nThe most simple report would be one with a single component, no listeners and no data sources e.g:\n\n```javascript\n{\n  \"components\": [\n      {\n          \"element\": \"h1\"\n      }\n  ],\n  \"streams\" : []\n}\n```\n\nwould render...\n\n`\u003ch1\u003e\u003c/h1\u003e`\n\n\u003e ### What's going on\n\u003e\n\u003e We define HTML elements to render in `components`. The JSON above will render a single `h1` element but you wouldn't see anything on the `dashboard` because we haven't added any data or text. If you inspect the page you will however notice an\n`h1` tag has been written to the DOM.\n\n\u003chr /\u003e\n\n### 2. ADDING ATTRIBUTES TO TAGS\n\nWe will now add attributes to the h1 tag. Attributes are described in the JSON using the `@` prefix. We'll add the a `foo` and `innerHTML` attributes to the h1 tag below:\n\n```javascript\n{\n    \"components\": [\n        {\n            \"element\": \"h1\",\n            \"@foo\": \"bar\",\n            \"@innerHTML\": \"Hello world!\"\n        }\n    ],\n    \"streams\" : []\n}\n```\n\nwould render...\n\n`\u003ch1 foo=\"bar\"\u003eHello World!\u003c/h1\u003e`\n\n\u003e ### What's going on\n\u003e\n\u003e Any key prefixed with the `@` symbol will be set as an `Element` prop on the tag and the value will be whatever is specified in the definition. Something special is happening with the `@innerHTML` attribute. `@innerHTML` is a prop that is native to any DOM Element objects (see [Element.innerHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML)). By setting this prop you are in fact interacting with the native DOM api for an Element and so the inner HTML content of the tag is set to whatever the values was. `@foo` is not a prop of `Element` and so is added to the tag as an attribute.\n\n### 3. EVENT LISTENERS \u0026 EVENT PAYLOADS\n\nIn the example above `Hello World!` is a static value as it is hard coded as the text inside the h1 element. The value of any attribute (and hence the value of `@innerHTML` in the example above) can be extracted from the payload of any event. Cronkite wil handle any event provided it inherits from [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) such as `CustomEvent`. Events will usually carry a payload of data, and we can extract and transform values from these payloads using functions built into Cronkite. Pulling content out of complex JSON blobs is handled using the [JMESPath library](http://jmespath.org/) and spec. In order to reference the jmespath function in the `dashboard` JSON schema we use the following function reference:\n\n```javascript\n{\n    \"fn:jmespath\": \"\u003cJMESPath goes here!!\u003e\"\n}\n```\n\nSo by way of example, lets display the `[clientX, clientY]` values of the mouse as it moves around over the UI. The corresponding JSON would look as follows:\n\n```javascript\n{\n    \"components\": [\n        {\n            \"element\": \"div\",\n            \"@innerText\": {\n                \"fn:jmespath\": \"[clientX, clientY]\"\n            },\n            \"listen\": \"mousemove\"\n        }\n    ],\n    \"streams\" : []\n}\n```\nand would render...\n\n`\u003cdiv\u003e123,456\u003c/div\u003e`\n\nand the X/Y coordinates would continue to change dynamically as the mouse is moved around\n\n\n\n\u003e ### What's going on\n\u003e\n\u003e Firstly, we set up a listener on the `element` using the `listen` attribute. The value set to the __event name__ that will be listened to (`mousemove` in this case). `mousemove` events are emitted when the mouse moves around the dashboard. Each `mousemove` event carries with it a payload of information including the `clientX` and `clientY` for example:\n\n```javascript\n\n// MouseMoveEvent payload\n{\n    \"altKey\": false,\n    \"bubbles\": true,\n    \"cancelable\": true,\n    \"clientX\": 648,\n    \"clientY\": 928,\n    \"layerX\": 648,\n    \"layerY\": 2194,\n    // Many more attributes besides...\n}\n\n```\n\nUsing the jmespath `[clientX, clientY]` we pull out the clientX and clientY from the payload root cor current node `@` and project them into a new array containing both coordinates. NOTE: `[clientX, clientY]` and `[@.clientX, @.clientY]` are equivalent because both queries operate on the \"current node\" which is the event payload.\n\nTo demonstrate a more advanced JMESPath we could have set the `@innerText` value to `\"Client X: NNN, Client Y: NNN\"` with the following JMESPath expression:\n\n```javascript\n{\n    \"fn:jmespath\": \"format('Client X: ${0}, Client Y: ${1}', [clientX, clientY])\"\n}\n```\n\nPayloads can be arbitrarily nested and this is supported in the JMESPath spec.\nWe won't go into explaining the JMESPath above but if you're interested in finding out more then check out the [specification page](http://jmespath.org/specification.html).\n\n#### 3.1 LISTENER OPTIONS\n\nListeners can optionally be fine tunes with options to window over event streams or debounce  busy streams. In the example above `mousemove` generates **LOADS** of events and hence payloads. We can do better that processing every payload by debouncing the stream. To do that the configuration can be changed to:\n\n```javascript\n{\n    \"components\": [\n        {\n            \"element\": \"div\",\n            \"@innerText\": {\n                \"fn:jmespath\": \"[clientX, clientY]\"\n            },\n            \"listen\": {\n              \"stream\": \"mousemove\",\n              \"debounce\": 300,\n              \"cache\": 1 // Not required as this is the default\n            }\n        }\n    ],\n    \"streams\" : []\n}\n```\n\u003e ### What's going on\n\u003e\n\u003e We're still listening to the `mousemove` event stream but now payloads are debounced to 300ms and although we've set the cache to 1 (the default) we could increase that number to collect a window of the last N `mousemove` payloads. This is particularly useful when you want to maintain a history of previous events as in the Socket.io demo.\n\n\u003chr /\u003e\n\n### 4. JMESPATH EXTENSIONS\n\nIn the example above we use the format function to generate a new string from a template and input source. This is one of many extensions to the JMESPath spec that are available in the expressions. JMESPath expressions in Cronkite include the [standard built-in functions from the spec](https://jmespath.org/specification.html#built-in-functions) as well as the following extra functions to help with payload reshaping:\n\n#### LODASH EXTENSIONS TO JMESPATH EXPRESSIONS\n\nAll of lodash functions are included in JMESPath expressions and are prefixed with an `_` e.g:\n\nto use `lodash.fromPairs()` you would write your expression like\n```javascript\nit('calls lodash.fromPairs with `_fromPairs` in JMESPath function expression', async () =\u003e {\n    const returnValue = search('_fromPairs(@)', [\n      ['a', 1],\n      ['b', 2],\n    ]);\n    expect(returnValue).toStrictEqual({ a: 1, b: 2 });\n  });\n\n```\n\u003e This is caveated by the fact that some lodash functions require a predicate which is a function. These will obviously not work in JMESPath as there is no way to describe a function literal.\n\n#### CRONKITE EXTENSIONS TO JMESPATH EXPRESSIONS\n\n- `mean` - Calculate the mean of `Array\u003cnumber\u003e`\n- `mode` - Calculate the mean of `Array\u003cnumber\u003e`\n- `median` - Calculate the mean of `Array\u003cnumber\u003e`\n- `toFixed` - Fixes the precision of a number to a given number decimals\n- `formatNumber` - Formats numbers given a precision and unit\n- `uniq` - De-duplicates a list of values\n- `divide` - Divides two numbers\n- `split` - Splits a string on a given pattern\n- `entries` - Lists entries in a map\n- `format` - Formats in put source given a template\n- `flatMapValues` - Flatten objects into `Array\u003ckey,value\u003e` where any iterable values are also decomposed\n- `toUpperCase` - Upercase all characters in a string\n- `toLowerCase` - Lowercase all characters in a string\n- `trim` - Trim whitespace off the beginning and end of a string\n- `groupBy` - Group an `Array\u003cobject\u003e` on a key or expression\n- `combine` - A better merge\n\n### 5. PAYLOAD TRANSFORMS (JMESPath extensions)\n\nYou have already been introduced to your first payload transform `fn:jmespath` which fishes out values from any JSON blobs provided as a payload to `Event`. Transforms are `object` types and are identified by keys with the `fn:xxxxxx` prefix. The Cronkite transforms are provided as an escape hatch when the jmespath spec lacks operations necessary to reshape the payloads or perform unsupported operations. At the time of writing the following transforms were supported\n\n```javascript\n{\n    \"fn:average\": // Calculate the average value for `number[]` (JMESPath spec see `avg` function)\n    \"fn:count\": // Calculate the length of a `any[]` (JMESPath spec see `length` function)\n    \"fn:formatNumber\": // Format a number with locale and units\n    \"fn:divide\": // Divide two numbers and return the result\n    \"fn:mod\": // Modulus two numbers and return the result\n    \"fn:jmespath\": // Fish out values from Event payloads\n    \"fn:map\": // Create a new object of key:value pairs\n    \"fn:mode\": // Calculate the mode of a `number[]`\n    \"fn:round\": // Round a number to the nearest whole\n    \"fn:sum\": // Calculate the sum of a `number[]`\n    \"fn:toFixed\": // Set the number of decimal values of a float\n    \"fn:uniq\": // Create a unique list of values from `any[]`\n}\n```\n\n\u003chr /\u003e\n\n## STREAMS\n\nStreams are a special type of component in that they __*do not*__ render any UI elements. They do however, emit events (usually `CustomEvent` types) with corresponding payloads that can be consumed by `components` via `listen` as described above.\n\nThere are (at the time of writing) two configurable data stream components available in Cronkite:\n  1. [cronk-poll-datastream](src/components/cronk-poll-datastream/readme.md)\n\n\n### 1. [cronk-poll-datastream](src/components/cronk-poll-datastream/readme.md)\n\nThe [cronk-poll-datastream](src/components/cronk-poll-datastream/readme.md) stream will abstract URL polling (via the fetch API) to any CORS enabled URL that returns an `application/json` response.\n\n#### Parameterisation of the request\n\n- `element` (required - `\"cronk-poll-datastream\"`)\n- `@url` (required - CORS enabled remote resource returning JSON)\n- `@channels` (optional - If a successful response is returned and no channels\n    are defined then cronkite will emit the payload on a default stream called\n    `cronkite:stream`)\n\n    You are able to specify the name of the event(s) (stream) that payloads are delivered on as well as how payloads are shaped for each stream. You can use almost any event stream names and then Cronkite will attach listeners to UI components if they're defined correctly as was demonstrated above. In the example below event payloads are delivered on the `my:todos` event stream and one would expect (although not enforced) there to be a component with an `\"@listen\": \"my:todos\"` key:value in its JSON configuration.\n\n- `@poll-frequency` (optional - default `15000`)\n\n    Users can control the frequency with which to poll the URL for changes using the `@poll-frequency` attribute. Polling is useful in settings where source data is updating in real time. Responses are cached internally to the data stream which only makes new GET requests once changes are detected at the remote source. `@poll-frequency` is optional and set at 15sec (15000). In cases where polling is not required users can simulate single requests by setting `@poll-frequency` to a very large number like `1e10`\n\n- `@acceptsFilters` (Optional - default `false`: Whether or not payloads emitted can be filtered by a custom function (discussed later))\n- `@credentials` (Optional - default `\"include\"` can be set to `\"include\" | \"omit\" | \"same-origin\"`)\n\n#### Example:\n\n```javascript\n\"streams\": [\n    {\n        \"@channels\": [\n          {\n            \"channel\": \"my:todos\"\n          }\n        ],\n        \"element\": \"cronk-poll-datastream\",\n        \"@url\": \"https://jsonplaceholder.typicode.com/todos\",\n        \"@poll-frequency\": 25000\n    }\n]\n```\n\n\u003e ### What's going on\n\u003e\n\u003e `element` specifies a web component [cronk-poll-datastream](src/components/cronk-poll-datastream/readme.md) that (in this case) abstracts the W3C fetch API. The component polls a URL provided by the user at regular intervals for JSON data. Successful responses are emitted as the payload to a `CustomEvent` object. The default event name is `cronkite:stream` although the user can specify a custom event name using the `@channels` attribute. In the example above, the url `https://jsonplaceholder.typicode.com/todos` will be polled every 25 seconds and the most recent successful response cached. On successful data fetch, the JSON response will be emitted as a `CustomEvent` with the event name set to `my:todos`.\n\n\u003chr/\u003e\n\n## WEB COMPONENTS\n\nEach component in the `components` block of the JSON specifies a single HTML tag in the `element` field. This will be rendered as a DOM element to which event handlers will be attached and attributes set. Apart from the standard set of HTML 5 tags a growing number of `Custom Elements` (web components) are being provided to help visualize more complex data. At the time of writing these include:\n\n- [cronk-funnel](src/components/cronk-funnel/readme.md)\n- [cronk-title](src/components/cronk-title/readme.md)\n- [cronk-version](src/components/cronk-version/readme.md)\n- [cronk-statsbox](src/components/cronk-statsbox/readme.md)\n- [cronk-selector](src/components/cronk-selector/readme.md)\n- `\u003cepi-headlinevalue\u003e`\n- `\u003cepi-coverageplot\u003e`\n- `\u003cepi-donutsummary\u003e`\n- `\u003cepi-checkmark\u003e`\n\n... as well as all of the web components in [Ionic 4](https://ionicframework.com/docs/components)\n\nFor information about the attributes available to set by the user see the [Storybook playground](https://metrichor-ui.git.oxfordnanolabs.local/component-storybook/?path=/story/epi2me-checkmark--default-configurable) where you can read documentation on the components and live-edit the values of attributes.\n\n### The [cronk-selector](src/components/cronk-selector/readme.md) component\n\nThe [cronk-selector](src/components/cronk-selector/readme.md) component is a cronkite specific component that generates filter functions and attaches them to datastreams that are configures to accept filters with the `@acceptsFilters` prop. The [cronk-selector](src/components/cronk-selector/readme.md) component currently has the following configuration as illustrated in the full example below:\n\n```javascript\n{\n  \"element\": \"cronk-selector\",\n  \"heading\": \"SELECT RUNID\",\n  \"@selectList\": {\n    \"fn:jmespath\": \"data.reads[?exit_status=='Classified'].{label: @.runid, select: @.runid, count: @._stats.count}\"\n  },\n  \"@selector\": [\"barcode\"],\n  \"listen\": \"qctelemetry:basecalling1d:1\"\n}\n```\n\n\u003e ### What's going on\n\u003e\n\u003e `element` specifies must be set to `\"cronk-selector\"` to use this component. In the example above the value of the `@selectList` attribute is set to a list of barcodes pulled out of event payload using JMESPath. A unique list of whatever is returned will be created by the component. The event listened to is `qctelemetry:basecalling1d:1`. Once the user makes a selection, a filter predicate (a closure containing the current selection) will be forwarded to all data streams which are configured to filter responses based on the `@selector` value. The `@acceptsFilters` prop is used to configure this for the datastream\n\n\u003chr/\u003e\n\n## PUTTING IT ALL TOGETHER\n\nHere's an full example of a [hello-world](./examples/reports/hello-world.json) report with layout defined including:\n\n1. Static values\n2. Components responding to DOM events\n3. Conditional styling of components\n4. Cronkite builtin functions\n5. Polling datastream\n6. JMESPath MAGIC!! ✨🌈🦄✨\n\n\n```javascript\n{\n  \"id\": \"hello:world\",\n  \"components\": [\n    {\n      \"element\": \"epi-headlinevalue\",\n      \"@label\": \"User defined values\",\n      \"@value\": \"Hello World!\"\n    },\n    {\n      \"element\": \"epi-headlinevalue\",\n      \"@label\": \"Native DOM events\",\n      \"@value\": {\n        \"fn:jmespath\": \"join(`, `, [join(`Client X: `, [``, to_string(clientX)]), join(`Client Y: `, [``, to_string(clientY)])])\"\n      },\n      \"listen\": \"mousemove\",\n      \"layout\": {\n        \"width\": 2\n      }\n    },\n    {\n      \"element\": \"epi-checkmark\",\n      \"@message\": {\n        \"fn:jmespath\": \"(clientX\u003e`500` \u0026\u0026 'GREATER than 500px' || 'LESS than 500px') || 'Mouse position...'\"\n      },\n      \"@status\": {\n        \"fn:jmespath\": \"clientX\u003e`500` \u0026\u0026 `success` || `error`\"\n      },\n      \"@size\": {\n        \"fn:divide\": [\n          {\n            \"fn:jmespath\": \"clientY\"\n          },\n          4\n        ]\n      },\n      \"listen\": \"mousemove\",\n      \"layout\": {\n        \"width\": 2\n      }\n    },\n    {\n      \"element\": \"div\",\n      \"style\": {\n        \"backgroundColor\": \"#222\"\n      },\n      \"components\": [\n        {\n          \"element\": \"h1\",\n          \"@innerHTML\": \"EUROPE PMC SEARCH RESULTS\",\n          \"layout\": {\n            \"position\": \"header\"\n          }\n        },\n        {\n          \"element\": \"epi-headlinevalue\",\n          \"@value\": {\n            \"fn:jmespath\": \"request.queryString\"\n          },\n          \"@label\": \"Query string\",\n          \"listen\": \"hello:world:event\"\n        },\n        {\n          \"element\": \"ol\",\n          \"@innerHTML\": {\n            \"fn:jmespath\": \"join(``, [].join(``,['\u003cli\u003e\u003ca href=\\\\'',@.id,'\\\\'\u003e',@.title,'\u003c/a\u003e']))\"\n          },\n          \"@label\": \"Query string\",\n          \"listen\": \"hello:world:results\"\n        }\n      ]\n    }\n  ],\n  \"streams\": [\n    {\n      \"element\": \"cronk-poll-datastream\",\n      \"@url\": \"https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=AUTH:%22Kulesha+E%22\u0026format=json\",\n      \"@poll-frequency\": 25000,\n      \"@credentials\": \"omit\",\n      \"@channels\": [\n        {\n          \"channel\": \"hello:world:event\",\n          \"shape\": {\n            \"fn:jmespath\": \"@\"\n          }\n        },\n        {\n          \"channel\": \"hello:world:results\",\n          \"shape\": {\n            \"fn:jmespath\": \"resultList.result\"\n          }\n        }\n      ]\n    }\n  ]\n}\n\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanoporetech%2Fcronkite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnanoporetech%2Fcronkite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnanoporetech%2Fcronkite/lists"}