diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..dcb5d90 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +graphtool-demo.harutohiroki.com \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a2b6f9f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# For Contributors +- The code is a mess, I know. I'm working on it. I'm sorry. +- If you want to contribute and help translate the tool, all the source English text is in the `en.json` file. Just copy the file and rename it to your language code (make sure to use ISO 639-1 Language Codes for auto-detection e.g. `fr.json` for French) and translate the text. I'll take care of the rest. +- The translation feature will always prioritize what's in the language files. If a text is missing in the translation file, it will default to the text in the `config.json` file. +- Please don't make any text/value `null`. If you want to not have a text/value, just leave it empty (e.g. `""` or `[]`). +- For targets, each target `"type"` is a key in the language files. The value of the key is the name of the category of targets. diff --git a/Configuring.md b/Configuring.md index 7cf6409..0df7b2a 100644 --- a/Configuring.md +++ b/Configuring.md @@ -35,17 +35,12 @@ page works and isn't claiming it's crinacle.com. * Remove or change the watermark. * Remove the `targets`, replace them with your own, or get permission from Crinacle to use the ones in the CrinGraph repository. -* If you are using a free/premium model, change `premium_html` in - `config_free.js` to point to your own site(s). ## Configuring CrinGraph -The main page used to display graphs is [graph.html](graph.html), which +The main page used to display graphs is [index.html](index.html), which defines the basic structure of a page and then includes a bunch of Javascript files that do the real work (at the end of the file). -[graph_free.html](graph_free.html) is identical but uses a different -configuration file to remove some functionality. You will only need to -use it if you intend to have both a free and a paid graph tool. Ideally all configuration can be done simply by changing [config.js](config.js). However, there are currently not very many @@ -72,7 +67,7 @@ Here are the current configuration parameters: key) is. You probably don't need to change this. * `targets` lists the available target frequency responses. If you don't want to display any targets set it to `false`. If you do use targets, - each one should be a file named `... Target.txt` in the `DIR` + each one should be a file named `... Target.txt` in the `DIR/targets` directory you specified. The targets which are already there were provided by Crinacle so make sure you have his permission before using them. @@ -81,21 +76,6 @@ Here are the current configuration parameters: its value is multiplied by `scale_smoothing` to get the actual level of smoothing. -The following parameters can be set to configure a restricted version -of the graph tool. They are only present in -[config_free.js](config_free.js). If you don't set them the tool will -be unrestricted. - -* `max_compare` is the maximum number of graphs allowed at a time. -* `disallow_target` prevents target FRs from ever being loaded. -* `allow_targets` is a list of target names, and overrides - `disallow_target` for those targets, so they can be loaded. If - `disallow_target` isn't set, it has no effect. -* `premium_html` is the message shown when a user tries to do something - which isn't allowed according to the previous two settings. Given that - it points to Crinacle's patreon and not yours, you probably want to - change it. - The following parameters are used to allow multiple samples per channel and different channel configurations than L/R. For example, `config_hp.js` is intended for headphones and shows only the right diff --git a/LICENSE b/LICENSE index ddf9912..7d9c1ec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,12 @@ -BSD Zero Clause License +Copyright (c) 2019 Marshall Lochbaum Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..cda95e4 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,9 @@ +# Migrating to Haruto's GraphTool version of CrinGraph for existing users of Squig.link + +## Steps + +1. Drag and drop your data folder over, overwriting the existing data folder. +2. (RECOMMENDED BUT OPTIONAL) Separate targets in your data folder to a separate folder named "targets" for each data folders. +3. Make changes to `config.js` and `config_hp.js` as necessary, should just be copy pastes from your old files as I've standardized the naming. +4. Make changes to `index.html` (your IEM page), and `index_hp.html` (your headphones page) as necessary, make sure to not touch anything outside the `` tag. +5. Deploy and enjoy your new functionalities! \ No newline at end of file diff --git a/README.md b/README.md index b2a7855..1207d46 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,59 @@ +# Demo Page +https://graphtool-demo.harutohiroki.com/ + +# Changes +- Added Equalizer (cred to Rohsa) +- Added Uploads +- Added Targets +- Added Website link on graph (cred to MRS) +- Re-themed graph window +- Re-done Frequency Range definitions +- Changed parser to universal parser +- Removed Restricted mode (cuz I want to keep it free) +- Reorganised code +- Moved targets to a different folder for organization +- Moved phone_book outside for easier access (reverted for squiglink compatibility) +- Added a function to average all active graphs (requested by listener) +- Custom Diffuse Field Tilt (requested by listener) +- Restyled EQ tab +- EQable pink noise in EQ tab (requested by listener) +- Added the ability to upload your own test track to EQ (requested by rollo) +- Added a button to disable and enable all EQ bands (requested by SK) +- Tone generator now EQable (requested by SK) +- Added a Channel balance slider +- Added a song progress slider to the EQ demo section (requested by XiaoShe) +- Added Ear Gain customisation to custom tilt (requested by listener) +- Made any target tiltable (requested by listener) +- Added Treble customisation to custom tilt (requested by listener) +- Added a button to swap between different y-axis scales (requested by rollo) +- Added Preference Bounds and Preference Bound scaling (requested by listener) +- Reversed the "any target tiltable" feature, now applying tilt on target automatically if supported (requested by listener) +- Per-measurement compensation (requested by listener) +- Added support for Haruto's Graph Extension to apply eq to browserwide +- Made Preference Bounds better and not relying on a png anymore +- Downloadable CSV of all active graphs +- Per page Y scaling (requested by listener) +- Added a Graph Customisation menu +- Added Translations (Thanks to potatosalad775) (removed for now due to not having enough translations, will be added back soon) +- Added the 90% Inclusion Zone feature (requested and long awaited by the community) + +# TODO +- Implement a way to measure the SPL of an IEM and decide whether to upload it or not, skipping REW + - ability to select which mic/output to use + - ability to select calibration files + - ability to apply smoothing +- Trace Arithmetic +- Realtime Analysis +- EQ upload to hardware + +# Contributors + + + + +# P.S. +- If you do implement code in here, do leave credits to the original author (me) and the contributors (Rohsa, MRS, potatosalad775) + # The In-Ear Graphing Library If you're not weirdly obsessed with headphones you can leave at any time. diff --git a/assets/audio/Scarlet_Fire-Otis_McDonald.mp3 b/assets/audio/Scarlet_Fire-Otis_McDonald.mp3 new file mode 100644 index 0000000..29681ab Binary files /dev/null and b/assets/audio/Scarlet_Fire-Otis_McDonald.mp3 differ diff --git a/assets/audio/pinknoise.wav b/assets/audio/pinknoise.wav new file mode 100644 index 0000000..e17ff09 Binary files /dev/null and b/assets/audio/pinknoise.wav differ diff --git a/styles/dark.css b/assets/css/dark.css similarity index 100% rename from styles/dark.css rename to assets/css/dark.css diff --git a/assets/css/extra.css b/assets/css/extra.css new file mode 100644 index 0000000..05c93aa --- /dev/null +++ b/assets/css/extra.css @@ -0,0 +1,810 @@ +/** hidden class **/ +.hidden { + display: none !important; +} + +/** Extra buttons **/ +button#avg-all, +button#yscalebtn { + position: relative; + + box-sizing: border-box; + padding: 11px 16px; + margin: 0; + overflow: hidden; + + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more) !important; + border: none; + border-radius: 6px; + + color: var(--accent-color-contrast); + font-family: var(--font-primary); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; + outline: none; +} + +button#avg-all:active, +button#yscalebtn:active { + box-sizing: border-box; + background-color: var(--accent-color) !important; + border-color: var(--accent-color) !important; + + color: var(--font-color-secondary); +} + +button#avg-all { + margin-left: 6px; +} + +/** y scaler **/ +div.yscaler { + order: 1; + + display: flex; + align-items: center; + + padding: 0 16px; + border-left: 1px solid var(--background-color-contrast-more); +} + +div.yscaler > span { + margin-right: 10px; + min-width: max-content; + + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + white-space: nowrap; +} + +div.yscaler > div { + display: flex; + align-items: center; +} + +/** extra anim and logo stuff **/ + +text.site_name { + color: var(--font-color-primary); + color: magenta; + font-family: var(--font-primary); + font-weight: 700; + font-size: 14px; + + filter: var(--svg-filter); +} + +image.graph_logo { + filter: var(--svg-filter); +} + +/** Color Picker Thingy **/ +.colorStylePicker { + display: flex; + align-items: flex-start; + box-sizing: border-box; + flex-wrap: wrap; + flex-direction: row; + background-color: var(--background-color); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + z-index: 1000; +} + +.colorStylePicker .left-side { + display: flex; + align-items: center; + padding: 0 10px 0 0; +} + +.colorStylePicker .right-side { + flex: 1; + align-items: center; +} + +.colorStylePicker .right-side .row { + display: flex; + flex-direction: row; +} + + +.colorStylePicker .right-side>div>span { + color: var(--font-color-inputs); + font-family: var(--font-primary); + font-size: 13.5px; + line-height: 1em; +} + +.tickText { + order: 1 !important; +} +.tickInput { + order: 2 !important; + width: 55px !important; +} +.spaceText { + order: 3 !important; +} +.spaceInput { + order: 4 !important; + width: 55px !important; +} + +/** Custom DF tilt**/ + +div.customDF { + display: flex; + align-items: flex-start; + box-sizing: border-box; + padding: 16px; + padding-left: 5px; + max-height: 68px; + overflow-x: auto; + overflow-y: hidden +} + +div.customDF>span { + margin-right: 10px; + padding-left: 10px; + padding-top: 12px; + color: var(--background-color-contrast-more); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + white-space: nowrap +} + +div.customDF .helptip { + display: none +} + +div.customDF>div { + position: relative; + display: flex; + align-items: center; + padding: 0 10px 0 0; +} + +div.customDF>button+div { + margin-left: 6px +} + +div.customDF>div>input, +.colorStylePicker input { + order: 2; + box-sizing: border-box; + width: 70px; + height: 36px; + padding: 10px 0; + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-right: none; + border-left: none; + border-radius: 0; + outline: none; + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: left; + padding-left: 11px +} + +.colorStylePicker input { + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + height: 35.6px; +} + +div.customDF>div:after{ + order: 3; + content: ''; + box-sizing: border-box; + display: block; + width: 6px; + height: 36px; + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-left: none; + border-radius: 0 6px 6px 0 +} + +div.customDF>div>span, +.colorStylePicker .right-side>.row>span { + order: 1; + padding: 11px 16px; + background-color: var(--background-color)!important; + border: 1px solid var(--background-color-contrast-more); + border-right: none; + border-radius: 6px 0 0 6px; + color: var(--accent-color-contrast); + font-weight: 400; + font-size: 12px; + line-height: 1em; + white-space: nowrap +} + +div.customDF>div.selected>span { + background-color: var(--accent-color)!important; + border-color: var(--accent-color); + color: var(--font-color-secondary) +} + +div.customDF>button, +.colorStylePicker button { + padding: 11px 16px; + + background-color: var(--background-color) !important; + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + + color: var(--accent-color-contrast); + font-weight: 400; + font-size: 12px; + line-height: 1em; + + white-space: nowrap; + cursor: pointer; +} +.colorStylePicker button { + order: 3; + margin-left: 7px; + height: 35.6px; +} + +div.customDF>button:active, +.colorStylePicker button:active { + box-sizing: border-box; + background-color: var(--accent-color) !important; + border-color: var(--accent-color) !important; + + color: var(--font-color-secondary); +} + +div.customDF>button.selected, +.colorStylePicker button.selected { + background-color: var(--accent-color)!important; + border-color: var(--accent-color); + color: var(--font-color-secondary) +} + +.colorStylePicker select { + box-sizing: border-box; + width: 120px; + height: 36px; + padding: 8px 6px 8px 0px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: center; +} + +/** extra panel **/ + +div.extra-panel { + flex-direction: column; + overflow: auto; +} + +div.extra-panel > div { + margin: 0 0 1em 0; +} + +div.extra-panel h5 { + margin: 0 0 1em 0; + color: var(--accent-color-contrast); +} + +div.extra-panel span { + color: var(--background-color-contrast-more); +} + +div.extra-panel > div.extra-eq > div.select-eq-phone, +div.extra-panel > div.extra-eq > div.filters, +div.extra-panel > div.settings-row, +div.extra-panel > div.eq-demo { + margin: 0 0 0.5em 0; +} + +div.extra-panel > div.extra-eq > div.select-eq-phone > select { + width: 100%; + z-index: 1; + outline: none; + + display: grid; + grid-template-areas: "select"; + align-items: center; + position: relative; + + border: 1px solid var(--background-color-contrast-more); + border-radius: 0.25em; + padding: 0.25em 0.5em; + + cursor: pointer; + color: var(--accent-color-contrast); + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-repeat: no-repeat, repeat; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='5'%3E%3Cpath d='M0 0l5 4.998L10 0z' fill='%238c8c8c'/%3E%3C/svg%3E"); + background-color: var(--background-color-graph) !important; + background-position: right 0.7em top 50%, 0 0; + background-size: 0.65em auto, 100%; +} + +div.extra-panel > div.extra-eq > div.filters-header > span, +div.extra-panel > div.extra-eq > div.filters > div.filter > span { + width: 25%; + display: inline-block; +} + +div.extra-panel > div.extra-eq > div.filters-header, +div.extra-panel > div.extra-eq > div.filters > div.filter { + box-sizing: border-box; + display: flex; + gap: 7px; +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type='checkbox'] { + margin: 0; + padding: 0 !important; + appearance: none; + background-color: var(--background-color) !important; + font: inherit; + width: 1.05em; + height: 1.05em; + border: 1px solid var(--background-color-contrast-more); + border-radius: 0.25em; + transform: translateY(-0.075em); + cursor: pointer; + vertical-align: middle; + place-content: center; +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type="checkbox"]::before { + content: ""; + width: 0.65em; + height: 0.65em; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + background-color: var(--accent-color); + transform: scale(0); + transition: transform 0.2s ease-in-out; + transform-origin: bottom left; + display: block; + margin: 2px auto; +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type="checkbox"]:checked::before { + transform: scale(1); +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > select, +div.extra-eq > div.eq-demo > select { + width: 70%; + z-index: 1; + outline: none; + margin-top: 2px; + cursor: pointer; + border: 1px solid var(--background-color-contrast-more); + border-radius: 0.25em; + padding: 0.3em 0.5em; + + color: var(--accent-color-contrast); + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-repeat: no-repeat, repeat; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='5'%3E%3Cpath d='M0 0l5 4.998L10 0z' fill='%238c8c8c'/%3E%3C/svg%3E"); + background-color: var(--background-color-graph) !important; + background-position: right 0.7em top 50%, 0 0; + background-size: 0.65em auto, 100%; +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input, +div.extra-panel div.settings-row > span > input { + width: 100%; + box-sizing: border-box; + border: 1px solid var(--background-color-contrast-more); + border-radius: 0.25em; + padding: 0.3em 0.5em; + color: var(--accent-color-contrast); + background-color: var(--background-color-graph) !important; +} + +div.extra-panel > div.extra-eq > div.filters > div.filter > span > input { + margin-top: 2px; +} + +span:has(input[name="freq"]), +span:has(input[name="gain"]), +span:has(input[name="autoeq-from"]), +span:has(input[name="autoeq-to"]), +span:has(input[name="autoeq-gain-from"]), +span:has(input[name="autoeq-gain-to"]), +span:has(input[name="tone-generator-from"]), +span:has(input[name="tone-generator-to"]), +span:has(input[name="vol-left"]), +span:has(input[name="vol-right"]) { + position: relative; +} + +span:has(input[name="freq"]):after, +span:has(input[name="gain"]):after, +span:has(input[name="autoeq-from"]):after, +span:has(input[name="autoeq-to"]):after, +span:has(input[name="autoeq-gain-from"]):after, +span:has(input[name="autoeq-gain-to"]):after, +span:has(input[name="tone-generator-from"]):after, +span:has(input[name="tone-generator-to"]):after, +span:has(input[name="vol-left"]):after, +span:has(input[name="vol-right"]):after { + position: absolute; + top: 3px; + right: 0px; + + content: 'Hz'; + + box-sizing: border-box; + padding: 6px 10px 6px 0; + + border-left: none; + border-radius: 0 6px 6px 0; + + font-family: var(--font-secondary); + font-size: 11px; + line-height: 11px; + + pointer-events: none; +} + +span:has(input[name="autoeq-from"]):after, +span:has(input[name="autoeq-to"]):after, +span:has(input[name="autoeq-gain-from"]):after, +span:has(input[name="autoeq-gain-to"]):after, +span:has(input[name="tone-generator-from"]):after, +span:has(input[name="tone-generator-to"]):after, +span:has(input[name="vol-left"]):after, +span:has(input[name="vol-right"]):after { + top: 1px; +} + +span:has(input[name="gain"]):after, +span:has(input[name="autoeq-gain-from"]):after, +span:has(input[name="autoeq-gain-to"]):after, +span:has(input[name="vol-left"]):after, +span:has(input[name="vol-right"]):after { + content: 'dB'; +} + +div.extra-panel div.settings-row, +div.extra-panel div.auto-eq-button, +div.extra-panel div.filters-button, +div.extra-panel div.extra-upload-buttons { + width: 100%; + margin: 4px 0 0 0; + box-sizing: border-box; + display: flex; + gap: 7px; +} + +div.extra-panel h3, +div.extra-panel h4 { + color: var(--font-color-inputs); + margin-bottom: 4px; +} + +div.extra-panel div.extra-upload-buttons > button { + width: 33.3%; +} + +div.extra-panel div.settings-row > span { + width: 25%; + align-self: center; +} + +div.extra-panel div.settings-row > span[name="title"] { + width: 50%; +} + +div.extra-panel div.filters-button > span[class="eqopts"] { + width: 25%; + display: inline-block; +} + +div.extra-panel div.filters-button > span[class="eqopts"] > button { + width: 100%; + padding: 0.7em; +} + +div.extra-panel div.filters-button > span[class="eqopts"] > button.add-filter, +div.extra-panel div.filters-button > span[class="eqopts"] > button.remove-filter { + width: calc(50% - 3.5px); +} + +div.extra-panel div.auto-eq-button > button { + width: 50%; + padding: 0.9em; +} + +div.extra-panel div.exports > button { + width: 100%; + padding: 0.9em; +} + +div.extra-eq-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.7); + color: #fff; + text-align: center; + padding: calc(50vh - 1em) 0; + font-weight: bold; +} + +div.extra-panel button { + margin-bottom: 4px !important; +} + +div.extra-eq > div.eq-demo { + margin: 0px 0 6px 0; + display: flex; + box-sizing: border-box; + gap: 7px; + width: 100%; + align-items: center; +} + +div.extra-eq > div.eq-demo > select { + width: 25% !important; +} + +div.extra-eq > div.eq-demo > button { + width: 25% !important; + margin: 0 !important; +} + +div.extra-eq > div.eq-demo > span { + text-align: center; +} + +div.extra-eq > div.eq-demo > input[name="demo-time"], +div.extra-eq > div.eq-demo > input[name="tone-generator-freq"], +div.settings-row > input[name="balance-vol"] { + width: 60% !important; + margin: 0; + border: 1px solid var(--background-color-contrast-more); + border-radius: 1em; + accent-color: var(--accent-color-contrast); + background-color: var(--background-color-graph) !important; +} + +div.extra-eq > div.eq-demo > input[name="tone-generator-freq"] { + width: 65% !important; +} + +div.settings-row > input[name="balance-vol"] { + width: 100% !important; + margin:auto; +} + +div.extra-eq > div.eq-demo > span[name="current-freq"] { + width: 35% !important; + text-align: left; +} + +div.extra-panel div.filters-button > button[class="pink-noise selected"] { + background-color: var(--accent-color) !important; + border-color: var(--accent-color) !important; +} + +.volume-button { + position: relative; + display: inline-block; + cursor: pointer; +} + +.volume-button:hover .volume-slider { + display: block; +} + +.volume-slider { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + display: none; + width: 100px; + background-color: transparent; + border: transparent; + padding: 5px; + border-radius: 5px; + color: #fff; + accent-color: var(--accent-color-contrast); +} + +.volume-slider input[type="range"] { + width: 100%; + background: var(--background-color-graph); + border: 1px solid var(--background-color-contrast-more); + border-radius: 1em; + outline: none; +} + +.volume-slider input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 15px; + height: 15px; + background: var(--accent-color-contrast); + border-radius: 50%; + cursor: pointer; +} + +/* Style the volume icon */ +.volume-icon { + color: var(--accent-color-contrast) !important; + fill: var(--accent-color-contrast) !important; +} + +/** separate target comp **/ +tbody.curves > tr > td.comp { + order: 3; + + padding: 0 0 0 13px; +} + +tbody.curves > tr > td.comp select { + box-sizing: border-box; + width: 120px; + height: 36px; + padding: 8px 6px 8px 0px; + + background-color: var(--background-color-inputs); + border: 1px solid var(--background-color-contrast-more); + border-radius: 6px; + outline: none; + + color: var(--font-color-inputs); + font-family: var(--font-secondary); + font-size: 11px; + line-height: 1em; + text-transform: uppercase; + text-align: center; +} + +@media ( max-width: 1200px ) { + tbody.curves > tr > td.comp { + margin-left: auto; + } +} + +@media ( max-width: 500px) { + tbody.curves > tr:before { + order: 5; + + content:''; + display: block; + flex: 100% 1 1; + + height: 0px; + } + + tbody.curves > tr > td.channels { + order: 3; + } + + tbody.curves > tr > td.levels { + order: 4; + + margin-right: 0px !important; + } + + tbody.curves > tr > td.comp { + order: 4 !important; + + margin-right: 16px; + } + + tbody.curves > tr > td.button-baseline { + margin-left: auto; + } + + tbody.curves > tr > td.button-baseline, + tbody.curves > tr > td.hideIcon, + tbody.curves > tr > td.button-pin { + order: 5; + } +} + +tbody.curves > tr > td.remove { + order: 7 !important; +} + +:root { + --icon-save: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M16 11h5l-9 10-9-10h5v-11h8v11zm1 11h-10v2h10v-2z'/%3E%3C/svg%3E"); +} + +tbody.curves > tr > td.button-saveSquig { + order: 6; +} + +tbody.curves > tr > td.button.button-saveSquig { + mask: var(--icon-save); + -webkit-mask: var(--icon-save); + mask-size: 15px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 15px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; +} + +tbody.curves > tr > td.button-saveSquig:before { + background-color: var(--background-color-contrast); + border-color: transparent; +} + +tbody.curves > tr > td.button-saveSquig:after { + background-color: var(--font-color-primary) !important; +} + +/** color and line weight / line dash style menu **/ +.line-style-menu { + display: flex; + align-items: center; + gap: 10px; + display: inline-block; +} + +.line-style-menu-content { + display: none; + position: absolute; + background-color: var(--background-color); + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.line-style-menu-content .style-button { + color: var(--font-color-primary); + padding: 12px 16px; + text-decoration: none; + display: block; + cursor: pointer; +} + +.line-style-menu-content input[type="squig_color"] { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + border: none; + cursor: pointer; +} \ No newline at end of file diff --git a/assets/css/reinvented-color-wheel.min.css b/assets/css/reinvented-color-wheel.min.css new file mode 100644 index 0000000..fc7d52f --- /dev/null +++ b/assets/css/reinvented-color-wheel.min.css @@ -0,0 +1 @@ +.reinvented-color-wheel,.reinvented-color-wheel--hue-handle,.reinvented-color-wheel--hue-wheel,.reinvented-color-wheel--sv-handle,.reinvented-color-wheel--sv-space{touch-action:manipulation;touch-action:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.reinvented-color-wheel{position:relative;display:inline-block;line-height:0;border-radius:50%}.reinvented-color-wheel--hue-wheel{border-radius:50%}.reinvented-color-wheel--sv-space{position:absolute;left:0;top:0;right:0;bottom:0;margin:auto}.reinvented-color-wheel--hue-handle,.reinvented-color-wheel--sv-handle{position:absolute;box-sizing:border-box;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px #000 inset}.reinvented-color-wheel--hue-handle{pointer-events:none} \ No newline at end of file diff --git a/style-alt-theme.css b/assets/css/style-alt-theme.css similarity index 85% rename from style-alt-theme.css rename to assets/css/style-alt-theme.css index 9701285..688eab8 100644 --- a/style-alt-theme.css +++ b/assets/css/style-alt-theme.css @@ -1,7 +1,7 @@ /* Primary color theme */ :root { - --accent-color: hsl(210, 70%, 40%); + --accent-color: hsl(0, 0%, 0%); --accent-color-contrast: hsl(0, 0%, 55%); --background-color: hsl(0, 0%, 96%); @@ -86,3 +86,24 @@ body.theme-contrast { --logo-filter: invert(1.0); --svg-filter: none; } + +/***** Squiglink site address *****/ + +image.wm-squiglink-logo { +} + +text.wm-squiglink-address { + color: var(--font-color-primary); + color: magenta; + font-family: var(--font-primary); + font-weight: 700; + font-size: 14px; + + filter: var(--svg-filter); +} + +g.curves-g path.target + path.target { + stroke-dasharray: 20, 10 !important; + stroke-width: 2.5; + opacity: 0.4; +} \ No newline at end of file diff --git a/style-alt.css b/assets/css/style-alt.css similarity index 90% rename from style-alt.css rename to assets/css/style-alt.css index bfd4d0d..28a9303 100644 --- a/style-alt.css +++ b/assets/css/style-alt.css @@ -36,10 +36,11 @@ Icons *****/ /***** https://yoksel.github.io/url-encoder/ *****/ :root { + --icon-90: url("data:image/svg+xml,%3Csvg%20%20xmlns=%22http://www.w3.org/2000/svg%22%20%20width=%2224%22%20%20height=%2224%22%20%20viewBox=%220%200%2024%2024%22%20%20fill=%22none%22%20%20stroke=%22currentColor%22%20%20stroke-width=%221.75%22%20%20stroke-linecap=%22round%22%20%20stroke-linejoin=%22round%22%20%20class=%22icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-number-90-small%22%3E%3Cpath%20stroke=%22none%22%20d=%22M0%200h24v24H0z%22%20fill=%22none%22/%3E%3Cpath%20d=%22M14%2010v4a2%202%200%201%200%204%200v-4a2%202%200%201%200%20-4%200%22%20/%3E%3Cpath%20d=%22M6%2015a1%201%200%200%200%201%201h2a1%201%200%200%200%201%20-1v-6a1%201%200%200%200%20-1%20-1h-2a1%201%200%200%200%20-1%201v2a1%201%200%200%200%201%201h3%22%20/%3E%3C/svg%3E"); + --icon-remove: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M95.36,24.64h0a5,5,0,0,0-7.08,0L60,52.93,31.72,24.64a5,5,0,0,0-7.08,0h0a5,5,0,0,0,0,7.08L52.93,60,24.64,88.28a5,5,0,0,0,0,7.08h0a5,5,0,0,0,7.08,0L60,67.07,88.28,95.36a5,5,0,0,0,7.08,0h0a5,5,0,0,0,0-7.08L67.07,60,95.36,31.72A5,5,0,0,0,95.36,24.64Z'/%3E%3C/svg%3E"); --icon-baseline: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Crect x='10' y='55' width='100' height='10' rx='5'/%3E%3C/svg%3E"); --icon-squiggle: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M80.37,85c-12.63,0-18.92-6.89-22-12.67A30.8,30.8,0,0,1,55,60.18V60a20.12,20.12,0,0,0-2.1-8c-2.46-4.7-6.68-7-12.9-7s-10.44,2.28-12.9,7A20.1,20.1,0,0,0,25,60a5,5,0,0,1-5,5h0a5,5,0,0,1-5-5c0-8.65,5.22-25,25-25S65,51.2,65,59.87C65.1,61.67,66.3,75,80.37,75c6,0,10.16-2.27,12.56-7A20.49,20.49,0,0,0,95,60a5,5,0,0,1,5-5h0a5,5,0,0,1,5,5,30,30,0,0,1-3,12.2C98,80.46,90.29,85,80.37,85Z'/%3E%3C/svg%3E"); - --icon-download: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 14L11.2929 14.7071L12 15.4142L12.7071 14.7071L12 14ZM13 5C13 4.44772 12.5523 4 12 4C11.4477 4 11 4.44771 11 5L13 5ZM6.29289 9.70711L11.2929 14.7071L12.7071 13.2929L7.70711 8.29289L6.29289 9.70711ZM12.7071 14.7071L17.7071 9.70711L16.2929 8.29289L11.2929 13.2929L12.7071 14.7071ZM13 14L13 5L11 5L11 14L13 14Z' fill='%23CCD2E3'/%3E%3Cpath d='M5 16L5 17C5 18.1046 5.89543 19 7 19L17 19C18.1046 19 19 18.1046 19 17V16' stroke='%23CCD2E3' stroke-width='2'/%3E%3C/svg%3E%0A"); --icon-hide: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M60,30C32.39,30,10,43.43,10,60S32.39,90,60,90s50-13.43,50-30S87.61,30,60,30ZM90.21,72.64C82.41,77.32,71.4,80,60,80,37.11,80,20,69.44,20,60c0-4.3,3.57-8.91,9.79-12.64C37.59,42.68,48.6,40,60,40c22.89,0,40,10.56,40,20C100,64.3,96.43,68.91,90.21,72.64Z'/%3E%3Ccircle class='cls-1' cx='60' cy='60' r='10'/%3E%3C/svg%3E"); --icon-pin: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M78.07,30.48A28.68,28.68,0,0,1,89.52,41.93l-2.33-.64L83.49,45,69.05,59.43l-4.78,4.78,3.27,5.93c2.61,4.74,2.9,7.76,2.79,8.93a3,3,0,0,1-.45,0c-2.64,0-7.89-1.7-14-6.48l-4.75-3.74-3.74-4.75c-5.55-7.09-6.65-12.6-6.48-14.45l.46,0c1,0,3.89.28,8.49,2.81l5.93,3.27L60.57,51,75,36.51l3.71-3.7-.64-2.33M74.41,20.27A6.34,6.34,0,0,0,69.77,22c-1.79,1.79-2.08,4.76-1.13,8.2L54.21,44.58C49.57,42,45.1,40.65,41.37,40.65a9.54,9.54,0,0,0-7,2.51c-5,5-2.27,16.09,5.9,26.51L23,95.58a1,1,0,0,0,.83,1.55,1,1,0,0,0,.55-.17L50.33,79.69c6.87,5.38,14,8.4,19.55,8.4a9.52,9.52,0,0,0,7-2.5c3.93-3.93,3.08-11.62-1.42-19.8L89.85,51.36a13.62,13.62,0,0,0,3.58.53,6.32,6.32,0,0,0,4.62-1.66C102,46.32,98.79,36.83,91,29c-5.54-5.54-11.93-8.75-16.57-8.75Z'/%3E%3C/svg%3E"); --icon-plus: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 120'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23000000;%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M110,60h0a5,5,0,0,0-5-5H65V15a5,5,0,0,0-5-5h0a5,5,0,0,0-5,5V55H15a5,5,0,0,0-5,5h0a5,5,0,0,0,5,5H55v40a5,5,0,0,0,5,5h0a5,5,0,0,0,5-5V65h40A5,5,0,0,0,110,60Z'/%3E%3C/svg%3E"); @@ -381,6 +382,14 @@ div.logo span { color: var(--header-links-color); } +select.language-selector { + border-radius: 4px; + background-color: #000000; + color: #ffffff; + padding: 7px; + margin-right: 10px; +} + ul.header-links { display: flex; justify-content: flex-start; @@ -415,6 +424,10 @@ ul.header-links li { white-space: nowrap; } +ul.header-links li + li { + margin-left: 32px; +} + ul.header-links a { display: flex; align-items: center; @@ -677,12 +690,14 @@ div.zoom { div.zoom > span { margin-right: 10px; + min-width: max-content; color: var(--background-color-contrast-more); font-family: var(--font-secondary); font-size: 11px; line-height: 1em; text-transform: uppercase; + white-space: nowrap; } div.zoom button { @@ -741,12 +756,14 @@ div.normalize { div.normalize > span { margin-right: 10px; + min-width: max-content; color: var(--background-color-contrast-more); font-family: var(--font-secondary); font-size: 11px; line-height: 1em; text-transform: uppercase; + white-space: nowrap; } div.normalize .helptip { @@ -840,12 +857,14 @@ div.smooth { div.smooth > span { margin-right: 10px; + min-width: max-content; color: var(--background-color-contrast-more); font-family: var(--font-secondary); font-size: 11px; line-height: 1em; text-transform: uppercase; + white-space: nowrap; } div.smooth input { @@ -899,7 +918,6 @@ div.miscTools button, div.extra-panel button { order: 1; - flex: auto 1 1; padding: 11px 8px; margin: 0; @@ -919,71 +937,6 @@ div.extra-panel button { outline: none; } -div.filters-button { - display: flex; - flex-wrap: wrap; - gap: 0 4px; -} - -div.extra-panel button.sort-filters, -div.extra-panel button.readme { - flex: calc(50% - 2px) 0 0; - border-color: var(--background-color-contrast) !important; -} - -div.extra-panel button.import-filters, -div.extra-panel button.export-filters { - position: relative; - - flex: 100% 0 0; -} - -button.export-graphic-filters { - border-color: var(--background-color-contrast) !important; -} - -button.export-filters:after { - content: ''; - - position: absolute; - top: 0; - right: 0; - content: ''; - box-sizing: border-box; - display: block; - width: 34px; - height: 34px; - background-color: var(--background-color-contrast-more); - pointer-events: none; - - mask: var(--icon-download); - -webkit-mask: var(--icon-download); - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; -} - -div.settings-row { - display: flex; - align-items: center; - gap: 4px; -} - -div.settings-row > span { - flex: calc(25% - 4px) 0 0; -} - -div.settings-row > span:nth-child(1) { - flex: auto 1 1; - - font-size: 12px; - line-height: 1em; - text-align: left; -} - div.miscTools button span { display: none; } @@ -1031,11 +984,6 @@ svg#expandTools path { display: none; } -button#theme { - justify-content: center; - min-width: 120px; -} - body.dark-mode div.miscTools button#theme, div.miscTools button#theme:active { background-color: var(--accent-color) !important; @@ -1044,259 +992,7 @@ div.miscTools button#theme:active { color: var(--font-color-secondary); } -/** extra panel **/ - -div.extra-panel { - flex-direction: column; - overflow: auto; -} - -div.extra-panel > div { - margin: 0 0 1em 0; -} - -div.extra-panel h5 { - margin: 16px 0 8px 0; - - font-size: 14px; - line-height: 1em; - color: var(--accent-color-contrast); -} - -div.extra-upload { - display: flex; - flex-wrap: wrap; -} - -div.extra-upload h5 { - flex: 100% 0 0; -} - -div.extra-upload button { - flex: calc(50% - 2px) 1 1; -} - -div.extra-upload button + button { - margin-left: 4px; -} - -div.extra-upload span { - order: 2; - flex: 100% 0 0; -} - -div.extra-panel span { - color: var(--background-color-contrast-more); -} - -div.extra-panel select { - position: relative; - - padding: 10px 0; - margin: 0; - - background-color: transparent; - border: none; - - color: var(--font-color-primary); - font-family: var(--font-family-secondary); - font-size: 11px; - line-height: 1em; - - cursor: pointer; - outline: none; - - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-appearance: none; - -moz-appearance: none; -} - -div.extra-panel select { - background-image: - linear-gradient(45deg, transparent 50%, var(--background-color-contrast-more) 50%), - linear-gradient(135deg, var(--background-color-contrast-more) 50%, transparent 50%); - background-position: - calc(100% - 5px) calc(1em + 2px), - calc(100% - 0px) calc(1em + 2px); - background-size: - 5px 5px, - 5px 5px; - background-repeat: no-repeat; -} - -div.extra-panel select[name="phone"] { - background-position: - calc(100% - 14px) calc(1em + 3px), - calc(100% - 9px) calc(1em + 3px); -} - -div.extra-panel > div.extra-eq > div.select-eq-phone, -div.extra-panel > div.extra-eq > div.filters, -div.extra-panel div.settings-row { - margin: 0 0 0.5em 0; -} - -div.extra-panel > div.extra-eq > div.select-eq-phone > select { - box-sizing: border-box; - width: 100%; - padding: 10px; - - border: 1px solid var(--background-color-contrast-more); - border-radius: 6px; -} - -div.extra-panel > div.extra-eq > div.filters-header > span, -div.extra-panel > div.extra-eq > div.filters > div.filter > span { - flex: 25% 1 1; - display: flex; - align-items: center; -} - -div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type='checkbox'] { - width: 20%; - margin: 0; - -webkit-appearance: checkbox; - appearance: checkbox; - - accent-color: var(--background-color-contrast); - - cursor: pointer; -} - -div.extra-panel > div.extra-eq > div.filters > div.filter > span > input[type='checkbox']:checked { -} - -div.extra-panel > div.extra-eq > div.filters > div.filter > span > select { - width: 70%; -} - -div.extra-panel div.settings-row > span { - width: 30%; - display: inline-block; -} - -div.extra-panel > div.extra-eq > div.filters > div.filter > span > input, -div.extra-panel div.settings-row > span > input { - box-sizing: border-box; - width: 100%; - padding: 6px 10px; - - background-color: var(--background-color-inputs); - border: 1px solid var(--background-color-contrast); - border-radius: 6px; - - color: var(--font-color-inputs); - font-family: var(--font-secondary); - font-size: 11px; - line-height: 12px; - - outline: none; -} - -span:has(input[name="freq"]), -span:has(input[name="gain"]), -span:has(input[name="autoeq-to"]), -span:has(input[name="tone-generator-to"]) { - position: relative; -} - -span:has(input[name="freq"]):after, -span:has(input[name="gain"]):after, -span:has(input[name="autoeq-to"]):after, -span:has(input[name="tone-generator-to"]):after { - position: absolute; - top: 1px; - right: 1px; - - content: 'Hz'; - - box-sizing: border-box; - padding: 6px 10px 6px 0; - - border-left: none; - border-radius: 0 6px 6px 0; - - font-family: var(--font-secondary); - font-size: 11px; - line-height: 11px; - - pointer-events: none; -} - -span:has(input[name="gain"]):after { - content: 'dB'; -} - -div.filters-header { - display: flex; - gap: 4px; - margin: 8px 0; - - font-size: 12px; - line-height: 1em; -} - -div.filter { - box-sizing: border-box; - display: flex; - gap: 4px; -} - -div.extra-eq-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1000; - background-color: rgba(0, 0, 0, 0.7); - color: #fff; - text-align: center; - padding: calc(50vh - 1em) 0; - font-weight: bold; -} - -div.extra-panel button { - margin-bottom: 4px !important; -} - -div.extra-panel div:has(> button.play) { - display: flex; - align-items: center; - - margin: 8px 0 0 0; -} - -div.extra-panel button.play { - order: 0; - flex: calc(50% - 2px) 0 0; -} - -div.extra-panel button.play + span { - flex: auto 1 1; - margin: 0 0 0 10px; - - font-size: 12px; - line-height: 1em; -} - -div.extra-panel > div.extra-tone-generator input[name='tone-generator-freq'] { - box-sizing: border-box; - width: 100%; - - background-color: var(--background-color-inputs); - border-radius: 100px; - - accent-color: var(--accent-color-contrast); - - cursor: pointer; - -} - -/***** -Targets styles *****/ +/***** Targets styles *****/ div.targets { display: flex; @@ -1484,7 +1180,7 @@ tbody.curves > tr > td.remove:after { /* Remove item */ tbody.curves > tr > td.remove { - order: 7; + order: 6; margin: 0 16px 0 6px; @@ -1531,16 +1227,16 @@ tbody.curves > tr > td.button.hideIcon:after { -webkit-mask-position: center; } -tbody.curves > tr > td.button.button-export:after { - mask: var(--icon-download); - -webkit-mask: var(--icon-download); - - mask-size: 20px; - mask-repeat: no-repeat; - mask-position: center; - -webkit-mask-size: 20px; - -webkit-mask-repeat: no-repeat; - -webkit-mask-position: center; +tbody.curves > tr > td.button.button-ninety:after { + mask: var(--icon-90); + -webkit-mask: var(--icon-90); + + mask-size: 25px; + mask-repeat: no-repeat; + mask-position: center; + -webkit-mask-size: 25px; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; } tbody.curves > tr > td.button.button-baseline:after { @@ -1583,47 +1279,47 @@ tbody.curves > tr > td.button-baseline:after { background-color: var(--font-color-primary); } -/* Download */ -tbody.curves > tr > td.button-export { +/* Hide + 90% Inclusion*/ +tbody.curves > tr > td.button-ninety { position: relative; - - background-color: transparent; - - order: 6; -} - -tbody.curves > tr > td.button-export:before { - background-color: var(--background-color); -} - -tbody.curves > tr > td.button-export:after { - background-color: var(--background-color-contrast-more); -} + + order: 3; + } -/* Hide */ tbody.curves > tr > td.hideIcon { position: relative; order: 4; } +tbody.curves > tr > td.button-ninety svg, tbody.curves > tr > td.hideIcon svg { display: none; } +tbody.curves > tr > td.button-ninety svg, tbody.curves > tr > td.hideIcon:before { background-color: var(--background-color); border-color: transparent; } +tbody.curves > tr > td.button-ninety:after { + background-color: var(--background-color-contrast-more); +} + tbody.curves > tr > td.hideIcon:after { background-color: var(--font-color-primary); } +tbody.curves > tr > td.button-ninety.selected:before, tbody.curves > tr > td.hideIcon.selected:before { border-color: var(--background-color-contrast); } +tbody.curves > tr > td.button-ninety.selected:after { + background-color: var(--font-color-primary); +} + tbody.curves > tr > td.hideIcon.selected:after { background-color: var(--background-color-contrast-more); } @@ -1757,7 +1453,7 @@ tbody.curves > tr > td.item-line div.variantName[style*="color"] { tbody.curves > tr > td.item-line span.variantPopout { position: relative; - top: 7px !important; + top: auto !important; left: auto !important; box-sizing: border-box; @@ -1812,6 +1508,167 @@ tbody.curves > tr > td.item-line span.variantPopout[style*="display"]:after { display: none; } +/* Variant ordering; pretty verbose because of DOM ordering, works up to 10 variants */ +tbody.curves > tr > td.item-line div.variantName:nth-of-type(1) { + order: 1; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(2) { + order: 3; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(3) { + order: 5; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(4) { + order: 7; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(5) { + order: 9; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(6) { + order: 11; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(7) { + order: 13; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(8) { + order: 15; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(9) { + order: 17; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(10) { + order: 19; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(11) { + order: 21; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(12) { + order: 23; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(13) { + order: 25; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(14) { + order: 27; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(15) { + order: 29; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(16) { + order: 31; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(17) { + order: 33; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(18) { + order: 35; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(19) { + order: 37; +} + +tbody.curves > tr > td.item-line div.variantName:nth-of-type(20) { + order: 39; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(1) { + order: 2; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(2) { + order: 4; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(3) { + order: 6; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(4) { + order: 8; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(5) { + order: 10; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(6) { + order: 12; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(7) { + order: 14; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(8) { + order: 16; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(9) { + order: 18; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(10) { + order: 20; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(11) { + order: 22; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(12) { + order: 24; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(13) { + order: 26; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(14) { + order: 28; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(15) { + order: 30; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(16) { + order: 32; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(17) { + order: 34; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(18) { + order: 36; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(19) { + order: 38; +} + +tbody.curves > tr > td.item-line span.variantPopout:nth-of-type(20) { + order: 40; +} + /* Styles for variantName + variantPopout layout */ div.variant-names { flex: auto 1 1; @@ -3040,15 +2897,16 @@ Responsive styles *****/ } */ + tbody.curves > tr > td.button-ninety.selected:before, tbody.curves > tr > td.hideIcon.selected:before, tbody.curves > tr > td.button-pin:before, - tbody.curves > tr > td.button-export:before, tbody.curves > tr > td.remove:before { border: 1px solid var(--background-color); background-color: var(--background-color-contrast); } tbody.curves > tr > td.button-baseline:before, + tbody.curves > tr > td.button-ninety:before, tbody.curves > tr > td.hideIcon:before, tbody.curves > tr > td.button-pin[data-pinned="true"]:before { background-color: var(--background-color-contrast); @@ -3434,6 +3292,7 @@ Responsive styles *****/ } tbody.curves > tr > td.button-baseline, + tbody.curves > tr > td.button-ninety, tbody.curves > tr > td.hideIcon, tbody.curves > tr > td.button-pin { order: 5; diff --git a/style.css b/assets/css/style.css similarity index 99% rename from style.css rename to assets/css/style.css index 7a64eee..b669086 100644 --- a/style.css +++ b/assets/css/style.css @@ -69,7 +69,6 @@ td.remove { width:1.4em; font-size:120%; padding-left:0.08em; } .calibrate { width:2.5em; } .baselineButton { width:2.5em; } .hideButton { width:2em; } -.button-download { display: none; } .lastColumn { width:2.2em; } .addPhone { height:1.8em; } diff --git a/styles/white.css b/assets/css/white.css similarity index 100% rename from styles/white.css rename to assets/css/white.css diff --git a/assets/images/cropped.png b/assets/images/cropped.png new file mode 100644 index 0000000..5e0f511 Binary files /dev/null and b/assets/images/cropped.png differ diff --git a/assets/images/haruto.png b/assets/images/haruto.png new file mode 100644 index 0000000..1c5fdb4 Binary files /dev/null and b/assets/images/haruto.png differ diff --git a/assets/images/haruto.svg b/assets/images/haruto.svg new file mode 100644 index 0000000..34b197c --- /dev/null +++ b/assets/images/haruto.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/assets/images/squiglink-giggle.svg b/assets/images/squiglink-giggle.svg new file mode 100644 index 0000000..6c000ad --- /dev/null +++ b/assets/images/squiglink-giggle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/transparent.png b/assets/images/transparent.png new file mode 100644 index 0000000..f52dded Binary files /dev/null and b/assets/images/transparent.png differ diff --git a/assets/js/90inclusion.js b/assets/js/90inclusion.js new file mode 100644 index 0000000..0ca52e5 --- /dev/null +++ b/assets/js/90inclusion.js @@ -0,0 +1,63 @@ +function calculatePercentile(values, percentile) { + const sorted = [...values].sort((a, b) => a - b); + const index = Math.ceil((percentile / 100) * sorted.length) - 1; + return sorted[index]; +} + +function calculateInclusionWindowForFrequency(values) { + const lowerPercentile = calculatePercentile(values, 5); // 5th percentile + const upperPercentile = calculatePercentile(values, 95); // 95th percentile + + return { + lower: lowerPercentile, + upper: upperPercentile, + }; +} + +function calculateInclusionWindows(rawChannels) { + const numFrequencies = rawChannels[0].length; // 480 ppo + + const upperBounds = []; + const lowerBounds = []; + + for (let i = 0; i < numFrequencies; i++) { + const frequency = rawChannels[0][i][0]; // assumes all measurements have the same frequencies + const dbValues = rawChannels.map(channel => channel[i][1]); + + const inclusionWindow = calculateInclusionWindowForFrequency(dbValues); + upperBounds.push([frequency, inclusionWindow.upper]); + lowerBounds.push([frequency, inclusionWindow.lower]); + } + + // Prompt user to download bounds + function downloadFile(data, filename) { + const formattedData = data.map(item => item.join(', ')).join('\n'); + const blob = new Blob([formattedData], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + // uncomment one at a time to download the lower and upper bounds, limitations of browser + // downloadFile(lowerBounds, 'lower_bounds.txt'); + // downloadFile(upperBounds, 'upper_bounds.txt'); + return [upperBounds, lowerBounds]; +} + +const inclusionName = " 90% Inclusion"; // edit this to change the name of the inclusion zone + +function setBoundsPhone(p, ch) { + let ninetyPercentInclusion = { + brand: p.brand, dispBrand: p.dispBrand, is90Bounds: true, + phone: p.dispName + inclusionName, fullName: p.fullName + inclusionName, + dispName: p.dispName + inclusionName, fileName: p.fullName + inclusionName, + rawChannels: ch, channels: ch, lr: ch, norm: p.norm, id: -69 + } + + return ninetyPercentInclusion; +} \ No newline at end of file diff --git a/assets/js/confidence_intervals.js b/assets/js/confidence_intervals.js new file mode 100644 index 0000000..30749fb --- /dev/null +++ b/assets/js/confidence_intervals.js @@ -0,0 +1,72 @@ +function calculateMean(values) { + const sum = values.reduce((acc, val) => acc + val, 0); + return sum / values.length; +} + +function calculateStandardDeviation(values, mean) { + const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / values.length; + return Math.sqrt(variance); +} + +function calculateConfidenceIntervalForFrequency(values) { + const mean = calculateMean(values); + const stdev = calculateStandardDeviation(values, mean); + const n = values.length; + + const marginOfError = 1.645 * (stdev / Math.sqrt(n)); + + return { + lower: mean - marginOfError, + upper: mean + marginOfError, + }; +} + +function calculateConfidenceIntervals(rawChannels) { + const numFrequencies = rawChannels[0].length; // 480 ppo + + const upperBounds = []; + const lowerBounds = []; + + for (let i = 0; i < numFrequencies; i++) { + const frequency = rawChannels[0][i][0]; // assumes all measurements have the same frequencies + const dBs = rawChannels.map(measurement => measurement[i][1]); + + const confidenceInterval = calculateConfidenceIntervalForFrequency(dBs); + upperBounds.push([frequency, confidenceInterval.upper]); + lowerBounds.push([frequency, confidenceInterval.lower]); + } + + console.log([upperBounds, lowerBounds]); + + // Prompt user to download upper bounds + function downloadFile(data, filename) { + const formattedData = data.map(item => item.join(', ')).join('\n'); + const blob = new Blob([formattedData], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + // uncomment one at a time to download the lower and upper bounds, limitations of browser + // downloadFile(lowerBounds, 'lower_bounds.txt'); + // downloadFile(upperBounds, 'upper_bounds.txt'); + return [upperBounds, lowerBounds]; +} + + +function setBoundsPhone(p, ch) { + let inclusionName = " 90% Confidence Interval"; // edit this to change the name of the confidence interval + let confidencePhone = { + brand: p.brand, dispBrand: p.dispBrand, is90Bounds: true, + phone: p.dispName + inclusionName, fullName: p.fullName + inclusionName, + dispName: p.dispName + inclusionName, fileName: p.fullName + inclusionName, + rawChannels: ch, channels: ch, lr: ch, norm: p.norm, id: -69 + } + + return confidencePhone; +} diff --git a/config.js b/assets/js/config.js similarity index 50% rename from config.js rename to assets/js/config.js index 1c1cfcc..2422db9 100644 --- a/config.js +++ b/assets/js/config.js @@ -1,10 +1,10 @@ // Configuration options -const init_phones = ["BKF"], // Optional. Which graphs to display on initial load. Note: Share URLs will override this set +const init_phones = ["Haruto 2024 Target", "AudioSense DT200"],// Optional. Which graphs to display on initial load. Note: Share URLs will override this set DIR = "data/", // Directory where graph files are stored default_channels = ["L","R"], // Which channels to display. Avoid javascript errors if loading just one channel per phone default_normalization = "dB", // Sets default graph normalization mode. Accepts "dB" or "Hz" default_norm_db = 60, // Sets default dB normalization point - default_norm_hz = 500, // Sets default Hz normalization point (500Hz is recommended by IEC) + default_norm_hz = 630, // Sets default Hz normalization point (500Hz is recommended by IEC) max_channel_imbalance = 5, // Channel imbalance threshold to show ! in the channel selector alt_layout = true, // Toggle between classic and alt layouts alt_sticky_graph = true, // If active graphs overflows the viewport, does the graph scroll with the page or stick to the viewport? @@ -12,48 +12,74 @@ const init_phones = ["BKF"], // Optional. Which graphs to display on alt_header = true, // Display a configurable header at the top of the alt layout alt_header_new_tab = false, // Clicking alt_header links opens in new tab alt_tutorial = true, // Display a configurable frequency response guide below the graph - alt_augment = false, // Display augment card in phone list, e.g. review sore, shop link - site_url = 'graph.html', // URL of your graph "homepage" + alt_augment = true, // Display augment card in phone list, e.g. review sore, shop link + site_url = '/', // URL of your graph "homepage" share_url = true, // If true, enables shareable URLs watermark_text = "CrinGraph", // Optional. Watermark appears behind graphs - watermark_image_url = "cringraph-logo.svg", // Optional. If image file is in same directory as config, can be just the filename + watermark_image_url = "assets/images/haruto.svg", // Optional. If image file is in same directory as config, can be just the filename rig_description = "clone IEC 711", // Optional. Labels the graph with a description of the rig used to make the measurement, e.g. "clone IEC 711" page_title = "CrinGraph", // Optional. Appended to the page title if share URLs are enabled page_description = "View and compare frequency response graphs for earphones", - accessories = false, // If true, displays specified HTML at the bottom of the page. Configure further below + accessories = true, // If true, displays specified HTML at the bottom of the page. Configure further below externalLinksBar = true, // If true, displays row of pill-shaped links at the bottom of the page. Configure further below - restricted = false, // Enables restricted mode. More restricted options below expandable = false, // Enables button to expand iframe over the top of the parent page expandableOnly = false, // Prevents iframe interactions unless the user has expanded it. Accepts "true" or "false" OR a pixel value; if pixel value, that is used as the maximum width at which expandableOnly is used headerHeight = '0px', // Optional. If expandable=true, determines how much space to leave for the parent page header themingEnabled = true, // Enable user-toggleable themes (dark mode, contrast mode) - targetDashed = false, // If true, makes target curves dashed lines + targetDashed = true, // If true, makes target curves dashed lines targetColorCustom = false, // If false, targets appear as a random gray value. Can replace with a fixed color value to make all targets the specified color, e.g. "black" targetRestoreLastUsed = false, // Restore user's last-used target settings on load - labelsPosition = "default", // Up to four labels will be grouped in a specified corner. Accepts "top-left," bottom-left," "bottom-right," and "default" + labelsPosition = "bottom-left", // Up to four labels will be grouped in a specified corner. Accepts "top-left," bottom-left," "bottom-right," and "default" stickyLabels = true, // "Sticky" labels - analyticsEnabled = true, // Enables Google Analytics 4 measurement of site usage - exportableGraphs = true, // Enables export graph button + analyticsEnabled = true, // Enables Google Analytics 4 measurement of site usage extraEnabled = true, // Enable extra features extraUploadEnabled = true, // Enable upload function extraEQEnabled = true, // Enable parametic eq function extraEQBands = 10, // Default EQ bands available - extraEQBandsMax = 20, // Max EQ bands available - extraToneGeneratorEnabled = true; // Enable tone generator function + extraEQBandsMax = 20; // Max EQ bands available // Specify which targets to display const targets = [ - { type:"Neutral", files:["Diffuse Field","Etymotic","Free Field","Innerfidelity ID"] }, - { type:"Reviewer", files:["Antdroid","Bad Guy","Banbeucmas","Crinacle","Precogvision","Super Review"] }, - { type:"Preference", files:["Harman","Rtings","Sonarworks"] } + { type:"Reference", files:["Haruto 2024","Haruto 2021"] }, + { type:"Neutral", files:["KEMAR DF","IEF Neutral 2023","Etymotic"] }, + { type:"Reviewer", files:["Antdroid","Banbeucmas","HBB","Precogvision","Super Review 22","Timmy","VSG"] }, + { type:"Preference", files:["Harman IE 2019v2","Harman IE 2017v2","AutoEQ","Rtings","Sonarworks"] } ]; - +// Haruto's Addons +const preference_bounds_name = "Bounds", // Preference bounds name + preference_bounds_dir = "assets/pref_bounds/", // Preference bounds directory + preference_bounds_startup = false, // If true, preference bounds are displayed on startup + allowSquigDownload = false, // If true, allows download of measurement data + // PHONE_BOOK = "phone_book.json", // Path to phone book JSON file /* UNCOMMENT THIS IF YOU WANT TO MOVE PHONEBOOK OUTSIDE AGAIN */ + default_y_scale = "40db", // Default Y scale; values: ["20db", "30db", "40db", "50db", "crin"] + default_DF_name = "KEMAR DF", // Default RAW DF name + dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on + default_bass_shelf = 8, // Default Custom DF bass shelf value + default_tilt = -0.8, // Default Custom DF tilt value + default_ear = 0, // Default Custom DF ear gain value + default_treble = 0, // Default Custom DF treble gain value + tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted + compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation + allowCreatorSupport = true; // Allow the creator to have a button top right to support them + + +const harmanFilters = [ + { name: "Harman C1 2024 IE", tilt: -0.9, bass_shelf: 1, ear: 0, treble: 0.5 }, + { name: "Harman C2 2024 IE", tilt: -0.3, bass_shelf: .5, ear: -0.2, treble: 1 }, + { name: "Harman C3 2024 IE", tilt: -2.1, bass_shelf: 0, ear: 0, treble: 10 }, + { name: "Harman C4 2024 IE", tilt: -2.1, bass_shelf: 0, ear: 0.5, treble: 3.7 }, + { name: "Harman 2013 OE", tilt: 0, bass_shelf: 4.8, ear: 0, treble: -4.4 }, + { name: "Harman 2015 OE", tilt: 0, bass_shelf: 6.6, ear: 0, treble: -1.4 }, + { name: "Harman 2018 OE", tilt: 0, bass_shelf: 6.6, ear: -1.8, treble: -3 }, +] // ************************************************************* // Functions to support config options set above; probably don't need to change these // ************************************************************* +// But I will anyways haha - Haruto + // Set up the watermark, based on config options above function watermark(svg) { let wm = svg.append("g") @@ -62,20 +88,30 @@ function watermark(svg) { if ( watermark_image_url ) { wm.append("image") - .attrs({x:-64, y:-64, width:128, height:128, "xlink:href":watermark_image_url}); + .attrs({id:'logo', x:-64, y:-64, width:128, height:128, "xlink:href":watermark_image_url, "class":"graph_logo"}); } if ( watermark_text ) { wm.append("text") - .attrs({x:0, y:70, "font-size":28, "text-anchor":"middle", "class":"graph-name"}) + .attrs({id:'wtext', x:0, y:80, "font-size":28, "text-anchor":"middle", "class":"graph-name"}) .text(watermark_text); } if ( rig_description ) { wm.append("text") - .attrs({x:380, y:-134, "font-size":8, "text-anchor":"end", "class":"rig-description"}) + .attrs({x:380, y:-134, "font-size":8, "text-anchor":"end", "class":"rig-description", "style": "filter: var(--svg-filter);"}) .text("Measured on: " + rig_description); } + + let wmSq = svg.append("g") + .attr("opacity",0.2); + + wmSq.append("image") + .attrs({x:652, y:254, width:100, height:94, "class":"wm-squiglink-logo", "xlink:href":"assets/images/squiglink-giggle.svg"}); + + wmSq.append("text") + .attrs({x:641, y:314, "font-size":10, "transform":"translate(0,0)", "text-anchor":"end", "class":"wm-squiglink-address"}) + .text("squig.link/lab/harutohiroki"); } @@ -102,68 +138,23 @@ function setLayout() { } if ( !alt_layout ) { - applyStylesheet("style.css"); + applyStylesheet("assets/css/style.css"); } else { - applyStylesheet("style-alt.css"); - applyStylesheet("style-alt-theme.css"); + applyStylesheet("assets/css/style-alt.css"); + applyStylesheet("assets/css/style-alt-theme.css"); } } setLayout(); -// Set restricted mode -function setRestricted() { - if ( restricted ) { - max_compare = 2; - restrict_target = false; - disallow_target = true; - premium_html = "

You gonna pay for that?

To use target curves, or more than two graphs, subscribe or upgrade to Patreon Silver tier and switch to the premium tool.

"; - } -} -setRestricted(); - - - // Configure HTML accessories to appear at the bottom of the page. Displayed only if accessories (above) is true // There are a few templates here for ease of use / examples, but these variables accept any HTML const // Short text, center-aligned, useful for a little side info, credits, links to measurement setup, etc. simpleAbout = ` -

This web software is based on the CrinGraph open source software project.

- `, - // Slightly different presentation to make more readable paragraphs. Useful for elaborated methodology, etc. - paragraphs = ` -

Viverra tellus in hac

- -

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Quisque non tellus orci ac. Dictumst quisque sagittis purus sit amet volutpat consequat. Vitae nunc sed velit dignissim sodales ut. Faucibus ornare suspendisse sed nisi lacus sed viverra tellus in. Dignissim enim sit amet venenatis urna cursus eget nunc. Mi proin sed libero enim. Ut sem viverra aliquet eget sit amet. Integer enim neque volutpat ac tincidunt vitae. Tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque. Lacus luctus accumsan tortor posuere ac ut consequat semper. Non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus. Aliquam sem et tortor consequat id. Cursus sit amet dictum sit amet justo donec. Donec adipiscing tristique risus nec feugiat in fermentum posuere.

- -

Diam donec adipiscing tristique risus nec. Amet nisl purus in mollis. Et malesuada fames ac turpis egestas maecenas pharetra. Ante metus dictum at tempor commodo ullamcorper a. Dui id ornare arcu odio ut sem nulla. Ut pharetra sit amet aliquam id diam maecenas. Scelerisque in dictum non consectetur a erat nam at. In ante metus dictum at tempor. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Euismod nisi porta lorem mollis aliquam ut porttitor leo a. Malesuada proin libero nunc consequat interdum. Turpis egestas sed tempus urna et pharetra pharetra massa massa. Quis blandit turpis cursus in hac habitasse. Amet commodo nulla facilisi nullam vehicula ipsum a.

- -

Mauris ultrices eros in cursus turpis massa tincidunt. Aliquam ut porttitor leo a diam sollicitudin. Curabitur vitae nunc sed velit. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Lectus nulla at volutpat diam ut. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Tincidunt id aliquet risus feugiat in ante metus. Elementum nibh tellus molestie nunc non blandit massa enim. Ac tortor vitae purus faucibus ornare suspendisse. Pellentesque sit amet porttitor eget. Commodo quis imperdiet massa tincidunt. Nunc sed id semper risus in hendrerit gravida. Proin nibh nisl condimentum id venenatis a condimentum. Tortor at risus viverra adipiscing at in. Pharetra massa massa ultricies mi quis hendrerit dolor. Tempor id eu nisl nunc mi ipsum faucibus vitae.

- -

Tellus orci

- -

Viverra mauris in aliquam sem. Viverra tellus in hac habitasse platea. Facilisi nullam vehicula ipsum a arcu cursus. Nunc sed augue lacus viverra vitae congue eu. Pretium fusce id velit ut tortor pretium viverra suspendisse. Eu scelerisque felis imperdiet proin. Tincidunt arcu non sodales neque sodales ut etiam sit amet. Tellus at urna condimentum mattis pellentesque. Congue nisi vitae suscipit tellus. Ut morbi tincidunt augue interdum.

- -

Scelerisque in dictum non consectetur a. Elit pellentesque habitant morbi tristique senectus et. Nulla aliquet enim tortor at auctor urna nunc id. In ornare quam viverra orci. Auctor eu augue ut lectus arcu bibendum at varius vel. In cursus turpis massa tincidunt dui ut ornare lectus. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor eu. A diam sollicitudin tempor id. Tellus mauris a diam maecenas sed enim ut sem. Pellentesque id nibh tortor id aliquet lectus proin. Fermentum et sollicitudin ac orci phasellus. Dolor morbi non arcu risus quis. Bibendum enim facilisis gravida neque. Tellus in metus vulputate eu scelerisque felis. Integer malesuada nunc vel risus commodo. Lacus laoreet non curabitur gravida arcu.

- `, - // Customize the count of widget divs, and customize the contents of them. As long as they're wrapped in the widget div, they should auto-wrap and maintain margins between themselves - widgets = ` -
-
- -
- -
- -
- -
- -
-
- `, +

This web software is based on a heavily modified version of the CrinGraph open source software project by HarutoHiroki, with Audio Spectrum's definition source.

+ `; // Which of the above variables to actually insert into the page whichAccessoriesToUse = simpleAbout; @@ -191,21 +182,29 @@ const linkSets = [ url: "https://www.hypethesonics.com/iemdbc/" }, { - name: "In-Ear Fidelity", - url: "https://crinacle.com/graphs/iems/graphtool/" + name: "Hangout.Audio", + url: "https://graph.hangout.audio/" }, { - name: "Precogvision", - url: "https://precog.squig.link/" + name: "HarutoHiroki", + url: "https://graphtool.harutohiroki.com/" }, { - name: "Rikudou Goku", - url: "https://rg.squig.link/" + name: "Precogvision", + url: "https://precog.squig.link/" }, { name: "Super* Review", url: "https://squig.link/" }, + { + name: "Timmy (Gizaudio)", + url: "https://timmyv.squig.link/" + }, + { + name: "Rohsa", + url: "https://rohsa.gitlab.io/graphtool/" + }, ] }, { @@ -219,6 +218,10 @@ const linkSets = [ name: "In-Ear Fidelity", url: "https://crinacle.com/graphs/headphones/graphtool/" }, + { + name: "Listener", + url: "https://listener800.github.io/" + }, { name: "Super* Review", url: "https://squig.link/hp.html" @@ -234,7 +237,7 @@ function setupGraphAnalytics() { if ( analyticsEnabled ) { const pageHead = document.querySelector("head"), graphAnalytics = document.createElement("script"), - graphAnalyticsSrc = "graphAnalytics.js"; + graphAnalyticsSrc = "assets/js/graphAnalytics.js"; graphAnalytics.setAttribute("src", graphAnalyticsSrc); pageHead.append(graphAnalytics); @@ -245,8 +248,8 @@ setupGraphAnalytics(); // If alt_header is enabled, these are the items added to the header -let headerLogoText = "CrinGraph", - headerLogoImgUrl = "", +let headerLogoText = "HarutoHiroki", + headerLogoImgUrl = "assets/images/haruto.svg", headerLinks = [ { name: "Sample", @@ -259,45 +262,46 @@ let headerLogoText = "CrinGraph", } ]; - +// Source: https://www.teachmeaudio.com/mixing/techniques/audio-spectrum let tutorialDefinitions = [ { name: 'Sub bass', - width: '20.1%', - description: 'Lorem ipsum.' + width: '16%', + description: 'The Rumble, usually out of human\'s hearing range and tend to be felt more than heard, providing a sense of power.' }, { - name: 'Mid bass', - width: '19.2%', - description: 'Lorem ipsum.' + name: 'Bass', + width: '20.6%', + description: 'Determins how "fat" or "thin" the sound is, boosting around 250hz tend to add a feeling of warmth. If you\'re a bass head you most likely like this range.' }, { - name: 'Lower midrange', - width: '17.4%', - description: 'Lorem ipsum.' + name: 'Lower Mids', + width: '10.1%', + description: 'Low order harmonics of most instruments, generally viewed as the bass presence range. Boosting a signal around 300 Hz adds clarity to the bass and lower-stringed instruments. Too much boost around 500 Hz can make higher-frequency instruments sound muffled.' }, { - name: 'Upper midrange', + name: 'Midrange', width: "20%", - description: 'Lorem ipsum.' + description: 'The midrange determines how prominent an instrument is in the mix. Boosting around 1000 Hz can give instruments a horn-like quality. Excess output at this range can sound tinny and may cause ear fatigue.' }, { - name: 'Presence region', - width: '6%', - description: 'Lorem ipsum.' + name: 'Upper Mids', + width: "10%", + description: 'The high midrange is responsible for the attack on percussive and rhythm instruments. If boosted, this range can add presence. However, too much boost around the 3 kHz range can cause listening fatigue.' }, { - name: 'Mid treble', - width: '7.3%', - description: 'Lorem ipsum.' + name: 'Presence', + width: '5.9%', + description: 'The Presence range is responsible for the clarity and definition of a sound. Over-boosting can cause an irritating, harsh sound. Cutting in this range makes the sound more distant and transparent.' }, { - name: 'Air', - width: '10%', - description: 'Lorem ipsum.' + name: 'Treble', + width: '17.4%', + description: 'The Treble range is composed entirely of harmonics and is responsible for sparkle and air of a sound. Over boosting in this region can accentuate hiss and cause ear fatigue.' } ] + // Configure paths to extraEQ plugins here let extraEQplugins = [ - //'./devicePEQ/plugin.js' // Path to one or more "extraEQ" plugins -]; + './devicePEQ/plugin.js' // Path to one or more "extraEQ" plugins +]; \ No newline at end of file diff --git a/assets/js/config_hp.js b/assets/js/config_hp.js new file mode 100644 index 0000000..106fc5f --- /dev/null +++ b/assets/js/config_hp.js @@ -0,0 +1,303 @@ +// Configuration options +const init_phones = ["IEF Neutral Target"], // Optional. Which graphs to display on initial load. Note: Share URLs will override this set + DIR = "data_hp/", // Directory where graph files are stored + default_channels = ["L","R"], // Which channels to display. Avoid javascript errors if loading just one channel per phone + default_normalization = "dB", // Sets default graph normalization mode. Accepts "dB" or "Hz" + default_norm_db = 60, // Sets default dB normalization point + default_norm_hz = 1000, // Sets default Hz normalization point (500Hz is recommended by IEC) + max_channel_imbalance = 5, // Channel imbalance threshold to show ! in the channel selector + alt_layout = true, // Toggle between classic and alt layouts + alt_sticky_graph = true, // If active graphs overflows the viewport, does the graph scroll with the page or stick to the viewport? + alt_animated = false, // Determines if new graphs are drawn with a 1-second animation, or appear instantly + alt_header = true, // Display a configurable header at the top of the alt layout + alt_header_new_tab = false, // Clicking alt_header links opens in new tab + alt_tutorial = true, // Display a configurable frequency response guide below the graph + alt_augment = true, // Display augment card in phone list, e.g. review sore, shop link + site_url = '/', // URL of your graph "homepage" + share_url = true, // If true, enables shareable URLs + watermark_text = "CrinGraph", // Optional. Watermark appears behind graphs + watermark_image_url = "assets/images/haruto.svg", // Optional. If image file is in same directory as config, can be just the filename + rig_description = "clone IEC 711", // Optional. Labels the graph with a description of the rig used to make the measurement, e.g. "clone IEC 711" + page_title = "CrinGraph", // Optional. Appended to the page title if share URLs are enabled + page_description = "View and compare frequency response graphs for headphones.", + accessories = true, // If true, displays specified HTML at the bottom of the page. Configure further below + externalLinksBar = true, // If true, displays row of pill-shaped links at the bottom of the page. Configure further below + expandable = false, // Enables button to expand iframe over the top of the parent page + expandableOnly = false, // Prevents iframe interactions unless the user has expanded it. Accepts "true" or "false" OR a pixel value; if pixel value, that is used as the maximum width at which expandableOnly is used + headerHeight = '0px', // Optional. If expandable=true, determines how much space to leave for the parent page header + themingEnabled = true, // Enable user-toggleable themes (dark mode, contrast mode) + targetDashed = true, // If true, makes target curves dashed lines + targetColorCustom = false, // If false, targets appear as a random gray value. Can replace with a fixed color value to make all targets the specified color, e.g. "black" + targetRestoreLastUsed = false, // Restore user's last-used target settings on load + labelsPosition = "bottom-left", // Up to four labels will be grouped in a specified corner. Accepts "top-left," bottom-left," "bottom-right," and "default" + stickyLabels = true, // "Sticky" labels + analyticsEnabled = true, // Enables Google Analytics 4 measurement of site usage + extraEnabled = true, // Enable extra features + extraUploadEnabled = true, // Enable upload function + extraEQEnabled = true, // Enable parametic eq function + extraEQBands = 10, // Default EQ bands available + extraEQBandsMax = 20, // Max EQ bands available + num_samples = 5, // Number of samples to average for smoothing + scale_smoothing = 0.2; // Smoothing factor for scale transitions + +// Specify which targets to display +const targets = [ + { type:"Neutral", files:["KEMAR DF", "IEF Neutral"] }, + { type:"Preference", files:["Harman Combined", "Harman 2018 OE", "Harman 2015 OE", "Harman 2013 OE"] } +]; + +// Haruto's Addons +const preference_bounds_name = "Bounds", // Preference bounds name + preference_bounds_dir = "assets/pref_bounds/", // Preference bounds directory + preference_bounds_startup = false, // If true, preference bounds are displayed on startup + allowSquigDownload = false, // If true, allows download of measurement data + // PHONE_BOOK = "phone_book_hp.json", // Path to phone book JSON file /* UNCOMMENT THIS IF YOU WANT TO MOVE PHONEBOOK OUTSIDE AGAIN */ + default_y_scale = "40db", // Default Y scale; values: ["20db", "30db", "40db", "50db", "crin"] + default_DF_name = "KEMAR DF", // Default RAW DF name + dfBaseline = true, // If true, DF is used as baseline when custom df tilt is on + default_bass_shelf = 8, // Default Custom DF bass shelf value + default_tilt = -0.8, // Default Custom DF tilt value + default_ear = 0, // Default Custom DF ear gain value + default_treble = 0, // Default Custom DF treble gain value + tiltableTargets = ["KEMAR DF"], // Targets that are allowed to be tilted + compTargets = ["KEMAR DF"], // Targets that are allowed to be used for compensation + allowCreatorSupport = true; // Allow the creator to have a button top right to support them + + +const harmanFilters = [ + { name: "Harman 2013 OE", tilt: 0, bass_shelf: 4.8, ear: 0, treble: -4.4 }, + { name: "Harman 2015 OE", tilt: 0, bass_shelf: 6.6, ear: 0, treble: -1.4 }, + { name: "Harman 2018 OE", tilt: 0, bass_shelf: 6.6, ear: -1.8, treble: -3 }, +] + +// ************************************************************* +// Functions to support config options set above; probably don't need to change these +// ************************************************************* + +// But I will anyways haha - Haruto + +// Set up the watermark, based on config options above +function watermark(svg) { + let wm = svg.append("g") + .attr("transform", "translate("+(pad.l+W/2)+","+(pad.t+H/2-20)+")") + .attr("opacity",0.2); + + if ( watermark_image_url ) { + wm.append("image") + .attrs({id:'logo', x:-64, y:-64, width:128, height:128, "xlink:href":watermark_image_url, "class":"graph_logo"}); + } + + if ( watermark_text ) { + wm.append("text") + .attrs({id:'wtext', x:0, y:80, "font-size":28, "text-anchor":"middle", "class":"graph-name"}) + .text(watermark_text); + } + + if ( rig_description ) { + wm.append("text") + .attrs({x:380, y:-134, "font-size":8, "text-anchor":"end", "class":"rig-description", "style": "filter: var(--svg-filter);"}) + .text("Measured on: " + rig_description); + } + + let wmSq = svg.append("g") + .attr("opacity",0.2); + + wmSq.append("image") + .attrs({x:652, y:254, width:100, height:94, "class":"wm-squiglink-logo", "xlink:href":"assets/images/squiglink-giggle.svg"}); + + wmSq.append("text") + .attrs({x:641, y:314, "font-size":10, "transform":"translate(0,0)", "text-anchor":"end", "class":"wm-squiglink-address"}) + .text("squig.link/lab/harutohiroki"); +} + + + +// Parse fr text data from REW or AudioTool format with whatever separator +function tsvParse(fr) { + return fr.split(/[\r\n]/) + .map(l => l.trim()).filter(l => l && l[0] !== '*') + .map(l => l.split(/[\s,]+/).map(e => parseFloat(e)).slice(0, 2)) + .filter(t => !isNaN(t[0]) && !isNaN(t[1])); +} + +// Apply stylesheet based layout options above +function setLayout() { + function applyStylesheet(styleSheet) { + var docHead = document.querySelector("head"), + linkTag = document.createElement("link"); + + linkTag.setAttribute("rel", "stylesheet"); + linkTag.setAttribute("type", "text/css"); + + linkTag.setAttribute("href", styleSheet); + docHead.append(linkTag); + } + + if ( !alt_layout ) { + applyStylesheet("assets/css/style.css"); + } else { + applyStylesheet("assets/css/style-alt.css"); + applyStylesheet("assets/css/style-alt-theme.css"); + } +} +setLayout(); + + + +// Configure HTML accessories to appear at the bottom of the page. Displayed only if accessories (above) is true +// There are a few templates here for ease of use / examples, but these variables accept any HTML +const + // Short text, center-aligned, useful for a little side info, credits, links to measurement setup, etc. + simpleAbout = ` +

This web software is based on a heavily modified version of the CrinGraph open source software project, with Audio Spectrum's definition source.

+ `; + // Which of the above variables to actually insert into the page + whichAccessoriesToUse = simpleAbout; + + + +// Configure external links to appear at the bottom of the page. Displayed only if externalLinksBar (above) is true +const linkSets = [ + { + label: "IEM graph databases", + links: [ + { + name: "Audio Discourse", + url: "https://iems.audiodiscourse.com/" + }, + { + name: "Bad Guy", + url: "https://hbb.squig.link/" + }, + { + name: "Banbeucmas", + url: "https://banbeu.com/graph/tool/" + }, + { + name: "HypetheSonics", + url: "https://www.hypethesonics.com/iemdbc/" + }, + { + name: "Hangout.Audio", + url: "https://graph.hangout.audio/" + }, + { + name: "HarutoHiroki", + url: "https://graphtool.harutohiroki.com/" + }, + { + name: "Precogvision", + url: "https://precog.squig.link/" + }, + { + name: "Super* Review", + url: "https://squig.link/" + }, + { + name: "Timmy (Gizaudio)", + url: "https://timmyv.squig.link/" + }, + { + name: "Rohsa", + url: "https://rohsa.gitlab.io/graphtool/" + }, + ] + }, + { + label: "Headphones", + links: [ + { + name: "Audio Discourse", + url: "https://headphones.audiodiscourse.com/" + }, + { + name: "In-Ear Fidelity", + url: "https://crinacle.com/graphs/headphones/graphtool/" + }, + { + name: "Listener", + url: "https://listener800.github.io/" + }, + { + name: "Super* Review", + url: "https://squig.link/hp.html" + } + ] + } +]; + + + +// Set up analytics +function setupGraphAnalytics() { + if ( analyticsEnabled ) { + const pageHead = document.querySelector("head"), + graphAnalytics = document.createElement("script"), + graphAnalyticsSrc = "assets/js/graphAnalytics.js"; + + graphAnalytics.setAttribute("src", graphAnalyticsSrc); + pageHead.append(graphAnalytics); + } +} +setupGraphAnalytics(); + + + +// If alt_header is enabled, these are the items added to the header +let headerLogoText = "HarutoHiroki", + headerLogoImgUrl = "assets/images/haruto.svg", + headerLinks = [ + { + name: "Sample", + url: "https://sample.com" + }, + { + name: "Sample External", + url: "https://sample.com", + external: true + } +]; + +// Source: https://www.teachmeaudio.com/mixing/techniques/audio-spectrum +let tutorialDefinitions = [ + { + name: 'Sub bass', + width: '16%', + description: 'The Rumble, usually out of human\'s hearing range and tend to be felt more than heard, providing a sense of power.' + }, + { + name: 'Bass', + width: '20.6%', + description: 'Determins how "fat" or "thin" the sound is, boosting around 250hz tend to add a feeling of warmth. If you\'re a bass head you most likely like this range.' + }, + { + name: 'Lower Mids', + width: '10.1%', + description: 'Low order harmonics of most instruments, generally viewed as the bass presence range. Boosting a signal around 300 Hz adds clarity to the bass and lower-stringed instruments. Too much boost around 500 Hz can make higher-frequency instruments sound muffled.' + }, + { + name: 'Midrange', + width: "20%", + description: 'The midrange determines how prominent an instrument is in the mix. Boosting around 1000 Hz can give instruments a horn-like quality. Excess output at this range can sound tinny and may cause ear fatigue.' + }, + { + name: 'Upper Mids', + width: "10%", + description: 'The high midrange is responsible for the attack on percussive and rhythm instruments. If boosted, this range can add presence. However, too much boost around the 3 kHz range can cause listening fatigue.' + }, + { + name: 'Presence', + width: '5.9%', + description: 'The Presence range is responsible for the clarity and definition of a sound. Over-boosting can cause an irritating, harsh sound. Cutting in this range makes the sound more distant and transparent.' + }, + { + name: 'Treble', + width: '17.4%', + description: 'The Treble range is composed entirely of harmonics and is responsible for sparkle and air of a sound. Over boosting in this region can accentuate hiss and cause ear fatigue.' + } +] + +// Configure paths to extraEQ plugins here +let extraEQplugins = [ + './devicePEQ/plugin.js' // Path to one or more "extraEQ" plugins +]; \ No newline at end of file diff --git a/assets/js/d3-selection-multi.v1.min.js b/assets/js/d3-selection-multi.v1.min.js new file mode 100644 index 0000000..fe341ad --- /dev/null +++ b/assets/js/d3-selection-multi.v1.min.js @@ -0,0 +1,2 @@ +// https://github.com/d3/d3-selection-multi Version 1.0.1. Copyright 2017 Mike Bostock. +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(require("d3-selection"),require("d3-transition")):"function"==typeof define&&define.amd?define(["d3-selection","d3-transition"],n):n(t.d3,t.d3)}(this,function(t,n){"use strict";function r(n,r){return n.each(function(){var n=r.apply(this,arguments),e=t.select(this);for(var i in n)e.attr(i,n[i])})}function e(t,n){for(var r in n)t.attr(r,n[r]);return t}function i(n,r,e){return n.each(function(){var n=r.apply(this,arguments),i=t.select(this);for(var o in n)i.style(o,n[o],e)})}function o(t,n,r){for(var e in n)t.style(e,n[e],r);return t}function f(n,r){return n.each(function(){var n=r.apply(this,arguments),e=t.select(this);for(var i in n)e.property(i,n[i])})}function u(t,n){for(var r in n)t.property(r,n[r]);return t}function s(n,r){return n.each(function(){var e=r.apply(this,arguments),i=t.select(this).transition(n);for(var o in e)i.attr(o,e[o])})}function c(t,n){for(var r in n)t.attr(r,n[r]);return t}function a(n,r,e){return n.each(function(){var i=r.apply(this,arguments),o=t.select(this).transition(n);for(var f in i)o.style(f,i[f],e)})}function p(t,n,r){for(var e in n)t.style(e,n[e],r);return t}var l=function(t){return("function"==typeof t?r:e)(this,t)},y=function(t,n){return("function"==typeof t?i:o)(this,t,null==n?"":n)},h=function(t){return("function"==typeof t?f:u)(this,t)},v=function(t){return("function"==typeof t?s:c)(this,t)},d=function(t,n){return("function"==typeof t?a:p)(this,t,null==n?"":n)};t.selection.prototype.attrs=l,t.selection.prototype.styles=y,t.selection.prototype.properties=h,n.transition.prototype.attrs=v,n.transition.prototype.styles=d}); \ No newline at end of file diff --git a/assets/js/d3.v5.min.js b/assets/js/d3.v5.min.js new file mode 100644 index 0000000..344d26c --- /dev/null +++ b/assets/js/d3.v5.min.js @@ -0,0 +1,2 @@ +// https://d3js.org v5.16.0 Copyright 2020 Mike Bostock +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t=t||self).d3=t.d3||{})}(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}var r=e(n),i=r.right,o=r.left;function a(t,n){return[t,n]}function u(t){return null===t?NaN:+t}function c(t,n){var e,r,i=t.length,o=0,a=-1,c=0,f=0;if(null==n)for(;++a1)return f/(o-1)}function f(t,n){var e=c(t,n);return e?Math.sqrt(e):e}function s(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=y?10:o>=_?5:o>=b?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=y?10:o>=_?5:o>=b?2:1)}function w(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=y?i*=10:o>=_?i*=5:o>=b&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),a=+e(t[o],o,t);return a+(+e(t[o+1],o+1,t)-a)*(i-o)}}function T(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function A(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function S(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function k(t){if(!(i=t.length))return[];for(var n=-1,e=S(t,E),r=new Array(e);++n=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function X(t,n){for(var e,r=0,i=t.length;r0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),$.hasOwnProperty(n)?{space:$[n],local:t}:t}function Z(t){var n=W(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===G&&n.documentElement.namespaceURI===G?n.createElement(t):n.createElementNS(e,t)}})(n)}function Q(){}function K(t){return null==t?Q:function(){return this.querySelector(t)}}function J(){return[]}function tt(t){return null==t?J:function(){return this.querySelectorAll(t)}}function nt(t){return function(){return this.matches(t)}}function et(t){return new Array(t.length)}function rt(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}rt.prototype={constructor:rt,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var it="$";function ot(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function ct(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function ft(t,n){return t.style.getPropertyValue(n)||ct(t).getComputedStyle(t,null).getPropertyValue(n)}function st(t){return t.trim().split(/^|\s+/)}function lt(t){return t.classList||new ht(t)}function ht(t){this._node=t,this._names=st(t.getAttribute("class")||"")}function dt(t,n){for(var e=lt(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Mt={};(t.event=null,"undefined"!=typeof document)&&("onmouseenter"in document.documentElement||(Mt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Nt(t,n,e){return t=Tt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function Tt(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=m&&(m=b+1);!(_=g[m])&&++m=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=ut);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):ft(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=st(t+"");if(arguments.length<2){for(var r=lt(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?St:At,null==e&&(e=!1),r=0;r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?gn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?gn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=on.exec(t))?new bn(n[1],n[2],n[3],1):(n=an.exec(t))?new bn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?gn(n[1],n[2],n[3],n[4]):(n=cn.exec(t))?gn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=fn.exec(t))?Mn(n[1],n[2]/100,n[3]/100,1):(n=sn.exec(t))?Mn(n[1],n[2]/100,n[3]/100,n[4]):ln.hasOwnProperty(t)?vn(ln[t]):"transparent"===t?new bn(NaN,NaN,NaN,0):null}function vn(t){return new bn(t>>16&255,t>>8&255,255&t,1)}function gn(t,n,e,r){return r<=0&&(t=n=e=NaN),new bn(t,n,e,r)}function yn(t){return t instanceof Jt||(t=pn(t)),t?new bn((t=t.rgb()).r,t.g,t.b,t.opacity):new bn}function _n(t,n,e,r){return 1===arguments.length?yn(t):new bn(t,n,e,null==r?1:r)}function bn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function mn(){return"#"+wn(this.r)+wn(this.g)+wn(this.b)}function xn(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}function wn(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function Mn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new An(t,n,e,r)}function Nn(t){if(t instanceof An)return new An(t.h,t.s,t.l,t.opacity);if(t instanceof Jt||(t=pn(t)),!t)return new An;if(t instanceof An)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new An(a,u,c,t.opacity)}function Tn(t,n,e,r){return 1===arguments.length?Nn(t):new An(t,n,e,null==r?1:r)}function An(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Sn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}Qt(Jt,pn,{copy:function(t){return Object.assign(new this.constructor,this,t)},displayable:function(){return this.rgb().displayable()},hex:hn,formatHex:hn,formatHsl:function(){return Nn(this).formatHsl()},formatRgb:dn,toString:dn}),Qt(bn,_n,Kt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new bn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new bn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:mn,formatHex:mn,formatRgb:xn,toString:xn})),Qt(An,Tn,Kt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new An(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new An(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new bn(Sn(t>=240?t-240:t+120,i,r),Sn(t,i,r),Sn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"hsl(":"hsla(")+(this.h||0)+", "+100*(this.s||0)+"%, "+100*(this.l||0)+"%"+(1===t?")":", "+t+")")}}));var kn=Math.PI/180,En=180/Math.PI,Cn=.96422,Pn=1,zn=.82521,Rn=4/29,Dn=6/29,qn=3*Dn*Dn,Ln=Dn*Dn*Dn;function Un(t){if(t instanceof Bn)return new Bn(t.l,t.a,t.b,t.opacity);if(t instanceof Vn)return Gn(t);t instanceof bn||(t=yn(t));var n,e,r=Hn(t.r),i=Hn(t.g),o=Hn(t.b),a=Fn((.2225045*r+.7168786*i+.0606169*o)/Pn);return r===i&&i===o?n=e=a:(n=Fn((.4360747*r+.3850649*i+.1430804*o)/Cn),e=Fn((.0139322*r+.0971045*i+.7141733*o)/zn)),new Bn(116*a-16,500*(n-a),200*(a-e),t.opacity)}function On(t,n,e,r){return 1===arguments.length?Un(t):new Bn(t,n,e,null==r?1:r)}function Bn(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Fn(t){return t>Ln?Math.pow(t,1/3):t/qn+Rn}function Yn(t){return t>Dn?t*t*t:qn*(t-Rn)}function In(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Hn(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function jn(t){if(t instanceof Vn)return new Vn(t.h,t.c,t.l,t.opacity);if(t instanceof Bn||(t=Un(t)),0===t.a&&0===t.b)return new Vn(NaN,0=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):ue(isNaN(t)?n:t)}function se(t){return 1==(t=+t)?le:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):ue(isNaN(n)?e:n)}}function le(t,n){var e=n-t;return e?ce(t,e):ue(isNaN(t)?n:t)}Qt(re,ee,Kt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new re(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new re(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*kn,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new bn(255*(n+e*($n*r+Wn*i)),255*(n+e*(Zn*r+Qn*i)),255*(n+e*(Kn*r)),this.opacity)}}));var he=function t(n){var e=se(n);function r(t,n){var r=e((t=_n(t)).r,(n=_n(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=le(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function de(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:me(e,r)})),o=Me.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:me(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:me(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:me(t,e)},{i:u-2,x:me(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(null,t),n=n._next;--tr}function pr(){or=(ir=ur.now())+ar,tr=nr=0;try{dr()}finally{tr=0,function(){var t,n,e=Ke,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:Ke=n);Je=t,gr(r)}(),or=0}}function vr(){var t=ur.now(),n=t-ir;n>rr&&(ar-=n,ir=t)}function gr(t){tr||(nr&&(nr=clearTimeout(nr)),t-or>24?(t<1/0&&(nr=setTimeout(pr,t-ur.now()-ar)),er&&(er=clearInterval(er))):(er||(ir=ur.now(),er=setInterval(vr,rr)),tr=1,cr(pr)))}function yr(t,n,e){var r=new lr;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}lr.prototype=hr.prototype={constructor:lr,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?fr():+e)+(null==n?0:+n),this._next||Je===this||(Je?Je._next=this:Ke=this,Je=this),this._call=t,this._time=e,gr()},stop:function(){this._call&&(this._call=null,this._time=1/0,gr())}};var _r=I("start","end","cancel","interrupt"),br=[],mr=0,xr=1,wr=2,Mr=3,Nr=4,Tr=5,Ar=6;function Sr(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(c){var f,s,l,h;if(e.state!==xr)return u();for(f in i)if((h=i[f]).name===e.name){if(h.state===Mr)return yr(o);h.state===Nr?(h.state=Ar,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fmr)throw new Error("too late; already scheduled");return e}function Er(t,n){var e=Cr(t,n);if(e.state>Mr)throw new Error("too late; already running");return e}function Cr(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Pr(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>wr&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?kr:Er;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=W(t),r="transform"===e?Le:Rr;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttributeNS(t.space,t.local))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttribute(t))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttribute(t)}})(e,r,zr(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttributeNS(t.space,t.local);return a===o?null:a===r?i:i=n(r=a,e)}}:function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttribute(t);return a===o?null:a===r?i:i=n(r=a,e)}})(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=W(t);return this.tween(e,(r.local?function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttributeNS(t.space,t.local,n.call(this,e))}}(t,i)),e}return i._value=n,i}:function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttribute(t,n.call(this,e))}}(t,i)),e}return i._value=n,i})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?qe:Rr;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=ft(this,t),a=(this.style.removeProperty(t),ft(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,qr(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=ft(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=ft(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,zr(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=Er(this,t),f=c.on,s=null==c.value[a]?o||(o=qr(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=ft(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(zr(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Cr(this.node(),e).tween,o=0,a=i.length;o0&&(r=o-P),M<0?d=p-z:M>0&&(u=c-z),x=Mi,B.attr("cursor",Pi.selection),I());break;default:return}xi()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:R&&(g=y=R=!1,I());break;case 18:x===Ti&&(w<0?f=h:w>0&&(r=o),M<0?d=p:M>0&&(u=c),x=Ni,I());break;case 32:x===Mi&&(t.event.altKey?(w&&(f=h-P*w,r=o+P*w),M&&(d=p-z*M,u=c+z*M),x=Ti):(w<0?f=h:w>0&&(r=o),M<0?d=p:M>0&&(u=c),x=Ni),B.attr("cursor",Pi[m]),I());break;default:return}xi()},!0),Ht(t.event.view)}mi(),Pr(b),s.call(b),U.start()}function Y(){var t=D(b);!R||g||y||(Math.abs(t[0]-L[0])>Math.abs(t[1]-L[1])?y=!0:g=!0),L=t,v=!0,xi(),I()}function I(){var t;switch(P=L[0]-q[0],z=L[1]-q[1],x){case Mi:case wi:w&&(P=Math.max(S-r,Math.min(E-f,P)),o=r+P,h=f+P),M&&(z=Math.max(k-u,Math.min(C-d,z)),c=u+z,p=d+z);break;case Ni:w<0?(P=Math.max(S-r,Math.min(E-r,P)),o=r+P,h=f):w>0&&(P=Math.max(S-f,Math.min(E-f,P)),o=r,h=f+P),M<0?(z=Math.max(k-u,Math.min(C-u,z)),c=u+z,p=d):M>0&&(z=Math.max(k-d,Math.min(C-d,z)),c=u,p=d+z);break;case Ti:w&&(o=Math.max(S,Math.min(E,r-P*w)),h=Math.max(S,Math.min(E,f+P*w))),M&&(c=Math.max(k,Math.min(C,u-z*M)),p=Math.max(k,Math.min(C,d+z*M)))}h1e-6)if(Math.abs(s*u-c*f)>1e-6&&i){var h=e-o,d=r-a,p=u*u+c*c,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Qi-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*f)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>f*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,o=!!o;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+f:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-f)>1e-6)&&(this._+="L"+c+","+f),e&&(l<0&&(l=l%Ki+Ki),l>Ji?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=c)+","+(this._y1=f):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Qi)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};function uo(){}function co(t,n){var e=new uo;if(t instanceof uo)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function wo(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function Mo(){}var No=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function To(){var t=1,n=1,e=M,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(_o);else{var r=s(t),i=r[0],a=r[1];n=w(i,a,n),n=g(Math.floor(i/n)*n,Math.floor(a/n)*n,n)}return n.map(function(n){return o(t,n)})}function o(e,i){var o=[],u=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=e[0]>=r,No[f<<1].forEach(p);for(;++o=r,No[c|f<<1].forEach(p);No[f<<0].forEach(p);for(;++u=r,s=e[u*t]>=r,No[f<<1|s<<2].forEach(p);++o=r,l=s,s=e[u*t+o+1]>=r,No[c|f<<1|s<<2|l<<3].forEach(p);No[f|s<<3].forEach(p)}o=-1,s=e[u*t]>=r,No[s<<2].forEach(p);for(;++o=r,No[s<<2|l<<3].forEach(p);function p(t){var n,e,r=[t[0][0]+o,t[0][1]+u],c=[t[1][0]+o,t[1][1]+u],f=a(r),s=a(c);(n=d[f])?(e=h[s])?(delete d[n.end],delete h[e.start],n===e?(n.ring.push(c),i(n.ring)):h[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete d[n.end],n.ring.push(c),d[n.end=s]=n):(n=h[s])?(e=d[f])?(delete h[n.start],delete d[e.end],n===e?(n.ring.push(c),i(n.ring)):h[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete h[n.start],n.ring.unshift(r),h[n.start=f]=n):h[f]=d[s]={start:f,end:s,ring:[r,c]}}No[s<<3].forEach(p)}(e,i,function(t){r(t,e,i),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?o.push([t]):u.push(t)}),u.forEach(function(t){for(var n,e=0,r=o.length;e0&&a0&&u0&&o>0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?bo(yo.call(t)):bo(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:Mo,i):r===u},i}function Ao(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[u-o+a*r]),n.data[u-e+a*r]=c/Math.min(u+1,r-1+o-u,o))}function So(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=c/Math.min(u+1,i-1+o-u,o))}function ko(t){return t[0]}function Eo(t){return t[1]}function Co(){return 1}var Po={},zo={},Ro=34,Do=10,qo=13;function Lo(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+'] || ""'}).join(",")+"}")}function Uo(t){var n=Object.create(null),e=[];return t.forEach(function(t){for(var r in t)r in n||e.push(n[r]=r)}),e}function Oo(t,n){var e=t+"",r=e.length;return r9999?"+"+Oo(t,6):Oo(t,4)}(t.getUTCFullYear())+"-"+Oo(t.getUTCMonth()+1,2)+"-"+Oo(t.getUTCDate(),2)+(i?"T"+Oo(n,2)+":"+Oo(e,2)+":"+Oo(r,2)+"."+Oo(i,3)+"Z":r?"T"+Oo(n,2)+":"+Oo(e,2)+":"+Oo(r,2)+"Z":e||n?"T"+Oo(n,2)+":"+Oo(e,2)+"Z":"")}function Fo(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return zo;if(f)return f=!1,Po;var n,r,i=a;if(t.charCodeAt(i)===Ro){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Do?f=!0:r===qo&&(f=!0,t.charCodeAt(a)===Do&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;a=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function ba(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function ma(t){return t[0]}function xa(t){return t[1]}function wa(t,n,e){var r=new Ma(null==n?ma:n,null==e?xa:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Ma(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Na(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Ta=wa.prototype=Ma.prototype;function Aa(t){return t.x+t.vx}function Sa(t){return t.y+t.vy}function ka(t){return t.index}function Ea(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function Ca(t){return t.x}function Pa(t){return t.y}Ta.copy=function(){var t,n,e=new Ma(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Na(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Na(n));return e},Ta.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return _a(this.cover(n,e),n,e,t)},Ta.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=y)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(c=(v+y)/2))?v=c:y=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},Ta.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function qa(t){return(t=Da(Math.abs(t)))?t[1]:NaN}var La,Ua=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function Oa(t){if(!(n=Ua.exec(t)))throw new Error("invalid format: "+t);var n;return new Ba({fill:n[1],align:n[2],sign:n[3],symbol:n[4],zero:n[5],width:n[6],comma:n[7],precision:n[8]&&n[8].slice(1),trim:n[9],type:n[10]})}function Ba(t){this.fill=void 0===t.fill?" ":t.fill+"",this.align=void 0===t.align?">":t.align+"",this.sign=void 0===t.sign?"-":t.sign+"",this.symbol=void 0===t.symbol?"":t.symbol+"",this.zero=!!t.zero,this.width=void 0===t.width?void 0:+t.width,this.comma=!!t.comma,this.precision=void 0===t.precision?void 0:+t.precision,this.trim=!!t.trim,this.type=void 0===t.type?"":t.type+""}function Fa(t,n){var e=Da(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}Oa.prototype=Ba.prototype,Ba.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(void 0===this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(void 0===this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var Ya={"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Fa(100*t,n)},r:Fa,s:function(t,n){var e=Da(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(La=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Da(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function Ia(t){return t}var Ha,ja=Array.prototype.map,Xa=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Va(t){var n,e,r=void 0===t.grouping||void 0===t.thousands?Ia:(n=ja.call(t.grouping,Number),e=t.thousands+"",function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}),i=void 0===t.currency?"":t.currency[0]+"",o=void 0===t.currency?"":t.currency[1]+"",a=void 0===t.decimal?".":t.decimal+"",u=void 0===t.numerals?Ia:function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(ja.call(t.numerals,String)),c=void 0===t.percent?"%":t.percent+"",f=void 0===t.minus?"-":t.minus+"",s=void 0===t.nan?"NaN":t.nan+"";function l(t){var n=(t=Oa(t)).fill,e=t.align,l=t.sign,h=t.symbol,d=t.zero,p=t.width,v=t.comma,g=t.precision,y=t.trim,_=t.type;"n"===_?(v=!0,_="g"):Ya[_]||(void 0===g&&(g=12),y=!0,_="g"),(d||"0"===n&&"="===e)&&(d=!0,n="0",e="=");var b="$"===h?i:"#"===h&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",m="$"===h?o:/[%p]/.test(_)?c:"",x=Ya[_],w=/[defgprs%]/.test(_);function M(t){var i,o,c,h=b,M=m;if("c"===_)M=x(t)+M,t="";else{var N=(t=+t)<0||1/t<0;if(t=isNaN(t)?s:x(Math.abs(t),g),y&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0&&(i=0)}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),N&&0==+t&&"+"!==l&&(N=!1),h=(N?"("===l?l:f:"-"===l||"("===l?"":l)+h,M=("s"===_?Xa[8+La/3]:"")+M+(N&&"("===l?")":""),w)for(i=-1,o=t.length;++i(c=t.charCodeAt(i))||c>57){M=(46===c?a+t.slice(i+1):t.slice(i))+M,t=t.slice(0,i);break}}v&&!d&&(t=r(t,1/0));var T=h.length+t.length+M.length,A=T>1)+h+t+M+A.slice(T);break;default:t=A+h+t+M}return u(t)}return g=void 0===g?6:/[gprs]/.test(_)?Math.max(1,Math.min(21,g)):Math.max(0,Math.min(20,g)),M.toString=function(){return t+""},M}return{format:l,formatPrefix:function(t,n){var e=l(((t=Oa(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(qa(n)/3))),i=Math.pow(10,-r),o=Xa[8+r/3];return function(t){return e(i*t)+o}}}}function Ga(n){return Ha=Va(n),t.format=Ha.format,t.formatPrefix=Ha.formatPrefix,Ha}function $a(t){return Math.max(0,-qa(Math.abs(t)))}function Wa(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(qa(n)/3)))-qa(Math.abs(t)))}function Za(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,qa(n)-qa(t))+1}function Qa(){return new Ka}function Ka(){this.reset()}Ga({decimal:".",thousands:",",grouping:[3],currency:["$",""],minus:"-"}),Ka.prototype={constructor:Ka,reset:function(){this.s=this.t=0},add:function(t){tu(Ja,t,this.t),tu(this,Ja.s,this.s),this.s?this.t+=Ja.t:this.s=Ja.t},valueOf:function(){return this.s}};var Ja=new Ka;function tu(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}var nu=1e-6,eu=1e-12,ru=Math.PI,iu=ru/2,ou=ru/4,au=2*ru,uu=180/ru,cu=ru/180,fu=Math.abs,su=Math.atan,lu=Math.atan2,hu=Math.cos,du=Math.ceil,pu=Math.exp,vu=Math.log,gu=Math.pow,yu=Math.sin,_u=Math.sign||function(t){return t>0?1:t<0?-1:0},bu=Math.sqrt,mu=Math.tan;function xu(t){return t>1?0:t<-1?ru:Math.acos(t)}function wu(t){return t>1?iu:t<-1?-iu:Math.asin(t)}function Mu(t){return(t=yu(t/2))*t}function Nu(){}function Tu(t,n){t&&Su.hasOwnProperty(t.type)&&Su[t.type](t,n)}var Au={Feature:function(t,n){Tu(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=hu(n=(n*=cu)/2+ou),a=yu(n),u=qu*a,c=Du*o+u*hu(i),f=u*r*yu(i);Lu.add(lu(f,c)),Ru=t,Du=o,qu=a}function Hu(t){return[lu(t[1],t[0]),wu(t[2])]}function ju(t){var n=t[0],e=t[1],r=hu(e);return[r*hu(n),r*yu(n),yu(e)]}function Xu(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Vu(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Gu(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function $u(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function Wu(t){var n=bu(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var Zu,Qu,Ku,Ju,tc,nc,ec,rc,ic,oc,ac,uc,cc,fc,sc,lc,hc,dc,pc,vc,gc,yc,_c,bc,mc,xc,wc=Qa(),Mc={point:Nc,lineStart:Ac,lineEnd:Sc,polygonStart:function(){Mc.point=kc,Mc.lineStart=Ec,Mc.lineEnd=Cc,wc.reset(),Ou.polygonStart()},polygonEnd:function(){Ou.polygonEnd(),Mc.point=Nc,Mc.lineStart=Ac,Mc.lineEnd=Sc,Lu<0?(Zu=-(Ku=180),Qu=-(Ju=90)):wc>nu?Ju=90:wc<-nu&&(Qu=-90),oc[0]=Zu,oc[1]=Ku},sphere:function(){Zu=-(Ku=180),Qu=-(Ju=90)}};function Nc(t,n){ic.push(oc=[Zu=t,Ku=t]),nJu&&(Ju=n)}function Tc(t,n){var e=ju([t*cu,n*cu]);if(rc){var r=Vu(rc,e),i=Vu([r[1],-r[0],0],r);Wu(i),i=Hu(i);var o,a=t-tc,u=a>0?1:-1,c=i[0]*uu*u,f=fu(a)>180;f^(u*tcJu&&(Ju=o):f^(u*tc<(c=(c+360)%360-180)&&cJu&&(Ju=n)),f?tPc(Zu,Ku)&&(Ku=t):Pc(t,Ku)>Pc(Zu,Ku)&&(Zu=t):Ku>=Zu?(tKu&&(Ku=t)):t>tc?Pc(Zu,t)>Pc(Zu,Ku)&&(Ku=t):Pc(t,Ku)>Pc(Zu,Ku)&&(Zu=t)}else ic.push(oc=[Zu=t,Ku=t]);nJu&&(Ju=n),rc=e,tc=t}function Ac(){Mc.point=Tc}function Sc(){oc[0]=Zu,oc[1]=Ku,Mc.point=Nc,rc=null}function kc(t,n){if(rc){var e=t-tc;wc.add(fu(e)>180?e+(e>0?360:-360):e)}else nc=t,ec=n;Ou.point(t,n),Tc(t,n)}function Ec(){Ou.lineStart()}function Cc(){kc(nc,ec),Ou.lineEnd(),fu(wc)>nu&&(Zu=-(Ku=180)),oc[0]=Zu,oc[1]=Ku,rc=null}function Pc(t,n){return(n-=t)<0?n+360:n}function zc(t,n){return t[0]-n[0]}function Rc(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nru?t+Math.round(-t/au)*au:t,n]}function $c(t,n,e){return(t%=au)?n||e?Vc(Zc(t),Qc(n,e)):Zc(t):n||e?Qc(n,e):Gc}function Wc(t){return function(n,e){return[(n+=t)>ru?n-au:n<-ru?n+au:n,e]}}function Zc(t){var n=Wc(t);return n.invert=Wc(-t),n}function Qc(t,n){var e=hu(t),r=yu(t),i=hu(n),o=yu(n);function a(t,n){var a=hu(n),u=hu(t)*a,c=yu(t)*a,f=yu(n),s=f*e+u*r;return[lu(c*i-s*o,u*e-f*r),wu(s*i+c*o)]}return a.invert=function(t,n){var a=hu(n),u=hu(t)*a,c=yu(t)*a,f=yu(n),s=f*i-c*o;return[lu(c*i+f*o,u*e+s*r),wu(s*e-u*r)]},a}function Kc(t){function n(n){return(n=t(n[0]*cu,n[1]*cu))[0]*=uu,n[1]*=uu,n}return t=$c(t[0]*cu,t[1]*cu,t.length>2?t[2]*cu:0),n.invert=function(n){return(n=t.invert(n[0]*cu,n[1]*cu))[0]*=uu,n[1]*=uu,n},n}function Jc(t,n,e,r,i,o){if(e){var a=hu(n),u=yu(n),c=r*e;null==i?(i=n+r*au,o=n-c/2):(i=tf(a,i),o=tf(a,o),(r>0?io)&&(i+=r*au));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function ef(t,n){return fu(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function af(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,T=N*M,A=T>ru,S=v*x;if(uf.add(lu(S*N*yu(T),g*w+S*hu(T))),a+=A?M+N*au:M,A^d>=e^b>=e){var k=Vu(ju(h),ju(_));Wu(k);var E=Vu(o,k);Wu(E);var C=(A^M>=0?-1:1)*wu(E[2]);(r>C||r===C&&(k[0]||k[1]))&&(u+=A^M>=0?1:-1)}}return(a<-nu||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(lf))}return h}}function lf(t){return t.length>1}function hf(t,n){return((t=t.x)[0]<0?t[1]-iu-nu:iu-t[1])-((n=n.x)[0]<0?n[1]-iu-nu:iu-n[1])}var df=sf(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?ru:-ru,c=fu(o-e);fu(c-ru)0?iu:-iu),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=ru&&(fu(e-i)nu?su((yu(n)*(o=hu(r))*yu(e)-yu(r)*(i=hu(n))*yu(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*iu,r.point(-ru,i),r.point(0,i),r.point(ru,i),r.point(ru,0),r.point(ru,-i),r.point(0,-i),r.point(-ru,-i),r.point(-ru,0),r.point(-ru,i);else if(fu(t[0]-n[0])>nu){var o=t[0]0,i=fu(n)>nu;function o(t,e){return hu(t)*hu(e)>n}function a(t,e,r){var i=[1,0,0],o=Vu(ju(t),ju(e)),a=Xu(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=Vu(i,o),h=$u(i,f);Gu(h,$u(o,s));var d=l,p=Xu(h,d),v=Xu(d,d),g=p*p-v*(Xu(h,h)-1);if(!(g<0)){var y=bu(g),_=$u(d,(-p-y)/v);if(Gu(_,h),_=Hu(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(fu(_[0]-m)ru^(m<=_[0]&&_[0]<=x)){var A=$u(d,(-p+y)/v);return Gu(A,h),[_,Hu(A)]}}}function u(n,e){var i=r?t:ru-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return sf(o,function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],v=o(l,h),g=r?v?0:u(l,h):v?u(l+(l<0?ru:-ru),h):0;if(!n&&(f=c=v)&&t.lineStart(),v!==c&&(!(d=a(n,p))||ef(n,d)||ef(p,d))&&(p[0]+=nu,p[1]+=nu,v=o(p[0],p[1])),v!==c)s=0,v?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1]),t.lineEnd()),n=d;else if(i&&n&&r^v){var y;g&e||!(y=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&ef(n,p)||t.point(p[0],p[1]),n=p,c=v,e=g},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}},function(n,r,i,o){Jc(o,t,e,i,n,r)},r?[0,-t]:[-ru,t-ru])}var vf=1e9,gf=-vf;function yf(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return fu(r[0]-t)0?0:3:fu(r[0]-e)0?2:1:fu(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,v,g,y,_,b=a,m=nf(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);y=!0,g=!1,p=v=NaN},lineEnd:function(){c&&(M(l,h),d&&g&&m.rejoin(),c.push(m.result()));x.point=w,g&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=A(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&of(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),y)l=o,h=a,d=u,y=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&g)b.point(o,a);else{var c=[p=Math.max(gf,Math.min(vf,p)),v=Math.max(gf,Math.min(vf,v))],m=[o=Math.max(gf,Math.min(vf,o)),a=Math.max(gf,Math.min(vf,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,v=a,g=u}return x}}var _f,bf,mf,xf=Qa(),wf={sphere:Nu,point:Nu,lineStart:function(){wf.point=Nf,wf.lineEnd=Mf},lineEnd:Nu,polygonStart:Nu,polygonEnd:Nu};function Mf(){wf.point=wf.lineEnd=Nu}function Nf(t,n){_f=t*=cu,bf=yu(n*=cu),mf=hu(n),wf.point=Tf}function Tf(t,n){t*=cu;var e=yu(n*=cu),r=hu(n),i=fu(t-_f),o=hu(i),a=r*yu(i),u=mf*e-bf*r*o,c=bf*e+mf*r*o;xf.add(lu(bu(a*a+u*u),c)),_f=t,bf=e,mf=r}function Af(t){return xf.reset(),Cu(t,wf),+xf}var Sf=[null,null],kf={type:"LineString",coordinates:Sf};function Ef(t,n){return Sf[0]=t,Sf[1]=n,Af(kf)}var Cf={Feature:function(t,n){return zf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r0&&(i=Ef(t[o],t[o-1]))>0&&e<=i&&r<=i&&(e+r-i)*(1-Math.pow((e-r)/i,2))nu}).map(c)).concat(g(du(o/d)*d,i,d).filter(function(t){return fu(t%v)>nu}).map(f))}return _.lines=function(){return b().map(function(t){return{type:"LineString",coordinates:t}})},_.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},_.extent=function(t){return arguments.length?_.extentMajor(t).extentMinor(t):_.extentMinor()},_.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),_.precision(y)):[[r,u],[e,a]]},_.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),_.precision(y)):[[n,o],[t,i]]},_.step=function(t){return arguments.length?_.stepMajor(t).stepMinor(t):_.stepMinor()},_.stepMajor=function(t){return arguments.length?(p=+t[0],v=+t[1],_):[p,v]},_.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],_):[h,d]},_.precision=function(h){return arguments.length?(y=+h,c=Of(o,i,90),f=Bf(n,t,y),s=Of(u,a,90),l=Bf(r,e,y),_):y},_.extentMajor([[-180,-90+nu],[180,90-nu]]).extentMinor([[-180,-80-nu],[180,80+nu]])}function Yf(t){return t}var If,Hf,jf,Xf,Vf=Qa(),Gf=Qa(),$f={point:Nu,lineStart:Nu,lineEnd:Nu,polygonStart:function(){$f.lineStart=Wf,$f.lineEnd=Kf},polygonEnd:function(){$f.lineStart=$f.lineEnd=$f.point=Nu,Vf.add(fu(Gf)),Gf.reset()},result:function(){var t=Vf/2;return Vf.reset(),t}};function Wf(){$f.point=Zf}function Zf(t,n){$f.point=Qf,If=jf=t,Hf=Xf=n}function Qf(t,n){Gf.add(Xf*t-jf*n),jf=t,Xf=n}function Kf(){Qf(If,Hf)}var Jf=1/0,ts=Jf,ns=-Jf,es=ns,rs={point:function(t,n){tns&&(ns=t);nes&&(es=n)},lineStart:Nu,lineEnd:Nu,polygonStart:Nu,polygonEnd:Nu,result:function(){var t=[[Jf,ts],[ns,es]];return ns=es=-(ts=Jf=1/0),t}};var is,os,as,us,cs=0,fs=0,ss=0,ls=0,hs=0,ds=0,ps=0,vs=0,gs=0,ys={point:_s,lineStart:bs,lineEnd:ws,polygonStart:function(){ys.lineStart=Ms,ys.lineEnd=Ns},polygonEnd:function(){ys.point=_s,ys.lineStart=bs,ys.lineEnd=ws},result:function(){var t=gs?[ps/gs,vs/gs]:ds?[ls/ds,hs/ds]:ss?[cs/ss,fs/ss]:[NaN,NaN];return cs=fs=ss=ls=hs=ds=ps=vs=gs=0,t}};function _s(t,n){cs+=t,fs+=n,++ss}function bs(){ys.point=ms}function ms(t,n){ys.point=xs,_s(as=t,us=n)}function xs(t,n){var e=t-as,r=n-us,i=bu(e*e+r*r);ls+=i*(as+t)/2,hs+=i*(us+n)/2,ds+=i,_s(as=t,us=n)}function ws(){ys.point=_s}function Ms(){ys.point=Ts}function Ns(){As(is,os)}function Ts(t,n){ys.point=As,_s(is=as=t,os=us=n)}function As(t,n){var e=t-as,r=n-us,i=bu(e*e+r*r);ls+=i*(as+t)/2,hs+=i*(us+n)/2,ds+=i,ps+=(i=us*t-as*n)*(as+t),vs+=i*(us+n),gs+=3*i,_s(as=t,us=n)}function Ss(t){this._context=t}Ss.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,au)}},result:Nu};var ks,Es,Cs,Ps,zs,Rs=Qa(),Ds={point:Nu,lineStart:function(){Ds.point=qs},lineEnd:function(){ks&&Ls(Es,Cs),Ds.point=Nu},polygonStart:function(){ks=!0},polygonEnd:function(){ks=null},result:function(){var t=+Rs;return Rs.reset(),t}};function qs(t,n){Ds.point=Ls,Es=Ps=t,Cs=zs=n}function Ls(t,n){Ps-=t,zs-=n,Rs.add(bu(Ps*Ps+zs*zs)),Ps=t,zs=n}function Us(){this._string=[]}function Os(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function Bs(t){return function(n){var e=new Fs;for(var r in t)e[r]=t[r];return e.stream=n,e}}function Fs(){}function Ys(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),Cu(e,t.stream(rs)),n(rs.result()),null!=r&&t.clipExtent(r),t}function Is(t,n,e){return Ys(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function Hs(t,n,e){return Is(t,[[0,0],n],e)}function js(t,n,e){return Ys(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function Xs(t,n,e){return Ys(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}Us.prototype={_radius:4.5,_circle:Os(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=Os(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},Fs.prototype={constructor:Fs,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Vs=16,Gs=hu(30*cu);function $s(t,n){return+n?function(t,n){function e(r,i,o,a,u,c,f,s,l,h,d,p,v,g){var y=f-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=c+p,M=bu(m*m+x*x+w*w),N=wu(w/=M),T=fu(fu(w)-1)n||fu((y*E+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*cu:0,E()):[g*uu,y*uu,_*uu]},S.angle=function(t){return arguments.length?(b=t%360*cu,E()):b*uu},S.reflectX=function(t){return arguments.length?(m=t?-1:1,E()):m<0},S.reflectY=function(t){return arguments.length?(x=t?-1:1,E()):x<0},S.precision=function(t){return arguments.length?(a=$s(u,A=t*t),C()):bu(A)},S.fitExtent=function(t,n){return Is(S,t,n)},S.fitSize=function(t,n){return Hs(S,t,n)},S.fitWidth=function(t,n){return js(S,t,n)},S.fitHeight=function(t,n){return Xs(S,t,n)},function(){return n=t.apply(this,arguments),S.invert=n.invert&&k,E()}}function Js(t){var n=0,e=ru/3,r=Ks(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*cu,e=t[1]*cu):[n*uu,e*uu]},i}function tl(t,n){var e=yu(t),r=(e+yu(n))/2;if(fu(r)0?n<-iu+nu&&(n=-iu+nu):n>iu-nu&&(n=iu-nu);var e=i/gu(fl(n),r);return[e*yu(r*t),i-e*hu(r*t)]}return o.invert=function(t,n){var e=i-n,o=_u(r)*bu(t*t+e*e),a=lu(t,fu(e))*_u(e);return e*r<0&&(a-=ru*_u(t)*_u(e)),[a/r,2*su(gu(i/o,1/r))-iu]},o}function ll(t,n){return[t,n]}function hl(t,n){var e=hu(t),r=t===n?yu(t):(e-hu(n))/(n-t),i=e/r+t;if(fu(r)=0;)n+=e[r].value;else n=1;t.value=n}function kl(t,n){var e,r,i,o,a,u=new zl(t),c=+t.value&&(u.value=t.value),f=[u];for(null==n&&(n=El);e=f.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)f.push(r=e.children[o]=new zl(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(Pl)}function El(t){return t.children}function Cl(t){t.data=t.data.data}function Pl(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function zl(t){this.data=t,this.depth=this.height=0,this.parent=null}_l.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(dl+pl*i+o*(vl+gl*i))-n)/(dl+3*pl*i+o*(7*vl+9*gl*i)))*r)*i*i,!(fu(e)nu&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},xl.invert=il(wu),wl.invert=il(function(t){return 2*su(t)}),Ml.invert=function(t,n){return[-n,2*su(pu(t))-iu]},zl.prototype=kl.prototype={constructor:zl,count:function(){return this.eachAfter(Sl)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return kl(this).eachBefore(Cl)}};var Rl=Array.prototype.slice;function Dl(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(Rl.call(t))).length,o=[];r0&&e*e>r*r+i*i}function Ol(t,n){for(var e=0;e(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function Hl(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function jl(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function Xl(t){this._=t,this.next=null,this.previous=null}function Vl(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,c,f,s,l;if((n=t[0]).x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Il(e,n,r=t[2]),n=new Xl(n),e=new Xl(e),r=new Xl(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:c1?n:1)},e}(vh);var _h=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(vh);function bh(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function mh(t,n){return t[0]-n[0]||t[1]-n[1]}function xh(t){for(var n=t.length,e=[0,1],r=2,i=2;i1&&bh(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function wh(){return Math.random()}var Mh=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(wh),Nh=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(wh),Th=function t(n){function e(){var t=Nh.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(wh),Ah=function t(n){function e(t){return function(){for(var e=0,r=0;rr&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function Ih(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?Hh:Ih,i=o=null,l}function l(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),me)))(e)))},l.domain=function(t){return arguments.length?(a=zh.call(t,Uh),f===Bh||(f=Yh(a)),s()):a.slice()},l.range=function(t){return arguments.length?(u=Rh.call(t),s()):u.slice()},l.rangeRound=function(t){return u=Rh.call(t),c=Ae,s()},l.clamp=function(t){return arguments.length?(f=t?Yh(a):Bh,l):f!==Bh},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Vh(t,n){return Xh()(t,n)}function Gh(n,e,r,i){var o,a=w(n,e,r);switch((i=Oa(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=Wa(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Za(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=$a(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function $h(t){var n=t.domain;return t.ticks=function(t){var e=n();return m(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return Gh(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i=n(),o=0,a=i.length-1,u=i[o],c=i[a];return c0?r=x(u=Math.floor(u/r)*r,c=Math.ceil(c/r)*r,e):r<0&&(r=x(u=Math.ceil(u*r)/r,c=Math.floor(c*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(c/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(c*r)/r,n(i)),t},t}function Wh(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;hc)break;v.push(l)}}else for(;h=1;--s)if(!((l=f*s)c)break;v.push(l)}}else v=m(h,d,Math.min(d-h,p)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*a0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return ld.setTime(+n),hd.setTime(+r),t(ld),t(hd),Math.floor(e(ld,hd))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}var pd=dd(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});pd.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?dd(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):pd:null};var vd=pd.range,gd=6e4,yd=6048e5,_d=dd(function(t){t.setTime(t-t.getMilliseconds())},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),bd=_d.range,md=dd(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds())},function(t,n){t.setTime(+t+n*gd)},function(t,n){return(n-t)/gd},function(t){return t.getMinutes()}),xd=md.range,wd=dd(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds()-t.getMinutes()*gd)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),Md=wd.range,Nd=dd(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*gd)/864e5},function(t){return t.getDate()-1}),Td=Nd.range;function Ad(t){return dd(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*gd)/yd})}var Sd=Ad(0),kd=Ad(1),Ed=Ad(2),Cd=Ad(3),Pd=Ad(4),zd=Ad(5),Rd=Ad(6),Dd=Sd.range,qd=kd.range,Ld=Ed.range,Ud=Cd.range,Od=Pd.range,Bd=zd.range,Fd=Rd.range,Yd=dd(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),Id=Yd.range,Hd=dd(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});Hd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?dd(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var jd=Hd.range,Xd=dd(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*gd)},function(t,n){return(n-t)/gd},function(t){return t.getUTCMinutes()}),Vd=Xd.range,Gd=dd(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),$d=Gd.range,Wd=dd(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),Zd=Wd.range;function Qd(t){return dd(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/yd})}var Kd=Qd(0),Jd=Qd(1),tp=Qd(2),np=Qd(3),ep=Qd(4),rp=Qd(5),ip=Qd(6),op=Kd.range,ap=Jd.range,up=tp.range,cp=np.range,fp=ep.range,sp=rp.range,lp=ip.range,hp=dd(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),dp=hp.range,pp=dd(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});pp.every=function(t){return isFinite(t=Math.floor(t))&&t>0?dd(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var vp=pp.range;function gp(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function yp(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function _p(t,n,e){return{y:t,m:n,d:e,H:0,M:0,S:0,L:0}}function bp(t){var n=t.dateTime,e=t.date,r=t.time,i=t.periods,o=t.days,a=t.shortDays,u=t.months,c=t.shortMonths,f=Sp(i),s=kp(i),l=Sp(o),h=kp(o),d=Sp(a),p=kp(a),v=Sp(u),g=kp(u),y=Sp(c),_=kp(c),b={a:function(t){return a[t.getDay()]},A:function(t){return o[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return u[t.getMonth()]},c:null,d:Wp,e:Wp,f:tv,H:Zp,I:Qp,j:Kp,L:Jp,m:nv,M:ev,p:function(t){return i[+(t.getHours()>=12)]},q:function(t){return 1+~~(t.getMonth()/3)},Q:Cv,s:Pv,S:rv,u:iv,U:ov,V:av,w:uv,W:cv,x:null,X:null,y:fv,Y:sv,Z:lv,"%":Ev},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:hv,e:hv,f:yv,H:dv,I:pv,j:vv,L:gv,m:_v,M:bv,p:function(t){return i[+(t.getUTCHours()>=12)]},q:function(t){return 1+~~(t.getUTCMonth()/3)},Q:Cv,s:Pv,S:mv,u:xv,U:wv,V:Mv,w:Nv,W:Tv,x:null,X:null,y:Av,Y:Sv,Z:kv,"%":Ev},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=y.exec(n.slice(e));return r?(t.m=_[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=g[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,e,r){return N(t,n,e,r)},d:Bp,e:Bp,f:Xp,H:Yp,I:Yp,j:Fp,L:jp,m:Op,M:Ip,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s[r[0].toLowerCase()],e+r[0].length):-1},q:Up,Q:Gp,s:$p,S:Hp,u:Cp,U:Pp,V:zp,w:Ep,W:Rp,x:function(t,n,r){return N(t,e,n,r)},X:function(t,n,e){return N(t,r,n,e)},y:qp,Y:Dp,Z:Lp,"%":Vp};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=yp(_p(o.y,0,1))).getUTCDay(),r=i>4||0===i?Jd.ceil(r):Jd(r),r=Wd.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=gp(_p(o.y,0,1))).getDay(),r=i>4||0===i?kd.ceil(r):kd(r),r=Nd.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?yp(_p(o.y,0,1)).getUTCDay():gp(_p(o.y,0,1)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,yp(o)):gp(o)}}function N(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in xp?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",!1);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t+="",!0);return n.toString=function(){return t},n}}}var mp,xp={"-":"",_:" ",0:"0"},wp=/^\s*\d+/,Mp=/^%/,Np=/[\\^$*+?|[\]().{}]/g;function Tp(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function Lp(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Up(t,n,e){var r=wp.exec(n.slice(e,e+1));return r?(t.q=3*r[0]-3,e+r[0].length):-1}function Op(t,n,e){var r=wp.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function Bp(t,n,e){var r=wp.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function Fp(t,n,e){var r=wp.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Yp(t,n,e){var r=wp.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Ip(t,n,e){var r=wp.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Hp(t,n,e){var r=wp.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function jp(t,n,e){var r=wp.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Xp(t,n,e){var r=wp.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Vp(t,n,e){var r=Mp.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Gp(t,n,e){var r=wp.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function $p(t,n,e){var r=wp.exec(n.slice(e));return r?(t.s=+r[0],e+r[0].length):-1}function Wp(t,n){return Tp(t.getDate(),n,2)}function Zp(t,n){return Tp(t.getHours(),n,2)}function Qp(t,n){return Tp(t.getHours()%12||12,n,2)}function Kp(t,n){return Tp(1+Nd.count(Hd(t),t),n,3)}function Jp(t,n){return Tp(t.getMilliseconds(),n,3)}function tv(t,n){return Jp(t,n)+"000"}function nv(t,n){return Tp(t.getMonth()+1,n,2)}function ev(t,n){return Tp(t.getMinutes(),n,2)}function rv(t,n){return Tp(t.getSeconds(),n,2)}function iv(t){var n=t.getDay();return 0===n?7:n}function ov(t,n){return Tp(Sd.count(Hd(t)-1,t),n,2)}function av(t,n){var e=t.getDay();return t=e>=4||0===e?Pd(t):Pd.ceil(t),Tp(Pd.count(Hd(t),t)+(4===Hd(t).getDay()),n,2)}function uv(t){return t.getDay()}function cv(t,n){return Tp(kd.count(Hd(t)-1,t),n,2)}function fv(t,n){return Tp(t.getFullYear()%100,n,2)}function sv(t,n){return Tp(t.getFullYear()%1e4,n,4)}function lv(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+Tp(n/60|0,"0",2)+Tp(n%60,"0",2)}function hv(t,n){return Tp(t.getUTCDate(),n,2)}function dv(t,n){return Tp(t.getUTCHours(),n,2)}function pv(t,n){return Tp(t.getUTCHours()%12||12,n,2)}function vv(t,n){return Tp(1+Wd.count(pp(t),t),n,3)}function gv(t,n){return Tp(t.getUTCMilliseconds(),n,3)}function yv(t,n){return gv(t,n)+"000"}function _v(t,n){return Tp(t.getUTCMonth()+1,n,2)}function bv(t,n){return Tp(t.getUTCMinutes(),n,2)}function mv(t,n){return Tp(t.getUTCSeconds(),n,2)}function xv(t){var n=t.getUTCDay();return 0===n?7:n}function wv(t,n){return Tp(Kd.count(pp(t)-1,t),n,2)}function Mv(t,n){var e=t.getUTCDay();return t=e>=4||0===e?ep(t):ep.ceil(t),Tp(ep.count(pp(t),t)+(4===pp(t).getUTCDay()),n,2)}function Nv(t){return t.getUTCDay()}function Tv(t,n){return Tp(Jd.count(pp(t)-1,t),n,2)}function Av(t,n){return Tp(t.getUTCFullYear()%100,n,2)}function Sv(t,n){return Tp(t.getUTCFullYear()%1e4,n,4)}function kv(){return"+0000"}function Ev(){return"%"}function Cv(t){return+t}function Pv(t){return Math.floor(+t/1e3)}function zv(n){return mp=bp(n),t.timeFormat=mp.format,t.timeParse=mp.parse,t.utcFormat=mp.utcFormat,t.utcParse=mp.utcParse,mp}zv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Rv=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");var Dv=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),qv=1e3,Lv=60*qv,Uv=60*Lv,Ov=24*Uv,Bv=7*Ov,Fv=30*Ov,Yv=365*Ov;function Iv(t){return new Date(t)}function Hv(t){return t instanceof Date?+t:+new Date(+t)}function jv(t,n,r,i,o,a,u,c,f){var s=Vh(Bh,Bh),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),v=f("%I:%M"),g=f("%I %p"),y=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y"),x=[[u,1,qv],[u,5,5*qv],[u,15,15*qv],[u,30,30*qv],[a,1,Lv],[a,5,5*Lv],[a,15,15*Lv],[a,30,30*Lv],[o,1,Uv],[o,3,3*Uv],[o,6,6*Uv],[o,12,12*Uv],[i,1,Ov],[i,2,2*Ov],[r,1,Bv],[n,1,Fv],[n,3,3*Fv],[t,1,Yv]];function M(e){return(u(e)=1?Cy:t<=-1?-Cy:Math.asin(t)}function Ry(t){return t.innerRadius}function Dy(t){return t.outerRadius}function qy(t){return t.startAngle}function Ly(t){return t.endAngle}function Uy(t){return t&&t.padAngle}function Oy(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/Sy(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*Sy(Ny(0,x*x*m-w*w)),N=(w*b-_*M)/m,T=(-w*_-b*M)/m,A=(w*b+_*M)/m,S=(-w*_+b*M)/m,k=N-g,E=T-y,C=A-g,P=S-y;return k*k+E*E>C*C+P*P&&(N=A,T=S),{cx:N,cy:T,x01:-s,y01:-l,x11:N*(i/x-1),y11:T*(i/x-1)}}function By(t){this._context=t}function Fy(t){return new By(t)}function Yy(t){return t[0]}function Iy(t){return t[1]}function Hy(){var t=Yy,n=Iy,e=my(!0),r=null,i=Fy,o=null;function a(a){var u,c,f,s=a.length,l=!1;for(null==r&&(o=i(f=no())),u=0;u<=s;++u)!(u=s;--l)u.point(g[l],y[l]);u.lineEnd(),u.areaEnd()}v&&(g[f]=+t(h,f,c),y[f]=+e(h,f,c),u.point(n?+n(h,f,c):g[f],r?+r(h,f,c):y[f]))}if(d)return u=null,d+""||null}function f(){return Hy().defined(i).curve(a).context(o)}return c.x=function(e){return arguments.length?(t="function"==typeof e?e:my(+e),n=null,c):t},c.x0=function(n){return arguments.length?(t="function"==typeof n?n:my(+n),c):t},c.x1=function(t){return arguments.length?(n=null==t?null:"function"==typeof t?t:my(+t),c):n},c.y=function(t){return arguments.length?(e="function"==typeof t?t:my(+t),r=null,c):e},c.y0=function(t){return arguments.length?(e="function"==typeof t?t:my(+t),c):e},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:my(+t),c):r},c.lineX0=c.lineY0=function(){return f().x(t).y(e)},c.lineY1=function(){return f().x(t).y(r)},c.lineX1=function(){return f().x(n).y(e)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:my(!!t),c):i},c.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),c):a},c.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),c):o},c}function Xy(t,n){return nt?1:n>=t?0:NaN}function Vy(t){return t}By.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Gy=Wy(Fy);function $y(t){this._curve=t}function Wy(t){function n(n){return new $y(t(n))}return n._curve=t,n}function Zy(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(Wy(t)):n()._curve},t}function Qy(){return Zy(Hy().curve(Gy))}function Ky(){var t=jy().curve(Gy),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Zy(e())},delete t.lineX0,t.lineEndAngle=function(){return Zy(r())},delete t.lineX1,t.lineInnerRadius=function(){return Zy(i())},delete t.lineY0,t.lineOuterRadius=function(){return Zy(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(Wy(t)):n()._curve},t}function Jy(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}$y.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var t_=Array.prototype.slice;function n_(t){return t.source}function e_(t){return t.target}function r_(t){var n=n_,e=e_,r=Yy,i=Iy,o=null;function a(){var a,u=t_.call(arguments),c=n.apply(this,u),f=e.apply(this,u);if(o||(o=a=no()),t(o,+r.apply(this,(u[0]=c,u)),+i.apply(this,u),+r.apply(this,(u[0]=f,u)),+i.apply(this,u)),a)return o=null,a+""||null}return a.source=function(t){return arguments.length?(n=t,a):n},a.target=function(t){return arguments.length?(e=t,a):e},a.x=function(t){return arguments.length?(r="function"==typeof t?t:my(+t),a):r},a.y=function(t){return arguments.length?(i="function"==typeof t?t:my(+t),a):i},a.context=function(t){return arguments.length?(o=null==t?null:t,a):o},a}function i_(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function o_(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function a_(t,n,e,r,i){var o=Jy(n,e),a=Jy(n,e=(e+i)/2),u=Jy(r,e),c=Jy(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],c[0],c[1])}var u_={draw:function(t,n){var e=Math.sqrt(n/Ey);t.moveTo(e,0),t.arc(0,0,e,0,Py)}},c_={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},f_=Math.sqrt(1/3),s_=2*f_,l_={draw:function(t,n){var e=Math.sqrt(n/s_),r=e*f_;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},h_=Math.sin(Ey/10)/Math.sin(7*Ey/10),d_=Math.sin(Py/10)*h_,p_=-Math.cos(Py/10)*h_,v_={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=d_*e,i=p_*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=Py*o/5,u=Math.cos(a),c=Math.sin(a);t.lineTo(c*e,-u*e),t.lineTo(u*r-c*i,c*r+u*i)}t.closePath()}},g_={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},y_=Math.sqrt(3),__={draw:function(t,n){var e=-Math.sqrt(n/(3*y_));t.moveTo(0,2*e),t.lineTo(-y_*e,-e),t.lineTo(y_*e,-e),t.closePath()}},b_=Math.sqrt(3)/2,m_=1/Math.sqrt(12),x_=3*(m_/2+1),w_={draw:function(t,n){var e=Math.sqrt(n/x_),r=e/2,i=e*m_,o=r,a=e*m_+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(-.5*r-b_*i,b_*r+-.5*i),t.lineTo(-.5*o-b_*a,b_*o+-.5*a),t.lineTo(-.5*u-b_*c,b_*u+-.5*c),t.lineTo(-.5*r+b_*i,-.5*i-b_*r),t.lineTo(-.5*o+b_*a,-.5*a-b_*o),t.lineTo(-.5*u+b_*c,-.5*c-b_*u),t.closePath()}},M_=[u_,c_,l_,g_,v_,__,w_];function N_(){}function T_(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function A_(t){this._context=t}function S_(t){this._context=t}function k_(t){this._context=t}function E_(t,n){this._basis=new A_(t),this._beta=n}A_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:T_(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:T_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},S_.prototype={areaStart:N_,areaEnd:N_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:T_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},k_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:T_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},E_.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var C_=function t(n){function e(t){return 1===n?new A_(t):new E_(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function P_(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function z_(t,n){this._context=t,this._k=(1-n)/6}z_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:P_(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:P_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var R_=function t(n){function e(t){return new z_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function D_(t,n){this._context=t,this._k=(1-n)/6}D_.prototype={areaStart:N_,areaEnd:N_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:P_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var q_=function t(n){function e(t){return new D_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function L_(t,n){this._context=t,this._k=(1-n)/6}L_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:P_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var U_=function t(n){function e(t){return new L_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function O_(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>ky){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>ky){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function B_(t,n){this._context=t,this._alpha=n}B_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:O_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var F_=function t(n){function e(t){return n?new B_(t,n):new z_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function Y_(t,n){this._context=t,this._alpha=n}Y_.prototype={areaStart:N_,areaEnd:N_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:O_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var I_=function t(n){function e(t){return n?new Y_(t,n):new D_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function H_(t,n){this._context=t,this._alpha=n}H_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:O_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var j_=function t(n){function e(t){return n?new H_(t,n):new L_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function X_(t){this._context=t}function V_(t){return t<0?-1:1}function G_(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(V_(o)+V_(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function $_(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function W_(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function Z_(t){this._context=t}function Q_(t){this._context=new K_(t)}function K_(t){this._context=t}function J_(t){this._context=t}function tb(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function ib(t,n){return t[n]}function ob(t){var n=t.map(ab);return rb(t).sort(function(t,e){return n[t]-n[e]})}function ab(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function ub(t){var n=t.map(cb);return rb(t).sort(function(t,e){return n[t]-n[e]})}function cb(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-c,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-f,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[c+s*h,f+s*d]),l<1&&(t[1]=[c+l*h,f+l*d]),!0)}}}}}function xb(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,c=t[0],f=t.left,s=t.right,l=f[0],h=f[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>d){if(c){if(c[1]>=i)return}else c=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(c){if(c[1]=r)return}else c=[n,a*n+u];o=[r,a*r+u]}else{if(c){if(c[0]=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},hb.prototype={constructor:hb,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=gb(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(pb(this,e),e=(t=e).U),e.C=!1,r.C=!0,vb(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(vb(this,e),e=(t=e).U),e.C=!1,r.C=!0,pb(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?gb(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,pb(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,vb(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,pb(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,vb(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,pb(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,vb(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var Tb,Ab=[];function Sb(){db(this),this.x=this.y=this.arc=this.site=this.cy=null}function kb(t){var n=t.P,e=t.N;if(n&&e){var r=n.site,i=t.site,o=e.site;if(r!==o){var a=i[0],u=i[1],c=r[0]-a,f=r[1]-u,s=o[0]-a,l=o[1]-u,h=2*(c*l-f*s);if(!(h>=-Hb)){var d=c*c+f*f,p=s*s+l*l,v=(l*d-f*p)/h,g=(c*p-s*d)/h,y=Ab.pop()||new Sb;y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=Fb._;b;)if(y.yIb)u=u.L;else{if(!((i=o-Ub(u,a))>Ib)){r>-Ib?(n=u.P,e=u):i>-Ib?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}!function(t){Bb[t.index]={site:t,halfedges:[]}}(t);var c=zb(t);if(Ob.insert(n,c),n||e){if(n===e)return Eb(n),e=zb(n.site),Ob.insert(c,e),c.edge=e.edge=yb(n.site,c.site),kb(n),void kb(e);if(e){Eb(n),Eb(e);var f=n.site,s=f[0],l=f[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];bb(e.edge,f,p,m),c.edge=yb(f,t,null,m),e.edge=yb(t,p,null,m),kb(n),kb(e)}else c.edge=yb(n.site,c.site)}}function Lb(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],c=e[1],f=c-n;if(!f)return u;var s=u-r,l=1/o-1/f,h=s/f;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*f)-c+f/2+i-o/2)))/l+r:(r+u)/2}function Ub(t,n){var e=t.N;if(e)return Lb(e,n);var r=t.site;return r[1]===n?r[0]:1/0}var Ob,Bb,Fb,Yb,Ib=1e-6,Hb=1e-12;function jb(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function Xb(t,n){return n[1]-t[1]||n[0]-t[0]}function Vb(t,n){var e,r,i,o=t.sort(Xb).pop();for(Yb=[],Bb=new Array(t.length),Ob=new hb,Fb=new hb;;)if(i=Tb,o&&(!i||o[1]Ib||Math.abs(i[0][1]-i[1][1])>Ib)||delete Yb[o]}(a,u,c,f),function(t,n,e,r){var i,o,a,u,c,f,s,l,h,d,p,v,g=Bb.length,y=!0;for(i=0;iIb||Math.abs(v-h)>Ib)&&(c.splice(u,0,Yb.push(_b(a,d,Math.abs(p-t)Ib?[t,Math.abs(l-t)Ib?[Math.abs(h-r)Ib?[e,Math.abs(l-e)Ib?[Math.abs(h-n)=u)return null;var c=t-i.site[0],f=n-i.site[1],s=c*c+f*f;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var c=t-u[0],f=n-u[1],l=c*c+f*f;lr?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Qb.prototype=Wb.prototype,t.FormatSpecifier=Ba,t.active=function(t,n){var e,r,i=t.__transition;if(i)for(r in n=null==n?null:n+"",i)if((e=i[r]).state>xr&&e.name===n)return new Ur([[t]],yi,n,+r);return null},t.arc=function(){var t=Ry,n=Dy,e=my(0),r=null,i=qy,o=Ly,a=Uy,u=null;function c(){var c,f,s=+t.apply(this,arguments),l=+n.apply(this,arguments),h=i.apply(this,arguments)-Cy,d=o.apply(this,arguments)-Cy,p=xy(d-h),v=d>h;if(u||(u=c=no()),lky)if(p>Py-ky)u.moveTo(l*My(h),l*Ay(h)),u.arc(0,0,l,h,d,!v),s>ky&&(u.moveTo(s*My(d),s*Ay(d)),u.arc(0,0,s,d,h,v));else{var g,y,_=h,b=d,m=h,x=d,w=p,M=p,N=a.apply(this,arguments)/2,T=N>ky&&(r?+r.apply(this,arguments):Sy(s*s+l*l)),A=Ty(xy(l-s)/2,+e.apply(this,arguments)),S=A,k=A;if(T>ky){var E=zy(T/s*Ay(N)),C=zy(T/l*Ay(N));(w-=2*E)>ky?(m+=E*=v?1:-1,x-=E):(w=0,m=x=(h+d)/2),(M-=2*C)>ky?(_+=C*=v?1:-1,b-=C):(M=0,_=b=(h+d)/2)}var P=l*My(_),z=l*Ay(_),R=s*My(x),D=s*Ay(x);if(A>ky){var q,L=l*My(b),U=l*Ay(b),O=s*My(m),B=s*Ay(m);if(p1?0:t<-1?Ey:Math.acos(t)}((F*I+Y*H)/(Sy(F*F+Y*Y)*Sy(I*I+H*H)))/2),X=Sy(q[0]*q[0]+q[1]*q[1]);S=Ty(A,(s-X)/(j-1)),k=Ty(A,(l-X)/(j+1))}}M>ky?k>ky?(g=Oy(O,B,P,z,l,k,v),y=Oy(L,U,R,D,l,k,v),u.moveTo(g.cx+g.x01,g.cy+g.y01),kky&&w>ky?S>ky?(g=Oy(R,D,L,U,s,-S,v),y=Oy(P,z,O,B,s,-S,v),u.lineTo(g.cx+g.x01,g.cy+g.y01),S>a,f=i+2*u>>a,s=bo(20);function l(r){var i=new Float32Array(c*f),l=new Float32Array(c*f);r.forEach(function(r,o,s){var l=+t(r,o,s)+u>>a,h=+n(r,o,s)+u>>a,d=+e(r,o,s);l>=0&&l=0&&h>a),So({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),Ao({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),So({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),Ao({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),So({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a);var d=s(i);if(!Array.isArray(d)){var p=T(i);d=w(0,p,d),(d=g(0,Math.floor(p/d)*d,d)).shift()}return To().thresholds(d).size([c,f])(i).map(h)}function h(t){return t.value*=Math.pow(2,-2*a),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function y(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,l}return l.x=function(n){return arguments.length?(t="function"==typeof n?n:bo(+n),l):t},l.y=function(t){return arguments.length?(n="function"==typeof t?t:bo(+t),l):n},l.weight=function(t){return arguments.length?(e="function"==typeof t?t:bo(+t),l):e},l.size=function(t){if(!arguments.length)return[r,i];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return r=n,i=e,y()},l.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),y()},l.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?bo(yo.call(t)):bo(t),l):s},l.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=Math.round((Math.sqrt(4*t*t+1)-1)/2),y()},l},t.contours=To,t.create=function(t){return Rt(Z(t).call(document.documentElement))},t.creator=Z,t.cross=function(t,n,e){var r,i,o,u,c=t.length,f=n.length,s=new Array(c*f);for(null==e&&(e=a),r=o=0;rt?1:n>=t?0:NaN},t.deviation=f,t.dispatch=I,t.drag=function(){var n,e,r,i,o=Gt,a=$t,u=Wt,c=Zt,f={},s=I("start","drag","end"),l=0,h=0;function d(t){t.on("mousedown.drag",p).filter(c).on("touchstart.drag",y).on("touchmove.drag",_).on("touchend.drag touchcancel.drag",b).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function p(){if(!i&&o.apply(this,arguments)){var u=m("mouse",a.apply(this,arguments),Bt,this,arguments);u&&(Rt(t.event.view).on("mousemove.drag",v,!0).on("mouseup.drag",g,!0),Ht(t.event.view),Yt(),r=!1,n=t.event.clientX,e=t.event.clientY,u("start"))}}function v(){if(It(),!r){var i=t.event.clientX-n,o=t.event.clientY-e;r=i*i+o*o>h}f.mouse("drag")}function g(){Rt(t.event.view).on("mousemove.drag mouseup.drag",null),jt(t.event.view,r),It(),f.mouse("end")}function y(){if(o.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=a.apply(this,arguments),u=r.length;for(n=0;nc+d||if+d||ou.index){var p=c-a.x-a.vx,v=f-a.y-a.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function u(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r=a)){(t.data!==n||t.next)&&(0===s&&(d+=(s=ya())*s),0===l&&(d+=(l=ya())*l),d1?(null==e?u.remove(t):u.set(t,d(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=ga(.1);function o(t){for(var i,o=0,a=n.length;o=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++ePc(r[0],r[1])&&(r[1]=i[1]),Pc(i[0],r[1])>Pc(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=Pc(r[1],i[0]))>a&&(a=u,Zu=i[0],Ku=r[1])}return ic=oc=null,Zu===1/0||Qu===1/0?[[NaN,NaN],[NaN,NaN]]:[[Zu,Qu],[Ku,Ju]]},t.geoCentroid=function(t){ac=uc=cc=fc=sc=lc=hc=dc=pc=vc=gc=0,Cu(t,Dc);var n=pc,e=vc,r=gc,i=n*n+e*e+r*r;return i2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=Ml,t.gray=function(t,n){return new Bn(t,0,0,null==n?1:n)},t.hcl=Xn,t.hierarchy=kl,t.histogram=function(){var t=v,n=s,e=M;function r(r){var o,a,u=r.length,c=new Array(u);for(o=0;ol;)h.pop(),--d;var p,v=new Array(d+1);for(o=0;o<=d;++o)(p=v[o]=[]).x0=o>0?h[o-1]:s,p.x1=o1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return ly.h=360*t-100,ly.s=1.5-1.5*n,ly.l=.8-.9*n,ly+""},t.interpolateRdBu=yg,t.interpolateRdGy=bg,t.interpolateRdPu=Yg,t.interpolateRdYlBu=xg,t.interpolateRdYlGn=Mg,t.interpolateReds=oy,t.interpolateRgb=he,t.interpolateRgbBasis=pe,t.interpolateRgbBasisClosed=ve,t.interpolateRound=Ae,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,hy.r=255*(n=Math.sin(t))*n,hy.g=255*(n=Math.sin(t+dy))*n,hy.b=255*(n=Math.sin(t+py))*n,hy+""},t.interpolateSpectral=Tg,t.interpolateString=Ne,t.interpolateTransformCss=qe,t.interpolateTransformSvg=Le,t.interpolateTurbo=function(t){return t=Math.max(0,Math.min(1,t)),"rgb("+Math.max(0,Math.min(255,Math.round(34.61+t*(1172.33-t*(10793.56-t*(33300.12-t*(38394.49-14825.05*t)))))))+", "+Math.max(0,Math.min(255,Math.round(23.31+t*(557.33+t*(1225.33-t*(3574.96-t*(1073.77+707.56*t)))))))+", "+Math.max(0,Math.min(255,Math.round(27.2+t*(3211.1-t*(15327.97-t*(27814-t*(22569.18-6838.66*t)))))))+")"},t.interpolateViridis=gy,t.interpolateWarm=fy,t.interpolateYlGn=Xg,t.interpolateYlGnBu=Hg,t.interpolateYlOrBr=Gg,t.interpolateYlOrRd=Wg,t.interpolateZoom=Ie,t.interrupt=Pr,t.interval=function(t,n,e){var r=new lr,i=n;return null==n?(r.restart(t,n,e),r):(n=+n,e=null==e?fr():+e,r.restart(function o(a){a+=i,r.restart(o,i+=n,e),t(a)},n,e),r)},t.isoFormat=Rv,t.isoParse=Dv,t.json=function(t,n){return fetch(t,n).then(la)},t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.lab=On,t.lch=function(t,n,e,r){return 1===arguments.length?jn(t):new Vn(e,n,t,null==r?1:r)},t.line=Hy,t.lineRadial=Qy,t.linkHorizontal=function(){return r_(i_)},t.linkRadial=function(){var t=r_(a_);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.linkVertical=function(){return r_(o_)},t.local=qt,t.map=co,t.matcher=nt,t.max=T,t.mean=function(t,n){var e,r=t.length,i=r,o=-1,a=0;if(null==n)for(;++o=r.length)return null!=t&&e.sort(t),null!=n?n(e):e;for(var c,f,s,l=-1,h=e.length,d=r[i++],p=co(),v=a();++lr.length)return e;var a,u=i[o-1];return null!=n&&o>=r.length?a=e.entries():(a=[],e.each(function(n,e){a.push({key:e,values:t(n,o)})})),null!=u?a.sort(function(t,n){return u(t.key,n.key)}):a}(o(t,0,lo,ho),0)},key:function(t){return r.push(t),e},sortKeys:function(t){return i[r.length-1]=t,e},sortValues:function(n){return t=n,e},rollup:function(t){return n=t,e}}},t.now=fr,t.pack=function(){var t=null,n=1,e=1,r=Wl;function i(i){return i.x=n/2,i.y=e/2,t?i.eachBefore(Kl(t)).eachAfter(Jl(r,.5)).eachBefore(th(1)):i.eachBefore(Kl(Ql)).eachAfter(Jl(Wl,1)).eachAfter(Jl(r,i.r/Math.min(n,e))).eachBefore(th(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=Gl(n),i):t},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:Zl(+t),i):r},i},t.packEnclose=Dl,t.packSiblings=function(t){return Vl(t),t},t.pairs=function(t,n){null==n&&(n=a);for(var e=0,r=t.length-1,i=t[0],o=new Array(r<0?0:r);e0&&(d+=l);for(null!=n?p.sort(function(t,e){return n(v[t],v[e])}):null!=e&&p.sort(function(t,n){return e(a[t],a[n])}),u=0,f=d?(y-h*b)/d:0;u0?l*f:0)+b,v[c]={data:a[c],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:my(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:my(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:my(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:my(+t),a):o},a},t.piecewise=function(t,n){for(var e=0,r=n.length-1,i=n[0],o=new Array(r<0?0:r);eu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonHull=function(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;n0?a[n-1]:r[0],n=o?[a[o-1],r]:[a[n-1],a[n]]},c.unknown=function(t){return arguments.length?(n=t,c):c},c.thresholds=function(){return a.slice()},c.copy=function(){return t().domain([e,r]).range(u).unknown(n)},Eh.apply($h(c),arguments)},t.scaleSequential=function t(){var n=$h(Xv()(Bh));return n.copy=function(){return Vv(n,t())},Ch.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=ed(Xv()).domain([1,10]);return n.copy=function(){return Vv(n,t()).base(n.base())},Ch.apply(n,arguments)},t.scaleSequentialPow=Gv,t.scaleSequentialQuantile=function t(){var e=[],r=Bh;function o(t){if(!isNaN(t=+t))return r((i(e,t)-1)/(e.length-1))}return o.domain=function(t){if(!arguments.length)return e.slice();e=[];for(var r,i=0,a=t.length;i0)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):(r[0]=0,r[1]=i)},t.stackOffsetExpand=function(t,n){if((r=t.length)>0){for(var e,r,i,o=0,a=t[0].length;o0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;a0)throw new Error("cycle");return o}return e.id=function(n){return arguments.length?(t=$l(n),e):t},e.parentId=function(t){return arguments.length?(n=$l(t),e):n},e},t.style=ft,t.sum=function(t,n){var e,r=t.length,i=-1,o=0;if(null==n)for(;++i=0;--i)u.push(e=n.children[i]=new dh(r[i],i)),e.parent=n;return(a.parent=new dh(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),v=e/(l.depth||1);i.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=sh(u),o=fh(o),u&&o;)c=fh(c),(a=sh(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(lh(hh(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!sh(a)&&(a.t=u,a.m+=l-s),o&&!fh(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=yh,n=!1,e=1,r=1,i=[0],o=Wl,a=Wl,u=Wl,c=Wl,f=Wl;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(nh),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}for(var l=f[n],h=r/2+l,d=n+1,p=e-1;d>>1;f[v]c-o){var _=(i*y+a*g)/r;t(n,d,g,i,o,_,c),t(d,e,y,_,o,a,c)}else{var b=(o*y+c*g)/r;t(n,d,g,i,o,a,b),t(d,e,y,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=eh,t.treemapResquarify=_h,t.treemapSlice=ph,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?ph:eh)(t,n,e,r,i)},t.treemapSquarify=yh,t.tsv=sa,t.tsvFormat=Ko,t.tsvFormatBody=Jo,t.tsvFormatRow=na,t.tsvFormatRows=ta,t.tsvFormatValue=ea,t.tsvParse=Zo,t.tsvParseRows=Qo,t.utcDay=Wd,t.utcDays=Zd,t.utcFriday=rp,t.utcFridays=sp,t.utcHour=Gd,t.utcHours=$d,t.utcMillisecond=pd,t.utcMilliseconds=vd,t.utcMinute=Xd,t.utcMinutes=Vd,t.utcMonday=Jd,t.utcMondays=ap,t.utcMonth=hp,t.utcMonths=dp,t.utcSaturday=ip,t.utcSaturdays=lp,t.utcSecond=_d,t.utcSeconds=bd,t.utcSunday=Kd,t.utcSundays=op,t.utcThursday=ep,t.utcThursdays=fp,t.utcTuesday=tp,t.utcTuesdays=up,t.utcWednesday=np,t.utcWednesdays=cp,t.utcWeek=Kd,t.utcWeeks=op,t.utcYear=pp,t.utcYears=vp,t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.variance=c,t.version="5.16.0",t.voronoi=function(){var t=sb,n=lb,e=null;function r(r){return new Vb(r.map(function(e,i){var o=[Math.round(t(e,i,r)/Ib)*Ib,Math.round(n(e,i,r)/Ib)*Ib];return o.index=i,o.data=e,o}),e)}return r.polygons=function(t){return r(t).polygons()},r.links=function(t){return r(t).links()},r.triangles=function(t){return r(t).triangles()},r.x=function(n){return arguments.length?(t="function"==typeof n?n:fb(+n),r):t},r.y=function(t){return arguments.length?(n="function"==typeof t?t:fb(+t),r):n},r.extent=function(t){return arguments.length?(e=null==t?null:[[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]],r):e&&[[e[0][0],e[0][1]],[e[1][0],e[1][1]]]},r.size=function(t){return arguments.length?(e=null==t?null:[[0,0],[+t[0],+t[1]]],r):e&&[e[1][0]-e[0][0],e[1][1]-e[0][1]]},r},t.window=ct,t.xml=da,t.zip=function(){return k(arguments)},t.zoom=function(){var n,e,r=tm,i=nm,o=om,a=rm,u=im,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=Ie,h=I("start","zoom","end"),d=500,p=150,v=0;function g(t){t.property("__zoom",em).on("wheel.zoom",M).on("mousedown.zoom",N).on("dblclick.zoom",T).filter(u).on("touchstart.zoom",A).on("touchmove.zoom",S).on("touchend.zoom touchcancel.zoom",k).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new Wb(n,t.x,t.y)}function _(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Wb(t.k,r,i)}function b(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function m(t,n,e){t.on("start.zoom",function(){x(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){x(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,o=x(t,r),a=i.apply(t,r),u=null==e?b(a):"function"==typeof e?e.apply(t,r):e,c=Math.max(a[1][0]-a[0][0],a[1][1]-a[0][1]),f=t.__zoom,s="function"==typeof n?n.apply(t,r):n,h=l(f.invert(u).concat(c/f.k),s.invert(u).concat(c/s.k));return function(t){if(1===t)t=s;else{var n=h(t),e=c/n[2];t=new Wb(e,u[0]-n[0]*e,u[1]-n[1]*e)}o.zoom(null,t)}})}function x(t,n,e){return!e&&t.__zooming||new w(t,n)}function w(t,n){this.that=t,this.args=n,this.active=0,this.extent=i.apply(t,n),this.taps=0}function M(){if(r.apply(this,arguments)){var t=x(this,arguments),n=this.__zoom,e=Math.max(c[0],Math.min(c[1],n.k*Math.pow(2,a.apply(this,arguments)))),i=Bt(this);if(t.wheel)t.mouse[0][0]===i[0]&&t.mouse[0][1]===i[1]||(t.mouse[1]=n.invert(t.mouse[0]=i)),clearTimeout(t.wheel);else{if(n.k===e)return;t.mouse=[i,n.invert(i)],Pr(this),t.start()}Jb(),t.wheel=setTimeout(function(){t.wheel=null,t.end()},p),t.zoom("mouse",o(_(y(n,e),t.mouse[0],t.mouse[1]),t.extent,f))}}function N(){if(!e&&r.apply(this,arguments)){var n=x(this,arguments,!0),i=Rt(t.event.view).on("mousemove.zoom",function(){if(Jb(),!n.moved){var e=t.event.clientX-u,r=t.event.clientY-c;n.moved=e*e+r*r>v}n.zoom("mouse",o(_(n.that.__zoom,n.mouse[0]=Bt(n.that),n.mouse[1]),n.extent,f))},!0).on("mouseup.zoom",function(){i.on("mousemove.zoom mouseup.zoom",null),jt(t.event.view,n.moved),Jb(),n.end()},!0),a=Bt(this),u=t.event.clientX,c=t.event.clientY;Ht(t.event.view),Kb(),n.mouse=[a,this.__zoom.invert(a)],Pr(this),n.start()}}function T(){if(r.apply(this,arguments)){var n=this.__zoom,e=Bt(this),a=n.invert(e),u=n.k*(t.event.shiftKey?.5:2),c=o(_(y(n,u),e,a),i.apply(this,arguments),f);Jb(),s>0?Rt(this).transition().duration(s).call(m,c,e):Rt(this).call(g.transform,c)}}function A(){if(r.apply(this,arguments)){var e,i,o,a,u=t.event.touches,c=u.length,f=x(this,arguments,t.event.changedTouches.length===c);for(Kb(),i=0;i { + var device = deviceDetails.rawDevice; + try { + if (!device.opened) { + await device.open(); + } + console.log("FiiO Device connected"); + } catch (error) { + console.error("Failed to connect to FiiO Device:", error); + throw error; + } + }; + const getCurrentSlot = async (deviceDetails) => { + var device = deviceDetails.rawDevice; + try { + let currentSlot = -99; + + device.oninputreport = async (event) => { + const data = new Uint8Array(event.data.buffer); + if (data[0] === GET_HEADER1 && data[1] === GET_HEADER2) { + switch (data[4]) { + case PEQ_PRESET_SWITCH: + currentSlot = handleEqPreset(data, deviceDetails); + break; + default: + console.log("Unhandled data type:", data[4]); + } + } + }; + + await getPresetPeq(device); + + // Wait at most 10 seconds for filters to be populated + const result = await waitForFilters(() => { + return currentSlot > -99 + }, device, 10000, (device) => ( + currentSlot + )); + + return result; + } catch (error) { + console.error("Failed to pull data from FiiO Device:", error); + throw error; + } + }; + + const pushToDevice = async (deviceDetails, slot, preamp_gain, filters) => { + try { + var device = deviceDetails.rawDevice; + + // FiiO devices will automatically cut the max SPL by the maxGain (typically -12) + // So, we can safely apply a +12 gain - the larged preamp_gain needed + // .e.g. if we need to +5dB for a filter then we can still make the globalGain 7dB + await setGlobalGain(device, deviceDetails.modelConfig.maxGain + preamp_gain); + const maxFilters = deviceDetails.modelConfig.maxFilters; + const maxFiltersToUse = Math.min(filters.length, maxFilters); + await setPeqCounter(device, maxFiltersToUse); + + for (let filterIdx = 0; filterIdx < maxFiltersToUse; filterIdx++) { + const filter = filters[filterIdx]; + var gain = 0; // If disabled we still need to reset to 0 gain as previous gain value will + // still be active + if (!filter.disabled) { + gain = filter.gain; + } + // A quick sanity check on the filters + if (filter.freq < 20 || filter.freq > 20000) { + filter.freq = 100; + } + if (filter.q < 0.01 || filter.q > 100) { + filter.q = 1; + } await setPeqParams(device, filterIdx, filter.freq, gain, filter.q, convertFromFilterType(filter.type)); + } + + saveToDevice(device, slot); + + console.log("PEQ filters pushed successfully."); + + if (deviceDetails.modelConfig.disconnectOnSave) { + return true; // Disconnect + } + return false; + + } catch (error) { + console.error("Failed to push data to FiiO Device:", error); + throw error; + } + }; + + const pullFromDevice = async (deviceDetails, slot) => { + try { + const filters = []; + let peqCount = 0; + let globalGain = 0; + let currentSlot = 0; + var device = deviceDetails.rawDevice; + + device.oninputreport = async (event) => { + const data = new Uint8Array(event.data.buffer); + if (data[0] === GET_HEADER1 && data[1] === GET_HEADER2) { + switch (data[4]) { + case PEQ_FILTER_COUNT: + peqCount = handlePeqCounter(data, device); + break; + case PEQ_FILTER_PARAMS: + handlePeqParams(data, device, filters); + break; + case PEQ_GLOBAL_GAIN: + globalGain = handleGain(data[6], data[7]); + break; + case PEQ_PRESET_SWITCH: + currentSlot = handleEqPreset(data, deviceDetails); + break; + case PEQ_SAVE_TO_DEVICE: + savedEQ(data, device); + break; + default: + console.log("Unhandled data type:", data[4]); + } + } + }; + + await getPresetPeq(device); + await getPeqCounter(device); + await getGlobalGain(device); + + // Wait at most 10 seconds for filters to be populated + const result = await waitForFilters(() => { + return filters.length == peqCount + }, device, 10000, (device) => ({ + filters: filters, + globalGain: globalGain, + currentSlot: currentSlot, + deviceDetails: deviceDetails.modelConfig + })); + + return result; + } catch (error) { + console.error("Failed to pull data from FiiO Device:", error); + throw error; + } + } + + const enablePEQ = async (deviceDetails, enable, slotId) => { + + var device = deviceDetails.rawDevice; + + if (enable) { // take the slotId we are given and switch to it + await setPresetPeq(device, slotId); + } else { + await setPresetPeq(device, deviceDetails.modelConfig.maxFilters); + } + } + return { + connect, + pushToDevice, + pullFromDevice, + getCurrentSlot, + enablePEQ + }; +})(); + + +// Private Helper Functions + +async function setPeqParams(device, filterIndex, fc, gain, q, filterType) { + const [frequencyLow, frequencyHigh] = splitUnsignedValue(fc); + const [gainLow, gainHigh] = fiioGainBytesFromValue(gain); + const qFactorValue = Math.round(q * 100); + const [qFactorLow, qFactorHigh] = splitUnsignedValue(qFactorValue); + + const packet = [ + SET_HEADER1, SET_HEADER2, 0, 0, PEQ_FILTER_PARAMS, 8, + filterIndex, gainLow, gainHigh, + frequencyLow, frequencyHigh, + qFactorLow, qFactorHigh, + filterType, 0, END_HEADERS + ]; + + const data = new Uint8Array(packet); + const reportId = device.collections[0].outputReports[0].reportId; + await device.sendReport(reportId, data); +} + +async function setPresetPeq(device, presetId) { // Default to 0 if not specified + const packet = [ + SET_HEADER1, SET_HEADER2, 0, 0, PEQ_PRESET_SWITCH, 1, + presetId, 0, END_HEADERS + ]; + + const data = new Uint8Array(packet); + const reportId = device.collections[0].outputReports[0].reportId; + await device.sendReport(reportId, data); +} + +async function setGlobalGain(device, gain) { + const globalGain = Math.round(gain * 10); + const gainBytes = toBytePair(globalGain); + + const packet = [ + SET_HEADER1, SET_HEADER2, 0, 0, PEQ_GLOBAL_GAIN, 2, + gainBytes[1], gainBytes[0], 0, END_HEADERS + ]; + + const data = new Uint8Array(packet); + const reportId = device.collections[0].outputReports[0].reportId; + await device.sendReport(reportId, data); +} + +async function setPeqCounter(device, counter) { + const packet = [ + SET_HEADER1, SET_HEADER2, 0, 0, PEQ_FILTER_COUNT, 1, + counter, 0, END_HEADERS + ]; + + const data = new Uint8Array(packet); + const reportId = device.collections[0].outputReports[0].reportId; + await device.sendReport(reportId, data); +} + +function convertFromFilterType(filterType) { + const mapping = {"PK": 0, "LSQ": 1, "HSQ": 2}; + return mapping[filterType] !== undefined ? mapping[filterType] : 0; +} + +function convertToFilterType(datum) { + switch (datum) { + case 0: + return "PK"; + case 1: + return "LSQ"; + case 2: + return "HSQ"; + default: + return "PK"; + } +} + +function toBytePair(value) { + return [ + value & 0xFF, + (value & 0xFF00) >> 8 + ]; +} + +function splitSignedValue(value) { + const signedValue = value < 0 ? value + 65536 : value; + return [ + (signedValue >> 8) & 0xFF, + signedValue & 0xFF + ]; +} + +function splitUnsignedValue(value) { + return [ + (value >> 8) & 0xFF, + value & 0xFF + ]; +} + +function combineBytes(lowByte, highByte) { + return (lowByte << 8) | highByte; +} + +function getGlobalGain(device) { + const packet = [GET_HEADER1, GET_HEADER2, 0, 0, PEQ_GLOBAL_GAIN, 0, 0, END_HEADERS]; + const data = new Uint8Array(packet); + console.log("getGlobalGain() Send data:", data); + const reportId = getFirstValidReportId(device); + device.sendReport(reportId, data); +} + +function getPeqCounter(device) { + const packet = [GET_HEADER1, GET_HEADER2, 0, 0, PEQ_FILTER_COUNT, 0, 0, END_HEADERS]; + const data = new Uint8Array(packet); + console.log("getPeqCounter() Send data:", data); + const reportId = getFirstValidReportId(device); + device.sendReport(reportId, data); +} + +function getPeqParams(device, filterIndex) { + const packet = [GET_HEADER1, GET_HEADER2, 0, 0, PEQ_FILTER_PARAMS, 1, filterIndex, 0, END_HEADERS]; + const data = new Uint8Array(packet); + console.log("getPeqParams() Send data:", data); + const reportId = getFirstValidReportId(device); + device.sendReport(reportId, data); +} + +function getPresetPeq(device) { + const packet = [GET_HEADER1, GET_HEADER2, 0, 0, PEQ_PRESET_SWITCH, 0, 0, END_HEADERS]; + const data = new Uint8Array(packet); + console.log("getPresetPeq() Send data:", data); + const reportId = getFirstValidReportId(device); + device.sendReport(reportId, data); +} + +/** + * Loops through all collections and returns the first valid reportId. + * @param {Object} device - The device object. + * @returns {string|null} - The first valid reportId, or null if none found. + */ +function getFirstValidReportId(device) { + if (device.collections && device.collections.length > 0) { + for (const collection of device.collections) { + if (collection.outputReports && collection.outputReports.length > 0) { + for (const report of collection.outputReports) { + if (report.reportId) { + return report.reportId; // Return the first valid reportId + } + } + } + } + } + return null; // Return null if no valid reportId is found +} + +function saveToDevice(device, slotId) { + const packet = [SET_HEADER1, SET_HEADER2, 0, 0, PEQ_SAVE_TO_DEVICE, 1, slotId, 0, END_HEADERS]; + const data = new Uint8Array(packet); + console.log("saveToDevice() reportId Send data:", data); + const reportId = getFirstValidReportId(device); + device.sendReport(reportId, data); +} + +function handlePeqCounter(data, device) { + let peqCount = data[6]; + console.log("***********oninputreport peq counter=", peqCount); + if (peqCount > 0) { + processPeqCount(device, peqCount); + } + return peqCount; +} + +function processPeqCount(device, peqCount) { + console.log("PEQ Counter:", peqCount); + + // Fetch individual PEQ settings based on count + for (let i = 0; i < peqCount; i++) { + getPeqParams(device, i); + } +} + +function handlePeqParams(data, device, filters) { + const filter = data[6]; + const gain = handleGain(data[7], data[8]); + const frequency = combineBytes(data[9], data[10]); + const qFactor = (combineBytes(data[11], data[12])) / 100 || 1; + const filterType = convertToFilterType(data[13]); + + console.log(`Filter ${filter}: Gain=${gain}, Frequency=${frequency}, Q=${qFactor}, Type=${filterType}`); + + filters[filter] = { + type: filterType, + freq: frequency, + q: qFactor, + gain: gain, + disabled: (gain || frequency || qFactor) ? false : true // Disable filter if 0 value found + }; +} + + +function handleGain(lowByte, highByte) { + let r = combineBytes(lowByte, highByte); + const gain = r & 32768 ? (r = (r ^ 65535) + 1, -r / 10) : r / 10; + return gain; +} + +function fiioGainBytesFromValue(e) { + let t = e * 10; + t < 0 && (t = (Math.abs(t) ^ 65535) + 1); + const r = t >> 8 & 255, + n = t & 255; + return [r, n] +} + +function handleEqPreset(data, deviceDetails) { + const presetId = data[6]; + console.log("EQ Preset ID:", presetId); + + if (presetId == deviceDetails.modelConfig.disabledPresetId) { + return -1; // with JA11 slot 4 == Off + } + // Handle preset switch if necessary + return presetId; +} + +function savedEQ(data, device) { + const slotId = data[6]; + console.log("EQ Slot ID:", slotId); + // Handle slot enablement if necessary +} + + +// Utility function to wait for a condition or timeout +function waitForFilters(condition, device, timeout, callback) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (!condition()) { + console.warn("Timeout reached before data returned?"); + reject(callback(device)); + } else { + resolve(callback(device)); + } + }, timeout); + + // Check every 100 milliseconds if everything is ready based on condition method !! + const interval = setInterval(() => { + if (condition()) { + clearTimeout(timer); + clearInterval(interval); + resolve(callback(device)); + } + }, 100); + }); +} + diff --git a/assets/js/devicePEQ/moondropHidHandler.js b/assets/js/devicePEQ/moondropHidHandler.js new file mode 100644 index 0000000..b3452bb --- /dev/null +++ b/assets/js/devicePEQ/moondropHidHandler.js @@ -0,0 +1,143 @@ +const SET_REPORT = 0x09; +const GET_REPORT = 0x01; +const REPORT_ID = 1; +const EQ_SLOT_READ = 0x03; +const EQ_SLOT_WRITE = 0x04; + + +export const moondropUsbHID = (function() { + async function connect(deviceDetails) { + var device = deviceDetails.rawDevice; + try { + if (!device.opened) { + await device.open(); + } + console.log("Moondrop Device connected"); + } catch (error) { + console.error("Failed to connect to Moondrop Device:", error); + throw error; + } + } + + async function getCurrentSlot(deviceDetails) { + var device = deviceDetails.rawDevice; + try { + let currentSlot = -99; + const requestData = new Uint8Array([REPORT_ID, GET_REPORT, EQ_SLOT_READ]); + const reportId = device.collections[0].outputReports[0].reportId; + + device.oninputreport = async (event) => { + const data = new Uint8Array(event.data.buffer); + if (data[0] === REPORT_ID && data[1] === GET_REPORT) { + currentSlot = data[3]; + } + }; + + await device.sendReport(reportId, requestData); + + return await waitForResponse(() => currentSlot > -99, device, 5000, () => currentSlot); + } catch (error) { + console.error("Failed to retrieve current EQ slot:", error); + throw error; + } + } + + async function pullFromDevice(deviceDetails, slot) { + try { + var device = deviceDetails.rawDevice; + const filters = []; + let globalGain = 0; + let peqCount = 0; + + const requestData = new Uint8Array([REPORT_ID, GET_REPORT, EQ_SLOT_READ, slot]); + const reportId = device.collections[0].outputReports[0].reportId; + + device.oninputreport = async (event) => { + const data = new Uint8Array(event.data.buffer); + if (data[0] === REPORT_ID && data[1] === GET_REPORT) { + peqCount = data[3]; + globalGain = data[4]; + for (let i = 0; i < peqCount; i++) { + filters.push(parseFilterData(data.slice(5 + i * 6, 11 + i * 6))); + } + } + }; + + await device.sendReport(reportId, requestData); + return await waitForResponse(() => filters.length === peqCount, device, 5000, () => ({ filters, globalGain })); + } catch (error) { + console.error("Failed to retrieve filters from Moondrop Device:", error); + throw error; + } + } + + async function pushToDevice(deviceDetails, slot, globalGain, filters) { + try { + var device = deviceDetails.rawDevice; + const reportId = device.collections[0].outputReports[0].reportId; + const requestData = new Uint8Array([REPORT_ID, SET_REPORT, EQ_SLOT_WRITE, slot, globalGain, filters.length, ...encodeFilters(filters)]); + await device.sendReport(reportId, requestData); + console.log("Filters pushed successfully"); + } catch (error) { + console.error("Failed to push filters to Moondrop Device:", error); + throw error; + } + } + + function parseFilterData(data) { + return { + freq: (data[0] << 8) | data[1], + gain: (data[2] << 8) | data[3], + q: (data[4] << 8) | data[5] + }; + } + + function encodeFilters(filters) { + let encoded = []; + for (const filter of filters) { + encoded.push(filter.freq >> 8, filter.freq & 0xFF, filter.gain >> 8, filter.gain & 0xFF, filter.q >> 8, filter.q & 0xFF); + } + return encoded; + } + + function waitForResponse(condition, device, timeout, callback) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (!condition()) { + console.warn("Timeout waiting for response"); + reject(callback()); + } else { + resolve(callback()); + } + }, timeout); + + const interval = setInterval(() => { + if (condition()) { + clearTimeout(timer); + clearInterval(interval); + resolve(callback()); + } + }, 100); + }); + } + + // Enable or disable PEQ by selecting a slot + const enablePEQ = async (deviceDetails, enable, slotId) => { + var device = deviceDetails.rawDevice; + const reportId = device.collections[0].outputReports[0].reportId; + if (enable) { + await device.sendReport(reportId, [WRITE_VALUE, FLASH_EQ, 0x00, slotId]); // Save EQ to Flash + } else { + await device.sendReport(reportId, [WRITE_VALUE, RESET_EQ_DEFAULT, 0x01, 0x04]); // Reset EQ to Default + } + }; + + return { + connect, + pushToDevice, + pullFromDevice, + getCurrentSlot, + enablePEQ, + }; +})(); + diff --git a/assets/js/devicePEQ/networkDeviceConnector.js b/assets/js/devicePEQ/networkDeviceConnector.js new file mode 100644 index 0000000..c300398 --- /dev/null +++ b/assets/js/devicePEQ/networkDeviceConnector.js @@ -0,0 +1,80 @@ +// networkDeviceConnector.js +// Copyright 2024 : Pragmatic Audio + +export const NetworkDeviceConnector = (function () { + let currentDevice = null; + const deviceHandlers = { + "WiiM": null, // Will be dynamically imported + }; + + async function initialize() { + const { wiimNetworkHandler } = await import('./wiimNetworkHandler.js'); + deviceHandlers["WiiM"] = wiimNetworkHandler; + } + + async function getDeviceConnected(deviceIP, deviceType) { + try { + if (!deviceIP) { + console.warn("No IP Address provided."); + return null; + } + + if (!deviceHandlers[deviceType]) { + console.warn("Unsupported Device Type."); + return null; + } + + currentDevice = { + ip: deviceIP, + type: deviceType, + handler: deviceHandlers[deviceType] + }; + + console.log(`Connected to ${deviceType} at ${deviceIP}`); + return currentDevice; + } catch (error) { + console.error("Failed to connect to Network Device:", error); + return null; + } + } + + async function disconnectDevice() { + if (currentDevice) { + console.log(`Disconnected from ${currentDevice.type} at ${currentDevice.ip}`); + currentDevice = null; + } + } + + async function pushToDevice(device, slot, preamp, filters) { + if (!currentDevice) { + console.warn("No network device connected."); + return; + } + return await currentDevice.handler.pushToDevice(currentDevice.ip, slot, preamp, filters); + } + + async function pullFromDevice(device, slot) { + if (!currentDevice) { + console.warn("No network device connected."); + return; + } + return await currentDevice.handler.pullFromDevice(currentDevice.ip, slot); + } + + async function enablePEQ(device, enabled, slotId) { + if (!currentDevice) { + console.warn("No network device connected."); + return; + } + return await currentDevice.handler.enablePEQ(currentDevice.ip, enabled, slotId); + } + + return { + initialize, + getDeviceConnected, + disconnectDevice, + pushToDevice, + pullFromDevice, + enablePEQ, + }; +})(); diff --git a/assets/js/devicePEQ/plugin.js b/assets/js/devicePEQ/plugin.js new file mode 100644 index 0000000..b6ffce2 --- /dev/null +++ b/assets/js/devicePEQ/plugin.js @@ -0,0 +1,464 @@ +// Copyright 2024 : Pragmatic Audio + +/** + * Initialise the plugin - passing the content from the extraEQ section so we can both query + * and update that area and add our UI elements. + * + * @param context + * @returns {Promise} + */ +async function initializeDeviceEqPlugin(context) { + console.log("Plugin initialized with context:", context); + + class DeviceEqUI { + constructor() { + this.deviceEqArea = document.getElementById('deviceEqArea'); + this.connectButton = this.deviceEqArea.querySelector('.connect-device'); + this.disconnectButton = this.deviceEqArea.querySelector('.disconnect-device'); + this.deviceNameElem = document.getElementById('deviceName'); + this.peqSlotArea = this.deviceEqArea.querySelector('.peq-slot-area'); + this.peqDropdown = document.getElementById('device-peq-slot-dropdown'); + this.pullButton = this.deviceEqArea.querySelector('.pull-filters-fromdevice'); + this.pushButton = this.deviceEqArea.querySelector('.push-filters-todevice'); + + this.useNetwork = false; + this.currentDevice = null; + this.initializeUI(); + } + + initializeUI() { + this.disconnectButton.hidden = true; + this.pullButton.hidden = true; + this.pushButton.hidden = true; + this.peqDropdown.hidden = true; + this.peqSlotArea.hidden = true; + } + + showConnectedState(device, useNetwork, availableSlots, currentSlot) { + this.connectButton.hidden = true; + this.currentDevice = device; + this.useNetwork = useNetwork; + this.disconnectButton.hidden = false; + this.deviceNameElem.textContent = device.model; + this.populatePeqDropdown(availableSlots, currentSlot); + this.pullButton.hidden = false; + this.pushButton.hidden = false; + this.peqDropdown.hidden = false; + this.peqSlotArea.hidden = false; + } + + showDisconnectedState() { + this.useNetwork = false; + this.currentDevice = null; + this.connectButton.hidden = false; + this.disconnectButton.hidden = true; + this.deviceNameElem.textContent = 'None'; + this.peqDropdown.innerHTML = ''; + this.peqDropdown.hidden = true; + this.pullButton.hidden = true; + this.pushButton.hidden = true; + this.peqSlotArea.hidden = true; + } + + populatePeqDropdown(slots, currentSlot) { + // Clear existing options and add the default "PEQ Disabled" option + this.peqDropdown.innerHTML = ''; + + // Populate the dropdown with available slots + slots.forEach(slot => { + const option = document.createElement('option'); + option.value = slot.id; + option.textContent = slot.name; + this.peqDropdown.appendChild(option); + }); + + // Set the selected option based on currentSlot + if (currentSlot === -1) { + // Select "PEQ Disabled" + this.peqDropdown.selectedIndex = 0; + } else { + // Attempt to select the option matching currentSlot + const matchingOption = Array.from(this.peqDropdown.options).find(option => option.value === String(currentSlot)); + if (matchingOption) { + this.peqDropdown.value = currentSlot; + } else { + // If no matching option, default to "PEQ Disabled" + this.peqDropdown.selectedIndex = 0; + } + } + } + } + + function loadHtml() { + // Define the HTML to insert + const deviceEqHTML = ` +
+
Device PEQ
+
+ + +
+
+ +
+
+ + +
+
+ `; + + // Find the
element + const extraEqElement = document.querySelector('.extra-eq'); + + if (extraEqElement) { + // Insert the new HTML below the "extra-eq" div + extraEqElement.insertAdjacentHTML('afterend', deviceEqHTML); + console.log('Device EQ UI added below
'); + } else { + console.error('Element
not found in the DOM.'); + } + } + + try { + // Dynamically import USB and Network connectors + const UsbHIDConnectorAsync = await import('./usbHidConnector.js').then((module) => module.UsbHIDConnector); + const UsbHIDConnector = await UsbHIDConnectorAsync; + console.log('UsbHIDConnector loaded'); + + const NetworkDeviceConnectorAsync = await import('./networkDeviceConnector.js').then((module) => module.NetworkDeviceConnector); + const NetworkDeviceConnector = await NetworkDeviceConnectorAsync; + console.log('NetworkDeviceConnector loaded'); + + if ('hid' in navigator) { // Only support browsers with HID support for now + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initializeDeviceEQ()); + } else { + // DOM is already loaded + initializeDeviceEQ(); + } + + function initializeDeviceEQ() { + // Dynamically load the HTML we need in the right place + loadHtml(); + + const deviceEqUI = new DeviceEqUI(); + + // Show the Connect button if WebHID is supported + deviceEqUI.deviceEqArea.classList.remove('disabled'); + deviceEqUI.connectButton.hidden = false; + deviceEqUI.disconnectButton.hidden = true; + + // Connect Button Event Listener + deviceEqUI.connectButton.addEventListener('click', async () => { + try { + let selection = {useNetwork: false}; // Assume usb only by default + if (context.config.showNetwork) { + // Show a custom dialog to select Network or USB + selection = await showDeviceSelectionDialog(); + } + + if (selection.useNetwork) { + if (!selection.ipAddress) { + alert("Please enter a valid IP address."); + return; + } + setCookie("networkDeviceIP", selection.ipAddress, 30); // Save IP for 30 days + setCookie("networkDeviceType", selection.deviceType, 30); // Store device type for 30 days + + // Connect via Network using the provided IP + const device = await NetworkDeviceConnector.getDeviceConnected(selection.ipAddress, selection.deviceType); + if (device) { + deviceEqUI.showConnectedState( + device, + selection.useNetwork, + await NetworkDeviceConnector.getAvailableSlots(device), + await NetworkDeviceConnector.getCurrentSlot(device) + ); + } + } else { + // Connect via USB and show the HID device picker + const device = await UsbHIDConnector.getDeviceConnected(); + if (device) { + deviceEqUI.showConnectedState( + device, + selection.useNetwork, + await UsbHIDConnector.getAvailableSlots(device), + await UsbHIDConnector.getCurrentSlot(device) + ); + + device.rawDevice.addEventListener('disconnect', () => { + console.log(`Device ${device.rawDevice.productName} disconnected.`); + deviceEqUI.showDisconnectedState(); + }); + } + } + } catch (error) { + console.error("Error connecting to device:", error); + alert("Failed to connect to the device."); + } + }); + + + // Cookie functions + function setCookie(name, value, days) { + let expires = ""; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + value + "; path=/" + expires; + } + + function getCookie(name) { + const nameEQ = name + "="; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let c = cookies[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; + } + + function deleteCookie(name) { + document.cookie = name + "=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC"; + } + + function showDeviceSelectionDialog() { + return new Promise((resolve) => { + const storedIP = getCookie("networkDeviceIP") || ""; // Retrieve stored IP + const storedDeviceType = getCookie("networkDeviceType") || "WiiM"; // Default to WiiM + // Define the HTML structure for the dialog + const dialogHTML = ` +
+

Select Connection Type

+

Choose whether to connect via USB or Network.

+ + + + + +
+ + + + + + + +
+ + +
+ `; + + // Create a container div for the dialog + const dialogContainer = document.createElement("div"); + dialogContainer.innerHTML = dialogHTML; + document.body.appendChild(dialogContainer); + + // Get references to elements inside the dialog + const dialog = document.getElementById("device-selection-dialog"); + const networkButton = document.getElementById("network-button"); + const usbButton = document.getElementById("usb-button"); + const ipInput = document.getElementById("ip-input"); + const networkOptions = document.getElementById("network-options"); + const submitButton = document.getElementById("submit-button"); + const cancelButton = document.getElementById("cancel-button"); + + // Handle Network Selection + networkButton.addEventListener("click", () => { + ipInput.style.display = "block"; // Show IP input field + networkOptions.style.display = "block"; // Show radio buttons + submitButton.style.display = "inline-block"; // Show submit button + }); + + // Handle USB Selection - Immediately resolve and remove the dialog + usbButton.addEventListener("click", async () => { + document.body.removeChild(dialogContainer); + resolve({useNetwork: false}); // Proceed with USB connection + }); + + // Handle Network Connection Submission + submitButton.addEventListener("click", () => { + const ipAddress = ipInput.value.trim(); + if (!ipAddress) { + alert("Please enter a valid IP address."); + return; + } + + const selectedDevice = document.querySelector('input[name="network-device"]:checked').value; + document.body.removeChild(dialogContainer); + resolve({useNetwork: true, ipAddress: ipAddress, deviceType: selectedDevice}); + }); + + // Handle Cancel Button Click + cancelButton.addEventListener("click", () => { + document.body.removeChild(dialogContainer); + resolve(null); // User canceled + }); + }); + } + + + // Disconnect Button Event Listener + deviceEqUI.disconnectButton.addEventListener('click', async () => { + try { + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.disconnectDevice(); + } else { + await UsbHIDConnector.disconnectDevice(); + } + deviceEqUI.showDisconnectedState(); + } catch (error) { + console.error("Error disconnecting:", error); + alert("Failed to disconnect."); + } + }); + + // Pull Button Event Listener + deviceEqUI.pullButton.addEventListener('click', async () => { + try { + const device = deviceEqUI.currentDevice; + const selectedSlot = deviceEqUI.peqDropdown.value; + if (!device || !selectedSlot) { + alert("No device connected or PEQ slot selected."); + return; + } + var result = null; + if (deviceEqUI.useNetwork) { + result = await NetworkDeviceConnector.pullFromDevice(device, selectedSlot); + } else { + result = await UsbHIDConnector.pullFromDevice(device, selectedSlot); + } + if (result.filters.length > 0) { + context.filtersToElem(result.filters); + context.applyEQ(); + } else { + alert("No PEQ filters found on the device."); + } + } catch (error) { + console.error("Error pulling PEQ filters:", error); + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.disconnectDevice(); + } else { + await UsbHIDConnector.disconnectDevice(); + } + deviceEqUI.showDisconnectedState(); + } + }); + + // Push Button Event Listener + deviceEqUI.pushButton.addEventListener('click', async () => { + try { + const device = deviceEqUI.currentDevice; + const selectedSlot = deviceEqUI.peqDropdown.value; + if (!device || !selectedSlot) { + alert("No device connected or PEQ slot selected."); + return; + } + + // ✅ Use context to get filters instead of undefined elemToFilters() + const filters = context.elemToFilters(true); + if (!filters.length) { + alert("Please add at least one filter before pushing."); + return; + } + + const preamp_gain = context.calcEqDevPreamp(filters); + let disconnect = false; + if (deviceEqUI.useNetwork) { + disconnect = await NetworkDeviceConnector.pushToDevice(device, selectedSlot, preamp_gain, filters); + } else { + disconnect = await UsbHIDConnector.pushToDevice(device, selectedSlot, preamp_gain, filters); + } + + if (disconnect) { + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.disconnectDevice(); + } else { + await UsbHIDConnector.disconnectDevice(); + } + deviceEqUI.showDisconnectedState(); + alert("PEQ Saved - Restarting"); + } + } catch (error) { + + console.error("Error pushing PEQ filters:", error); + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.disconnectDevice(); + } else { + await UsbHIDConnector.disconnectDevice(); + } + deviceEqUI.showDisconnectedState(); + } + }); + + // PEQ Dropdown Change Event Listener + deviceEqUI.peqDropdown.addEventListener('change', async (event) => { + const selectedValue = event.target.value; + console.log(`PEQ Slot selected: ${selectedValue}`); + + try { + if (selectedValue === "-1") { + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.enablePEQ(deviceEqUI.currentDevice, false, -1); + } else { + await UsbHIDConnector.enablePEQ(deviceEqUI.currentDevice, false, -1); + } + console.log("PEQ Disabled."); + } else { + const slotId = parseInt(selectedValue, 10); + + if (deviceEqUI.useNetwork) { + await NetworkDeviceConnector.enablePEQ(deviceEqUI.currentDevice, true, slotId); + } else { + await UsbHIDConnector.enablePEQ(deviceEqUI.currentDevice, true, slotId); + } + + console.log(`PEQ Enabled for slot ID: ${slotId}`); + } + } catch (error) { + console.error("Error updating PEQ slot:", error); + alert("Failed to update PEQ slot."); + } + }); + + } + } + } catch (error) { + console.error("Error initializing Device EQ Plugin:", error.message); + } +} + +// Export for CommonJS & ES Modules +if (typeof module !== "undefined" && module.exports) { + module.exports = initializeDeviceEqPlugin; +} + +// Export for ES Modules +export default initializeDeviceEqPlugin; diff --git a/assets/js/devicePEQ/usbDeviceConfig.js b/assets/js/devicePEQ/usbDeviceConfig.js new file mode 100644 index 0000000..8fe5c18 --- /dev/null +++ b/assets/js/devicePEQ/usbDeviceConfig.js @@ -0,0 +1,248 @@ +// Dynamically import manufacturer specific handlers for their unique devices +const {fiioUsbHID} = await import('./fiioUsbHidHandler.js'); +const {walkplayUsbHID} = await import('./walkplayHidHandler.js'); +const {moondropUsbHID} = await import('./moondropHidHandler.js'); + +// Main list of HID devices - each vendor has one or more vendorId, and a list of devices associated, +// each device has a model of how the slots are configured and a handler to handle reading / writing +// the raw USBHID reports to the device +export const usbHidDeviceHandlerConfig = ( [ + { + vendorId: 10610, + manufacturer: "FiiO", + handler: fiioUsbHID, + defaultModelConfig: { // Fallback if we haven't got specific details yet + minGain: -12, + maxGain: 12, + maxFilters: 5, + firstWritableEQSlot: -1, + maxWritableEQSlots: 0, + disconnectOnSave: true, + disabledPresetId: -1, + availableSlots: [] + }, + devices: { + "JadeAudio JA11": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 5, + firstWritableEQSlot: 3, + maxWritableEQSlots: 1, + disconnectOnSave: true, + disabledPresetId: 4, + availableSlots: [{id: 0, name: "Vocal"}, {id: 1, name: "Classic"}, {id: 2, name: "Bass"}, { + id: 3, + name: "USER1" + }] + } + }, + "FIIO KA17": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: false, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Jazz"}, {id: 1, name: "Pop"}, {id: 2, name: "Rock"}, { + id: 3, + name: "Dance" + }, { + id: 5, + name: "R&B" + }, {id: 6, name: "Classic"}, {id: 7, name: "Hip-hop"}, {id: 4, name: "USER1"}, {id: 8, name: "USER2"}, { + id: 9, + name: "USER3" + }] + } + }, + "FIIO Q7": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: false, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Jazz"}, {id: 1, name: "Pop"}, {id: 2, name: "Rock"}, { + id: 3, + name: "Dance" + }, { + id: 5, + name: "R&B" + }, {id: 6, name: "Classic"}, {id: 7, name: "Hip-hop"}, {id: 4, name: "USER1"}, {id: 8, name: "USER2"}, { + id: 9, + name: "USER3" + }] + } + }, + "FIIO BTR13": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: false, + disabledPresetId: 12, + availableSlots: [{id: 0, name: "Jazz"}, {id: 1, name: "Pop"}, {id: 2, name: "Rock"}, { + id: 3, + name: "Dance" + }, { + id: 4, + name: "R&B" + }, {id: 5, name: "Classic"}, {id: 6, name: "Hip-hop"}, {id: 7, name: "USER1"}, {id: 8, name: "USER2"}, { + id: 9, + name: "USER3" + }] + } + }, + "BTR17": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: false, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Jazz"}, {id: 1, name: "Pop"}, {id: 2, name: "Rock"}, { + id: 3, + name: "Dance" + }, { + id: 4, + name: "R&B" + }, {id: 5, name: "Classic"}, {id: 6, name: "Hip-hop"}, {id: 160, name: "USER1"}, {id: 161, name: "USER2"}, { + id: 162, + name: "USER3" + }, {id: 160, name: "USER1"}, {id: 161, name: "USER2"}, {id: 162, name: "USER3"}, {id: 163, name: "USER4"}, { + id: 164, + name: "USER5" + }, {id: 165, name: "USER6"}, {id: 166, name: "USER7"}, {id: 167, name: "USER8"}, {id: 168, name: "USER9"}, { + id: 169, + name: "USER10" + }] + } + }, + "FIIO KA15": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: false, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Jazz"}, {id: 1, name: "Pop"}, {id: 2, name: "Rock"}, { + id: 3, + name: "Dance" + }, { + id: 4, + name: "R&B" + }, {id: 5, name: "Classic"}, {id: 6, name: "Hip-hop"}, {id: 7, name: "USER1"}, {id: 8, name: "USER2"}, { + id: 9, + name: "USER3" + }] + } + }, + "K17": { + modelConfig: {} + }, + "LS-TC2": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 5, + firstWritableEQSlot: 3, + maxWritableEQSlots: 1, + disconnectOnSave: true, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Vocal"}, {id: 1, name: "Classic"}, {id: 2, name: "Bass"}, { + id: 3, + name: "Dance" + }, {id: 4, name: "R&B"}, {id: 5, name: "Classic"}, {id: 6, name: "Hip-hop"}, {id: 160, name: "USER1"}] + } + } + } + }, + { + vendorId: 2578, // Snowsky + manufacturer: "FiiO", + handler: fiioUsbHID, + devices: { + "RETRO NANO": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: 7, + maxWritableEQSlots: 3, + disconnectOnSave: true, + disabledPresetId: 11, + availableSlots: [{id: 0, name: "Vocal"}, {id: 1, name: "Classic"}, {id: 2, name: "Bass"}, { + id: 3, + name: "Dance" + }, {id: 4, name: "R&B"}, {id: 5, name: "Classic"}, {id: 6, name: "Hip-hop"}, {id: 8, name: "Retro"}, { + id: 11, + name: "Close" + }, {id: 160, name: "USER1"}, {id: 161, name: "USER2"}, {id: 162, name: "USER3"}] + } + }, + } + }, + { + vendorId: 13058, + manufacturer: "WalkPlay", + handler: walkplayUsbHID, + defaultModelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 10, + firstWritableEQSlot: -1, + maxWritableEQSlots: 0, + disconnectOnSave: true, + disabledPresetId: -1, + availableSlots: [{id: -99, name: "Default"}] + }, + devices: { + "Hi-MAX": { + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 8, + firstWritableEQSlot: -1, + maxWritableEQSlots: 0, + disconnectOnSave: false, + disabledPresetId: -1, + availableSlots: [{id: 101, name: "Custom"},{id: 102, name: "Custom 2"},{id: 103, name: "Custom 3"},{id: 104, name: "Custom 4"},{id: 1, name: "Pure"},{id: 2, name: "Pop"},{id: 3, name: "Rock"},{id: 4, name: "Vocal"},{id: 5, name: "Bass"},{id: 6, name: "Flat"},{id: 7, name: "Cinema"},{id: 8, name: "Game"},{id: -99, name: "Default"}] + } + }, + "Quark2": { + manufacturer: "Moondrop", + modelConfig: { + minGain: -12, + maxGain: 12, + maxFilters: 8, + firstWritableEQSlot: -1, + maxWritableEQSlots: 0, + disconnectOnSave: false, + disabledPresetId: -1, + availableSlots: [{id: 101, name: "Custom"},{id: 102, name: "Custom 2"},{id: 103, name: "Custom 3"},{id: 104, name: "Custom 4"},{id: 1, name: "Pure"},{id: 2, name: "Pop"},{id: 3, name: "Rock"},{id: 4, name: "Vocal"},{id: 5, name: "Bass"},{id: 6, name: "Flat"},{id: 7, name: "Cinema"},{id: 8, name: "Game"},{id: -99, name: "Default"}] + } + }, + } + }, + { + vendorId: 13784, + manufacturer: "Moondrop", + devices: { + "ECHO-B": { + handler: moondropUsbHID, + modelConfig: {} + }, + } + } +]) diff --git a/assets/js/devicePEQ/usbHidConnector.js b/assets/js/devicePEQ/usbHidConnector.js new file mode 100644 index 0000000..20f979d --- /dev/null +++ b/assets/js/devicePEQ/usbHidConnector.js @@ -0,0 +1,164 @@ +// +// Copyright 2024 : Pragmatic Audio +// +// Declare UsbHIDConnector and attach it to the global window object + +export const UsbHIDConnector = ( async function () { + let devices = []; + let currentDevice = null; + + const {usbHidDeviceHandlerConfig} = await import('./usbDeviceConfig.js'); + + const getDeviceConnected = async () => { + try { + // Build filters from your configuration + // (Assuming each configuration has a unique vendorId) + const vendorToManufacturer = usbHidDeviceHandlerConfig.map(entry => ({ + vendorId: entry.vendorId, + // You could also include productId if needed + })); + + // Request devices matching the filters + const selectedDevices = await navigator.hid.requestDevice({ filters: vendorToManufacturer }); + + if (selectedDevices.length > 0) { + const rawDevice = selectedDevices[0]; + // Find the vendor configuration matching the selected device + const vendorConfig = usbHidDeviceHandlerConfig.find(entry => entry.vendorId === rawDevice.vendorId); + + if (!vendorConfig) { + console.error("No configuration found for vendor:", rawDevice.vendorId); + return; + } + + const model = rawDevice.productName; + + // Look up the model-specific configuration from the vendor config. + // If no specific model configuration exists, fall back to a default if provided. + let deviceDetails = vendorConfig.devices[model] || {}; + let modelConfig = deviceDetails.modelConfig || vendorConfig.defaultModelConfig || {}; + + const manufacturer = deviceDetails.manufacturer | vendorConfig.manufacturer; + let handler = deviceDetails.handler || vendorConfig.handler; + + // Check if already connected + const existingDevice = devices.find(d => d.rawDevice === rawDevice); + if (existingDevice) { + console.log("Device already connected:", existingDevice.model); + currentDevice = existingDevice; + return currentDevice; + } + + // Open the device if not already open + if (!rawDevice.opened) { + await rawDevice.open(); + } + currentDevice = { + rawDevice: rawDevice, + manufacturer: manufacturer, + model: model, + handler: handler, + modelConfig: modelConfig + }; + + if (currentDevice.handler) { + await currentDevice.handler.connect(currentDevice); + } else { + console.error(`No handler found for ${manufacturer} ${model}`); + return null; + } + + devices.push(currentDevice); + return currentDevice; + } else { + console.log("No device found."); + return null; + } + } catch (error) { + console.error("Failed to connect to HID device:", error); + return null; + } + }; + + const disconnectDevice = async () => { + if (currentDevice && currentDevice.rawDevice) { + try { + await currentDevice.rawDevice.close(); + console.log("Device disconnected:", currentDevice.model); + devices = devices.filter(d => d !== currentDevice); + currentDevice = null; + } catch (error) { + console.error("Failed to disconnect device:", error); + } + } + }; + const checkDeviceConnected = async (rawDevice) => { + const devices = await navigator.hid.getDevices(); + var connected = devices.some(d => d === rawDevice); + if (!connected) { + console.error("Device disconnected?"); + alert('Device disconnected?'); + return false; + } + return true; + }; + + const pushToDevice = async (device, slot, preamp, filters) => { + if (!await checkDeviceConnected(device.rawDevice)) { + throw Error("Device Disconnected"); + } + if (device && device.handler) { + return await device.handler.pushToDevice(device, slot, preamp, filters); + } else { + console.error("No device handler available for pushing."); + } + return true; // Disconnect anyway + }; + + // Helper Function to Get Available 'Custom' Slots Based on the Device that we can write too + const getAvailableSlots = async (device) => { + return device.modelConfig.availableSlots; + }; + + const getCurrentSlot = async (device) => { + if (device && device.handler) { + return await device.handler.getCurrentSlot(device) + }{ + console.error("No device handler available for querying"); + return -2; + } + }; + + const pullFromDevice = async (device, slot) => { + if (!await checkDeviceConnected(device.rawDevice)) { + throw Error("Device Disconnected"); + } + if (device && device.handler) { + return await device.handler.pullFromDevice(device, slot); + } else { + console.error("No device handler available for pulling."); + return { filters: [] }; // Empty filters + } + }; + + const enablePEQ = async (device, enabled, slotId) => { + if (device && device.handler) { + return await device.handler.enablePEQ(device, enabled, slotId); + } else { + console.error("No device handler available for enabling."); + } + }; + + const getCurrentDevice = () => currentDevice; + + return { + getDeviceConnected, + getAvailableSlots, + disconnectDevice, + pushToDevice, + pullFromDevice, + getCurrentDevice, + getCurrentSlot, + enablePEQ, + }; +})(); diff --git a/assets/js/devicePEQ/walkplayHidHandler.js b/assets/js/devicePEQ/walkplayHidHandler.js new file mode 100644 index 0000000..d8b4d58 --- /dev/null +++ b/assets/js/devicePEQ/walkplayHidHandler.js @@ -0,0 +1,302 @@ +// +// Copyright 2024 : Pragmatic Audio +// +// Define the shared logic for Walkplay devices +// +// Many thanks to ma0shu for providing a dump + +const REPORT_ID = 0x4B; // Report ID +const ALT_REPORT_ID = 0x3C; // Alternative Report ID +const READ_VALUE = 0x80; // From capture provided +const WRITE_VALUE = 0x01; // +const END_HEADER = 0x00; +const PEQ_VALUES = 0x09; // Read or write PEQ - read is current slot / write contains +const RESET_DEVICE = 0x23; +const RESET_EQ_DEFAULT = 0x05; +const FIRMWARE_VERSION = 0x0C; +const TEMP_WRITE = 0x0A; +const FLASH_EQ = 0x01; + +// These are probably not suitable for this plugin - but here because ma0shu provided the capture +const ADC_OFFSET = 0x02; +const DAC_OFFSET = 0x03; +const DAC_WORKING_MODE = 0x1D; // 0 -> "Class-H" 1 -> "Class-AB" +const ENC_STATUS = 0x1B; // environment noise cancellation ?? Microphone ?? +const FILTER_MODE = 0x11; // FAST-LL etc +const HIGH_LOW_GAIN = 0x19; // 0 = LOW 1 = HIGH + +export const walkplayUsbHID = (function() { + let device = null; + + // Connect to Walkplay USB-HID device + const connect = async (deviceDetails) => { + var hidDevice = deviceDetails.rawDevice; + try { + device = hidDevice || (await navigator.hid.requestDevice({ filters: [] }))[0]; + if (!device) throw new Error("No HID device selected."); + if (!device.opened) await device.open(); + console.log("Walkplay Device connected."); + } catch (error) { + console.error("Failed to connect:", error); + throw error; + } + }; + + // Get the currently selected EQ slot + const getCurrentSlot = async (deviceDetails) => { + var device = deviceDetails.rawDevice; + if (!device) throw new Error("Device not connected."); + console.log("Fetching current EQ slot..."); + + await sendReport(device, REPORT_ID,[READ_VALUE, PEQ_VALUES, END_HEADER]); + const response = await waitForResponse(device); + const slot = response ? response[35] : -1; + + console.log("Current EQ Slot:", slot); + return slot; + }; + + // Push PEQ settings to Walkplay device + const pushToDevice = async (deviceDetails, slot, preampGain, filters) => { + var device = deviceDetails.rawDevice; + if (!device) throw new Error("Device not connected."); + console.log("Pushing PEQ settings..."); + if (typeof slot === "string" ) // Convert from string + slot = parseInt(slot, 10); + + var useAltReport = false; + + for (let i = 0; i < filters.length; i++) { + const filter = filters[i]; + const bArr = computeIIRFilter(i, filter.freq, filter.gain, filter.q); + + const packet = [ + WRITE_VALUE, PEQ_VALUES, 0x18, 0x00, i, 0x00, 0x00, + ...bArr, + ...convertToByteArray(filter.freq, 2), + ...convertToByteArray(Math.round(filter.q * 256), 2), + ...convertToByteArray(Math.round(filter.gain * 256), 2), + 0x02, + 0x0, + slot, + END_HEADER + ]; + console.log("Write HID data:", packet); + + await sendReport(device,useAltReport ? ALT_REPORT_ID : REPORT_ID ,packet); + } + + // Apply EQ settings temporarily + await sendReport(device,REPORT_ID,[WRITE_VALUE, TEMP_WRITE, 0x04, 0x00, 0x00, 0xFF, 0xFF,END_HEADER]); + + // Apply EQ settings temporarily + await sendReport(device,REPORT_ID,[WRITE_VALUE, FLASH_EQ, 0x01, END_HEADER]); + + console.log("PEQ filters pushed successfully."); + }; + + // Pull PEQ settings from Walkplay device + const pullFromDevice = async (deviceDetails, slot = DEFAULT_EQ_SLOT) => { + try { + var device = deviceDetails.rawDevice; + if (!device) throw new Error("Device not connected."); + console.log("Pulling PEQ settings..."); + + const filters = []; + let globalGain = 0; + let currentSlot = -1; + const expectedFilters = 8; // WalkPlay has 8 filters + + // Set up event listener to process incoming data + device.oninputreport = async (event) => { + const data = new Uint8Array(event.data.buffer); + console.log("Received HID response:", data); + + // Extract filter data if it's a valid response + if (data.length >= 32) { + let filter = parseIndividualPEQPacket(data); + filters[filter.filterIndex] = filter; // Store at correct index + } + + // Extract global gain if available + if (data.length > 40) { + globalGain = parseGlobalGain(data); + } + + // Extract EQ slot + if (data.length >= 37) { + currentSlot = data[36]; + } + }; + + // Request each filter (0-7) individually + for (let i = 0; i < expectedFilters; i++) { + await sendReport(device, REPORT_ID,[READ_VALUE, PEQ_VALUES, 0x00, 0x00, i, END_HEADER]); // Read filter `i` + await delay(50); // Give time for each request to be processed + } + + // Wait until all 8 filters are retrieved + const result = await waitForFilters(() => { + return filters.filter(f => f !== undefined).length === expectedFilters; + }, device, 10000, () => ({ + filters: filters, + globalGain: globalGain, + currentSlot: currentSlot, + deviceDetails: deviceDetails.modelConfig, + })); + + console.log("PEQ Data Pulled:", result); + return result; + } catch (error) { + console.error("Failed to pull data from WalkPlay Device:", error); + throw error; + } + }; + + // Enable or disable PEQ + const enablePEQ = async (deviceDetails, enable, slotId) => { + var device = deviceDetails.rawDevice; + if (!enable) slotId = 0x00; // ?? Determine the not enabled + const packet = [WRITE_VALUE, FLASH_EQ, 0x00, slotId, END_HEADER] + await sendReport(device, REPORT_ID, packet); + console.log(`PEQ ${enable ? "enabled" : "disabled"}.`); + }; + + +// Internal functions + async function sendReport(device, reportId, packet) { + if (!device) throw new Error("Device not connected."); + const data = new Uint8Array(packet); + console.log("Sending:", data); + await device.sendReport(reportId, data); + } + +// Wait for response + async function waitForResponse(device, timeout = 5000) { + return new Promise((resolve, reject) => { + let response = null; + const timer = setTimeout(() => reject("Timeout waiting for HID response"), timeout); + + device.oninputreport = (event) => { + clearTimeout(timer); + response = new Uint8Array(event.data.buffer); + console.log("Received:", response); + resolve(response); + }; + }); + } + + return { + connect, + pushToDevice, + pullFromDevice, + getCurrentSlot, + enablePEQ, + }; +})(); + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function waitForFilters(condition, device, timeout, callback) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (!condition()) { + console.warn("Timeout: Filters not fully received."); + reject(callback(device)); + } else { + resolve(callback(device)); + } + }, timeout); + + const interval = setInterval(() => { + if (condition()) { + clearTimeout(timer); + clearInterval(interval); + resolve(callback(device)); + } + }, 100); + }); +} + +function parseIndividualPEQPacket(packet) { + if (packet.length < 32) { + throw new Error("Packet too short to contain filter data."); + } + let filterType = convertToFilterType(packet[26]); + let filterIndex = packet[4]; // Extract filter index + let freq = packet[27] | (packet[28] << 8); // Frequency (little-endian) + let qFactor = (packet[29] | (packet[30] << 8)) / 256; // Q factor (scaled) + + let gainRaw = packet[31] | (packet[32] << 8); // Gain (little-endian) + if (gainRaw > 32767) gainRaw -= 65536; // Handle negative values + let gain = gainRaw / 256; // Convert to dB + + return { + type: filterType, + filterIndex: filterIndex, + freq: freq, + q: qFactor, + gain: gain, + disabled: (gain || freq || qFactor) ? false : true // Disable filter if 0 value found + }; +} +function convertToFilterType(datum) { + switch (datum) { + case 0: + return "PK"; // Walkplay only seems to have Peaking filters + default: + return "PK"; + } +} + +function parseGlobalGain(data) { + if (data.length < 40) return 0; // No global gain found + + let gainRaw = data[38] | (data[39] << 8); // Extract gain (little-endian) + if (gainRaw > 32767) gainRaw -= 65536; // Convert to signed integer + return gainRaw / 256; // Convert to dB +} + +// Compute IIR filter +function computeIIRFilter(i, freq, gain, q) { + let bArr = new Array(20).fill(0); + let sqrt = Math.sqrt(Math.pow(10, gain / 20)); + let d3 = (freq * 6.283185307179586) / 96000; + let sin = Math.sin(d3) / (2 * q); + let d4 = sin * sqrt; + let d5 = sin / sqrt; + let d6 = d5 + 1; + let quantizerData = quantizer( + [1, (Math.cos(d3) * -2) / d6, (1 - d5) / d6], + [(d4 + 1) / d6, (Math.cos(d3) * -2) / d6, (1 - d4) / d6] + ); + + let index = 0; + for (let value of quantizerData) { + bArr[index] = value & 0xFF; + bArr[index + 1] = (value >> 8) & 0xFF; + bArr[index + 2] = (value >> 16) & 0xFF; + bArr[index + 3] = (value >> 24) & 0xFF; + index += 4; + } + + return bArr; +} + +// Convert values to byte array +function convertToByteArray(value, length) { + let arr = []; + for (let i = 0; i < length; i++) { + arr.push((value >> (8 * i)) & 0xFF); + } + return arr; +} + +// Quantizer function for IIR filter +function quantizer(dArr, dArr2) { + let iArr = dArr.map(d => Math.round(d * 1073741824)); + let iArr2 = dArr2.map(d => Math.round(d * 1073741824)); + return [iArr2[0], iArr2[1], iArr2[2], -iArr[1], -iArr[2]]; +} diff --git a/assets/js/devicePEQ/wiimNetworkHandler.js b/assets/js/devicePEQ/wiimNetworkHandler.js new file mode 100644 index 0000000..27583ee --- /dev/null +++ b/assets/js/devicePEQ/wiimNetworkHandler.js @@ -0,0 +1,169 @@ +// +// Copyright 2024 : Pragmatic Audio +// +// Define the WiiM Network Handler for PEQ over HTTP API +// + +const PLUGIN_URI = "http://moddevices.com/plugins/caps/EqNp"; +const SOURCE_NAME = "wifi"; // Default source, can be changed dynamically + +export const wiimNetworkHandler = (function () { + + /** + * Fetch PEQ settings from the device + * @param {string} ip - The device IP address + * @param {number} slot - The PEQ slot (currently not used in WiiM API) + * @returns {Promise} The parsed EQ settings + */ + async function pullFromDevice(ip, slot) { + try { + const url = `https://${ip}/httpapi.asp?command=EQGetLV2SourceBandEx:${encodeURIComponent(JSON.stringify({ source_name: SOURCE_NAME, pluginURI: PLUGIN_URI }))}`; + const response = await fetch(url, { method: "GET" }); + + if (!response.ok) throw new Error(`Failed to fetch PEQ data: ${response.status}`); + + const data = await response.json(); + if (data.status !== "OK") throw new Error(`PEQ fetch failed: ${JSON.stringify(data)}`); + + console.log("WiiM PEQ Data:", data); + + const filters = parseWiiMEQData(data); + return { filters, globalGain: 0, currentSlot: slot, deviceDetails: { maxFilters: 10 } }; + + } catch (error) { + console.error("Error pulling PEQ settings from WiiM:", error); + throw error; + } + } + + /** + * Push PEQ settings to the device + * @param {string} ip - The device IP address + * @param {number} slot - The PEQ slot (currently not used in WiiM API) + * @param {number} preamp - The preamp gain + * @param {Array} filters - Array of PEQ filters + * @returns {Promise} Returns true if push was successful + */ + async function pushToDevice(ip, slot, preamp, filters) { + try { + const eqBandData = filters.map((filter, index) => ({ + index, + param_name: `${String.fromCharCode(97 + index)}_mode`, + value: filter.disabled ? -1 : convertToWiimMode(filter.type), + })); + + filters.forEach((filter, index) => { + eqBandData.push( + { index, param_name: `${String.fromCharCode(97 + index)}_freq`, value: filter.freq }, + { index, param_name: `${String.fromCharCode(97 + index)}_q`, value: filter.q }, + { index, param_name: `${String.fromCharCode(97 + index)}_gain`, value: filter.gain } + ); + }); + + const payload = { + pluginURI: PLUGIN_URI, + source_name: SOURCE_NAME, + EQBand: eqBandData, + }; + + const url = `https://${ip}/httpapi.asp?command=EQSetLV2SourceBand:${encodeURIComponent(JSON.stringify(payload))}`; + const response = await fetch(url, { method: "GET" }); + + if (!response.ok) throw new Error(`Failed to push PEQ data: ${response.status}`); + + const data = await response.json(); + if (data.status !== "OK") throw new Error(`PEQ push failed: ${JSON.stringify(data)}`); + + console.log("WiiM PEQ updated successfully:", data); + return true; + + } catch (error) { + console.error("Error pushing PEQ settings to WiiM:", error); + throw error; + } + } + + /** + * Enable or disable PEQ + * @param {string} ip - The device IP address + * @param {boolean} enabled - Whether to enable or disable PEQ + * @param {number} slotId - The PEQ slot (currently not used in WiiM API) + * @returns {Promise} + */ + async function enablePEQ(ip, enabled, slotId) { + try { + const command = enabled ? "EQChangeSourceFX" : "EQSourceOff"; + const payload = { source_name: SOURCE_NAME, pluginURI: PLUGIN_URI }; + const url = `https://${ip}/httpapi.asp?command=${command}:${encodeURIComponent(JSON.stringify(payload))}`; + const response = await fetch(url, { method: "GET" }); + + if (!response.ok) throw new Error(`Failed to ${enabled ? "enable" : "disable"} PEQ: ${response.status}`); + + const data = await response.json(); + if (data.status !== "OK") throw new Error(`PEQ ${enabled ? "enable" : "disable"} failed: ${JSON.stringify(data)}`); + + console.log(`WiiM PEQ ${enabled ? "enabled" : "disabled"} successfully`); + + } catch (error) { + console.error("Error toggling WiiM PEQ:", error); + throw error; + } + } + + /** + * Parse WiiM PEQ JSON response into a standardized format + * @param {Object} data - The WiiM PEQ data + * @returns {Array} Formatted PEQ filter list + */ + function parseWiiMEQData(data) { + const eqBands = data.EQBand || []; + const filters = []; + + for (let i = 0; i < eqBands.length; i += 4) { + const filterType = convertFromWiimMode(eqBands[i].value); + const frequency = eqBands[i + 1].value; + const qFactor = eqBands[i + 2].value; + const gain = eqBands[i + 3].value; + + filters.push({ + type: filterType, + freq: frequency, + q: qFactor, + gain: gain, + disabled: filterType === "Off", + }); + } + + return filters; + } + + /** + * Convert internal filter type to WiiM filter mode + * @param {string} type - Internal filter type (PK, LSQ, HSQ) + * @returns {number} WiiM PEQ mode value + */ + function convertToWiimMode(type) { + const mapping = { "Off": -1, "Low-Shelf": 0, "Peak": 1, "High-Shelf": 2 }; + return mapping[type] !== undefined ? mapping[type] : 1; + } + + /** + * Convert WiiM filter mode to internal filter type + * @param {number} mode - WiiM PEQ mode value + * @returns {string} Internal filter type + */ + function convertFromWiimMode(mode) { + switch (mode) { + case 0: return "Low-Shelf"; + case 1: return "Peak"; + case 2: return "High-Shelf"; + default: return "Off"; + } + } + + return { + pullFromDevice, + pushToDevice, + enablePEQ, + }; +})(); \ No newline at end of file diff --git a/equalizer.js b/assets/js/equalizer.js similarity index 100% rename from equalizer.js rename to assets/js/equalizer.js diff --git a/assets/js/fuse.min.js b/assets/js/fuse.min.js new file mode 100644 index 0000000..4072ad5 --- /dev/null +++ b/assets/js/fuse.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Fuse",[],t):"object"==typeof exports?exports.Fuse=t():e.Fuse=t()}(this,function(){return function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}return o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=1)}([function(e,t){e.exports=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===Object.prototype.toString.call(e)}},function(e,t,n){function l(e){return(l="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){for(var n=0;n 0 and <= 1");d=d.name}else s[d]={weight:1};this._analyze({key:d,value:this.options.getFn(l,d),record:l,index:c},{resultMap:r,results:o,tokenSearchers:e,fullSearcher:t})}return{weights:s,results:o}}},{key:"_analyze",value:function(e,t){var n=e.key,r=e.arrayIndex,o=void 0===r?-1:r,i=e.value,a=e.record,s=e.index,c=t.tokenSearchers,h=void 0===c?[]:c,l=t.fullSearcher,u=void 0===l?[]:l,f=t.resultMap,d=void 0===f?{}:f,v=t.results,p=void 0===v?[]:v;if(null!=i){var g=!1,y=-1,m=0;if("string"==typeof i){this._log("\nKey: ".concat(""===n?"-":n));var k=u.search(i);if(this._log('Full text: "'.concat(i,'", score: ').concat(k.score)),this.options.tokenize){for(var S=i.split(this.options.tokenSeparator),x=[],b=0;b=h.length;if(this._log("\nCheck Matches: ".concat(P)),(g||k.isMatch)&&P){var F=d[s];F?F.output.push({key:n,arrayIndex:o,value:i,score:j,matchedIndices:k.matchedIndices}):(d[s]={item:a,output:[{key:n,arrayIndex:o,value:i,score:j,matchedIndices:k.matchedIndices}]},p.push(d[s]))}}else if(E(i))for(var T=0,z=i.length;Tn)return l(e,this.pattern,r);var o=this.options,i=o.location,a=o.distance,s=o.threshold,c=o.findAllMatches,h=o.minMatchCharLength;return u(e,this.pattern,this.patternAlphabet,{location:i,distance:a,threshold:s,findAllMatches:c,minMatchCharLength:h})}}]),m}();e.exports=o},function(e,t){var l=/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g;e.exports=function(e,t){var n=2g)break;M=P}return{isMatch:0<=y,score:0===_?.001:_,matchedIndices:K(k,d)}}},function(e,t){e.exports=function(e,t){var n=t.errors,r=void 0===n?0:n,o=t.currentLocation,i=void 0===o?0:o,a=t.expectedLocation,s=void 0===a?0:a,c=t.distance,h=void 0===c?100:c,l=r/e.length,u=Math.abs(s-i);return h?l+u/h:u?1:l}},function(e,t){e.exports=function(){for(var e=0=t&&n.push([r,o]),r=-1)}return e[i-1]&&t<=i-r&&n.push([r,i-1]),n}},function(e,t){e.exports=function(e){for(var t={},n=e.length,r=0;r @@ -17,6 +19,14 @@ doc.html(` PIN + + + + + + + + @@ -31,6 +41,14 @@ doc.html(`
+ +
+ +
+ Y-axis Scale: +
+ +
@@ -79,6 +97,28 @@ doc.html(`
+
+ Preference Adjustments: +
+ + Tilt (dB/Oct) +
+
+ + Bass (dB) +
+
+ + Treble (dB) +
+
+ + Ear Gain (dB) +
+ + + +
@@ -131,20 +171,24 @@ doc.html(`