sampling-app.riot 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. <sampling-app>
  2. <header app-title="{ props.header.title }"
  3. description="{ props.header.description }"
  4. investment="{ props.investment }"
  5. periods="{ props.periods }"
  6. period="{ state.sPeriod }"
  7. on-period-change="{ onPeriodChange }"
  8. on-investment-input-change="{ onInvestmentInputChange }"
  9. on-investment-output-change="{ onInvestmentOutputChange }"
  10. labels="{ props.labels }"
  11. is="sampling-header">
  12. </header>
  13. <section class="samplers">
  14. <div each="{ sampler in this.samplers }"
  15. class="sampler-wrapper sampler-wrapper-{ sampler.id }">
  16. <div is="sampler-controls"
  17. class="reference-group"
  18. investment="{ props.investment }"
  19. referenceClasses="{ props.referenceClasses }"
  20. on-option-change="{ onOptionChange }"
  21. on-ratio-change="{ onRatioChange }"
  22. sampler="{ sampler }">
  23. </div>
  24. <sampler-visual class="sampler-visual"
  25. on-mouse-over="{ onMouseOver }"
  26. on-mouse-out="{ onMouseOut }"
  27. mouse-over="{ state.mouseOver }"
  28. classes="{ props.classes }"
  29. colours="{ state.colours }"
  30. investment-output="{ state.investment.output.value }"
  31. sampler="{ sampler }">
  32. </sampler-visual>
  33. </div>
  34. </section>
  35. <section class="textual controls">
  36. <div class="buttons size-selectors">
  37. <div each="{ gsize in props.groupSizes }"
  38. class="size-input">
  39. <input id="size-{ gsize.id }"
  40. type="radio"
  41. class="input-radio"
  42. value="{ gsize.id }"
  43. defaultChecked="{ gsize.id === state.sGroupSize.id }"
  44. name="groupSize"
  45. onchange="{ groupSizeChanged }" />
  46. <label for="size-{ gsize.id }" class="form-item">{ gsize.name }</label>
  47. </div>
  48. </div>
  49. <sampler-textual each="{ sampler in this.samplers }"
  50. id="sampler-textual-{ sampler.id }"
  51. class="sampler-textual sampler-textual-{ sampler.id }"
  52. sampler="{ sampler }"
  53. group-size-id="{ state.sGroupSize.id }"
  54. textual="{ props.textual }"
  55. colours="{ state.colours }"
  56. investment-output="{ state.investment.output.value }"
  57. mouse-over="{ state.mouseOver }">
  58. </sampler-textual>
  59. <div class="sample-button" onclick="{ sampleButtonClicked }">
  60. <button class="sample"
  61. disabled="{ maximumSamplesDrawn }">
  62. <a href="#">{ props.labels.sample } { state.investment.output.value } { props.labels.currency }?</a>
  63. </button>
  64. </div>
  65. <div class="buttons export-reset">
  66. <button id="export"
  67. class=" export form-item"
  68. disabled="{ state.isLoading }"
  69. onclick="{ generatePDF }">
  70. <a href="{ state.pdfDownloadURL }" id="exportPDF" class="export" download="{ state.pdfFilename }">
  71. <i class="demo-icon icon-download">&#xe801;</i>Faktenbox
  72. </a>
  73. </button>
  74. <button class="reset form-item"
  75. onclick="{ onReset }"
  76. disabled="{ resetEnabled }">
  77. <a href="#"><i class="demo-icon icon-ccw">&#xe802;</i>{ props.labels.reset }</a>
  78. </button>
  79. </div>
  80. </section>
  81. <script charset="utf-8">
  82. import '@babel/polyfill';
  83. import * as riot from 'riot';
  84. import * as d3 from '../d3custom';
  85. import { Sampler } from '../sampler';
  86. import { PDFExport } from '../pdfExport';
  87. import visual_config from './config_visual.json';
  88. import info from './sampling-info.riot'; // built from /data/sampling-info.md by bin/preprocess.js
  89. import header from './sampling-header.riot';
  90. import textual from './sampler-textual.riot';
  91. import controls from './sampler-controls.riot';
  92. import visual from './sampler-visual.riot';
  93. riot.register('sampling-header', header);
  94. riot.register('sampling-info', info);
  95. riot.register('sampler-controls', controls);
  96. riot.register('sampler-textual', textual);
  97. riot.register('sampler-visual', visual);
  98. export default {
  99. // life cycle methods
  100. onBeforeMount(props, state) {
  101. state.investment = props.investment;
  102. state.period = props.period;
  103. this.state.isLoading = false;
  104. // load defaults: selected values / elements
  105. state.mouseOver = false;
  106. state.sPeriod = props.periods[props.defaults.periodId];
  107. state.sGroupSize = props.groupSizes[props.defaults.groupSizeId];
  108. state.colours = props.classes.map(c => c.colour);
  109. this.fetchData(props.dataFilePath);
  110. document.title = props.header.title;
  111. },
  112. // fetch all data once
  113. fetchData(filepath) { //-> initialise, create samplers, samplers call controller.filterData(s.o1, s.o2)
  114. d3.tsv(filepath).then(this.initialise);
  115. },
  116. // filter data for sampler with primaryKeys, make sure there is also data for opposing sampler (secondaryKeys)
  117. filterData(primaryKeys, secondaryKeys) {
  118. function parseVal(val) {
  119. return parseFloat(val.replace(',', '.'));
  120. }
  121. let filteredData = this.data
  122. .filter(
  123. d => d[primaryKeys[0]] && d[primaryKeys[1]] &&
  124. d[secondaryKeys[0]] && d[secondaryKeys[1]]) // only return data if row contains values for both options for both samplers
  125. .map(d => {
  126. return {
  127. year: d.date_of_investment,
  128. values: [parseVal(d[primaryKeys[0]]), parseVal(d[primaryKeys[1]])]
  129. }
  130. });
  131. return filteredData;
  132. },
  133. // Create new data set for sampler identified by samplerId
  134. queryData(samplerId, leftOptions, rightOptions) {
  135. return this.filterData(
  136. [
  137. this.createDataKey(leftOptions[samplerId].name),
  138. this.createDataKey(rightOptions[samplerId].name)
  139. ], [
  140. this.createDataKey(leftOptions[(samplerId + 1) % 2].name),
  141. this.createDataKey(rightOptions[(samplerId + 1) % 2].name)
  142. ]
  143. );
  144. },
  145. createDataKey(name) {
  146. return this.state.sPeriod.name + '_' + name;
  147. },
  148. getLeftOptions() {
  149. if (this.samplers === undefined) {
  150. return [
  151. this.props.investment.options[this.props.defaults.selectedOptionsIds[0][0]],
  152. this.props.investment.options[this.props.defaults.selectedOptionsIds[1][0]]
  153. ];
  154. }
  155. return [
  156. this.samplers[0].investment.options[0],
  157. this.samplers[1].investment.options[0]
  158. ];
  159. },
  160. getRightOptions() {
  161. if (this.samplers === undefined) {
  162. return [
  163. this.props.investment.options[this.props.defaults.selectedOptionsIds[0][1]],
  164. this.props.investment.options[this.props.defaults.selectedOptionsIds[1][1]]
  165. ];
  166. }
  167. return [
  168. this.samplers[0].investment.options[1],
  169. this.samplers[1].investment.options[1]
  170. ];
  171. },
  172. getRatio(i) {
  173. if(this.samplers === undefined) {
  174. return this.props.defaults.ratios[i];
  175. }
  176. return this.samplers[i].investment.ratio;
  177. },
  178. createSamplers() {
  179. let samplers = [];
  180. let leftOptions = this.getLeftOptions();
  181. let rightOptions = this.getRightOptions();
  182. for (let i = 0; i < 2; ++i) {
  183. samplers.push(new Sampler(
  184. i,
  185. this.queryData(i, leftOptions, rightOptions),
  186. this.props.classes,
  187. {
  188. ratio: this.getRatio(i),
  189. inverseRatio: 1 - this.getRatio(i), // inverted <input type="range"> bar!!
  190. input: this.props.investment.input,
  191. output: this.props.investment.output,
  192. options: [leftOptions[i], rightOptions[i]]
  193. },
  194. this.props.maxSamples
  195. ));
  196. }
  197. return samplers;
  198. },
  199. initialise(data) {
  200. this.data = data;
  201. this.samplers = this.createSamplers();
  202. // define getters for template updating only after 'this.props' is defined
  203. Object.defineProperty(this, 'maximumSamplesDrawn', { get: this.maximumSamplesDrawn });
  204. Object.defineProperty(this, 'resetEnabled', { get: this.resetEnabled });
  205. this.resetSamplers();
  206. },
  207. drawSamples(amount, samplers) {
  208. if (samplers[0].data.length !== samplers[1].data.length) {
  209. throw new Error('Data arrays of samplers are not of equal size: ' + samplers[0].data.length + ', ' + samplers[1].data.length);
  210. }
  211. for (let i = 0; i < amount; ++i) {
  212. // Importance sampling implementation
  213. let cumulatedProb = 0.0;
  214. let random = Math.random();
  215. let prob = 1.0 / samplers[0].data.length;
  216. let index = 0;
  217. for (; index < samplers[0].data.length; ++index) {
  218. cumulatedProb += prob;
  219. if (random < cumulatedProb) {
  220. break;
  221. }
  222. }
  223. for (let sampler of samplers) {
  224. sampler.drawSample(index);
  225. }
  226. }
  227. },
  228. clearSamples() {
  229. for (let sampler of this.samplers) {
  230. sampler.reset();
  231. }
  232. },
  233. // Generates pdf on button click
  234. generatePDF(e) {
  235. if (e.hasBeenProcessed === true) { // Discard event and do nothing if the event already has been processed
  236. e.stopImmediatePropagation();
  237. this.$('#exportPDF').classList.remove('loading');
  238. this.update({
  239. isLoading: false
  240. });
  241. return;
  242. }
  243. this.$('#exportPDF').classList.add('loading');
  244. this.update({
  245. isLoading: true
  246. });
  247. e.stopPropagation(); // Stop event propagation
  248. let pdfSamplers = this.createSamplers();
  249. this.drawSamples(this.props.maxSamples, pdfSamplers);
  250. // Create new PDFExport and pass all necessary data and the click event
  251. this.pdfx = new PDFExport(
  252. this,
  253. {
  254. title: this.props.header.title,
  255. clickEvent: e,
  256. description: this.props.header.description,
  257. referenceClasses: this.props.referenceClasses,
  258. author: this.props.author,
  259. subject: this.props.subject,
  260. classes: this.props.classes,
  261. colours: this.state.colours,
  262. context: this.props.context,
  263. fontRatios: this.props.fontRatios,
  264. iconArray: {
  265. blocks: visual_config.blocks,
  266. dotspacingRatio: visual_config.dotspacingRatio,
  267. radius: visual_config.radius
  268. },
  269. investment: this.props.investment,
  270. lang: this.lang,
  271. labels: this.props.labels,
  272. maxSamples: this.props.maxSamples,
  273. samplers: pdfSamplers,
  274. selectedPeriod: this.state.sPeriod
  275. }
  276. );
  277. },
  278. /**
  279. * Called from pdf generation code
  280. * The event that has been triggered to start the pdf generation process
  281. * is dispatched so that the pdf download happens after the variables have been assigned
  282. *
  283. * @param url: Blob url
  284. * @param e: Event object
  285. */
  286. updatePDFLink(url, e) {
  287. this.update({
  288. pdfDownloadURL: url,
  289. pdfFilename: `${this.props.header.title}.pdf`
  290. });
  291. let anchor = this.$('#exportPDF');
  292. anchor.classList.remove('loading');
  293. e.hasBeenProcessed = true; // indicate that the event has been processed and should be discarded (otherwise the pdf would be downloaded indefinitely)
  294. anchor.dispatchEvent(e);
  295. this.update({
  296. isLoading: false
  297. });
  298. },
  299. resetSamplers() {
  300. this.clearSamples();
  301. this.update();
  302. },
  303. maximumSamplesDrawn() {
  304. let sampler = this.samplers[0]; // Both samplers always have the same amount of samples and maxSamples
  305. return sampler.samples.length === sampler.maxSamples;
  306. },
  307. resetEnabled() {
  308. let result = false;
  309. if (this.samplers !== undefined) {
  310. result = this.samplers[0].samples.length === 0;
  311. }
  312. return result;
  313. },
  314. sampleButtonClicked(e) {
  315. e.preventDefault();
  316. let groupSize = this.state.sGroupSize.value;
  317. let remaining = this.samplers[0].maxSamples - this.samplers[0].samples.length;
  318. let amount = Math.min(remaining, groupSize);
  319. this.drawSamples(amount, this.samplers);
  320. this.update();
  321. },
  322. groupSizeChanged(e) {
  323. this.state.sGroupSize = this.props.groupSizes[parseInt(e.target.value)];
  324. this.update();
  325. },
  326. // events
  327. onPeriodChange (e) {
  328. this.state.sPeriod = this.props.periods[e.target.selectedIndex];
  329. for (let sampler of this.samplers) {
  330. sampler.data = this.queryData(sampler.id, this.getLeftOptions(), this.getRightOptions());
  331. }
  332. this.resetSamplers();
  333. },
  334. onMouseOver (e, idx) {
  335. this.state.mouseOver = true;
  336. for (let sampler of this.samplers) {
  337. sampler.setCurrentSample(idx);
  338. }
  339. this.update();
  340. },
  341. onMouseOut (e, idx) {
  342. this.state.mouseOver = false;
  343. for (let sampler of this.samplers) {
  344. sampler.setCurrentSample(sampler.samples.length - 1);
  345. }
  346. this.update();
  347. },
  348. onInvestmentInputChange (e) {
  349. let newVal = Math.max(Math.min(e.target.valueAsNumber, this.state.investment.input.max), this.state.investment.input.min);
  350. this.state.investment.input.value = newVal;
  351. this.resetSamplers();
  352. },
  353. onInvestmentOutputChange (e) {
  354. let newVal = Math.max(Math.min(e.target.valueAsNumber, this.state.investment.output.max), this.state.investment.output.min);
  355. this.state.investment.output.value = newVal
  356. this.resetSamplers();
  357. },
  358. onOptionChange(e, sampler) {
  359. let leftOptions = this.getLeftOptions();
  360. let rightOptions = this.getRightOptions();
  361. // Set new data for both samplers, as data arrays must be of equal lengths
  362. for (let s of this.samplers) {
  363. s.data = this.queryData(s.id, leftOptions, rightOptions);
  364. }
  365. this.resetSamplers();
  366. },
  367. onRatioChange(e) {
  368. this.resetSamplers();
  369. },
  370. onReset(e) {
  371. e.preventDefault();
  372. this.resetSamplers();
  373. }
  374. }
  375. </script>
  376. </sampling-app>