@spinnaker/kayenta 0.0.0-2025.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.js +1 -0
  3. package/.huskyrc +5 -0
  4. package/.lintstagedrc.json +4 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc.js +1 -0
  7. package/LICENSE.txt +203 -0
  8. package/README.md +81 -0
  9. package/__mocks__/styleMock.js +1 -0
  10. package/__mocks__/version.json +4 -0
  11. package/babel.config.js +3 -0
  12. package/build.gradle +67 -0
  13. package/build_scripts/checkLicenses.js +79 -0
  14. package/gradle.properties +0 -0
  15. package/jest.config.js +204 -0
  16. package/jest.setup.js +5 -0
  17. package/package.json +166 -0
  18. package/rollup-plugin-angularjs-template-loader.js +82 -0
  19. package/rollup.config.js +30 -0
  20. package/src/index.ts +2 -0
  21. package/src/kayenta/actions/creators.ts +163 -0
  22. package/src/kayenta/actions/index.ts +98 -0
  23. package/src/kayenta/canary.dataSource.bridge.ts +53 -0
  24. package/src/kayenta/canary.dataSource.stub.ts +64 -0
  25. package/src/kayenta/canary.help.ts +136 -0
  26. package/src/kayenta/canary.less +168 -0
  27. package/src/kayenta/canary.settings.ts +26 -0
  28. package/src/kayenta/canary.tsx +67 -0
  29. package/src/kayenta/components/canaryScore.component.less +77 -0
  30. package/src/kayenta/components/canaryScore.component.ts +12 -0
  31. package/src/kayenta/components/canaryScore.tsx +63 -0
  32. package/src/kayenta/components/canaryScores.component.ts +20 -0
  33. package/src/kayenta/components/canaryScores.less +22 -0
  34. package/src/kayenta/components/canaryScores.tsx +163 -0
  35. package/src/kayenta/components/loadStates.tsx +52 -0
  36. package/src/kayenta/domain/ICanaryConfig.ts +57 -0
  37. package/src/kayenta/domain/ICanaryConfigSummary.ts +7 -0
  38. package/src/kayenta/domain/ICanaryConfigUpdateResponse.ts +3 -0
  39. package/src/kayenta/domain/ICanaryExecutionStatusResult.ts +72 -0
  40. package/src/kayenta/domain/ICanaryJudgeResult.ts +51 -0
  41. package/src/kayenta/domain/ICanaryJudgeResultSummary.ts +5 -0
  42. package/src/kayenta/domain/ICanaryScoreThresholds.ts +4 -0
  43. package/src/kayenta/domain/IJudge.ts +5 -0
  44. package/src/kayenta/domain/IKayentaAccount.ts +14 -0
  45. package/src/kayenta/domain/IKayentaStageConfig.ts +58 -0
  46. package/src/kayenta/domain/IMetricSetPair.ts +17 -0
  47. package/src/kayenta/domain/IMetricsServiceMetadata.ts +2 -0
  48. package/src/kayenta/domain/ISetupCanaryStage.ts +11 -0
  49. package/src/kayenta/domain/MetricClassificationLabel.ts +8 -0
  50. package/src/kayenta/domain/ScoreClassificationLabel.ts +7 -0
  51. package/src/kayenta/domain/index.ts +15 -0
  52. package/src/kayenta/edit/changeMetricGroupModal.tsx +107 -0
  53. package/src/kayenta/edit/configDetail.tsx +31 -0
  54. package/src/kayenta/edit/configDetailActionButtons.tsx +24 -0
  55. package/src/kayenta/edit/configDetailHeader.tsx +104 -0
  56. package/src/kayenta/edit/configDetailLoadStates.tsx +36 -0
  57. package/src/kayenta/edit/configDetailLoader.tsx +60 -0
  58. package/src/kayenta/edit/configJson.less +42 -0
  59. package/src/kayenta/edit/configJsonModal.tsx +158 -0
  60. package/src/kayenta/edit/configList.less +7 -0
  61. package/src/kayenta/edit/configList.tsx +57 -0
  62. package/src/kayenta/edit/copyConfigButton.tsx +34 -0
  63. package/src/kayenta/edit/createConfigButton.tsx +34 -0
  64. package/src/kayenta/edit/deleteModal.tsx +87 -0
  65. package/src/kayenta/edit/edit.tsx +24 -0
  66. package/src/kayenta/edit/editMetricEffectSizes.tsx +186 -0
  67. package/src/kayenta/edit/editMetricModal.less +9 -0
  68. package/src/kayenta/edit/editMetricModal.spec.tsx +129 -0
  69. package/src/kayenta/edit/editMetricModal.tsx +294 -0
  70. package/src/kayenta/edit/editMetricValidation.spec.ts +63 -0
  71. package/src/kayenta/edit/editMetricValidation.ts +50 -0
  72. package/src/kayenta/edit/filterTemplateSelector.less +15 -0
  73. package/src/kayenta/edit/filterTemplateSelector.spec.tsx +106 -0
  74. package/src/kayenta/edit/filterTemplateSelector.tsx +194 -0
  75. package/src/kayenta/edit/filterTemplatesValidation.spec.ts +108 -0
  76. package/src/kayenta/edit/filterTemplatesValidation.ts +95 -0
  77. package/src/kayenta/edit/footer.less +30 -0
  78. package/src/kayenta/edit/footer.tsx +12 -0
  79. package/src/kayenta/edit/groupName.tsx +80 -0
  80. package/src/kayenta/edit/groupTabs.tsx +106 -0
  81. package/src/kayenta/edit/groupWeight.tsx +80 -0
  82. package/src/kayenta/edit/groupWeights.tsx +38 -0
  83. package/src/kayenta/edit/inlineTemplateEditor.spec.tsx +42 -0
  84. package/src/kayenta/edit/inlineTemplateEditor.tsx +61 -0
  85. package/src/kayenta/edit/judgeSelect.tsx +82 -0
  86. package/src/kayenta/edit/metricConfigurerDelegator.tsx +30 -0
  87. package/src/kayenta/edit/metricList.less +21 -0
  88. package/src/kayenta/edit/metricList.tsx +215 -0
  89. package/src/kayenta/edit/metricStoreSelector.tsx +66 -0
  90. package/src/kayenta/edit/nameAndDescription.tsx +90 -0
  91. package/src/kayenta/edit/openConfigJsonModalButton.tsx +33 -0
  92. package/src/kayenta/edit/openDeleteModalButton.tsx +50 -0
  93. package/src/kayenta/edit/ownedBy.tsx +34 -0
  94. package/src/kayenta/edit/save.tsx +19 -0
  95. package/src/kayenta/edit/saveConfigButton.tsx +65 -0
  96. package/src/kayenta/edit/saveConfigError.tsx +59 -0
  97. package/src/kayenta/edit/scoring.tsx +35 -0
  98. package/src/kayenta/edit/selectConfig.tsx +10 -0
  99. package/src/kayenta/edit/validationErrors.tsx +39 -0
  100. package/src/kayenta/index.ts +6 -0
  101. package/src/kayenta/layout/addNewButton.tsx +20 -0
  102. package/src/kayenta/layout/centeredDetail.tsx +13 -0
  103. package/src/kayenta/layout/deleteButton.tsx +11 -0
  104. package/src/kayenta/layout/disableable.tsx +87 -0
  105. package/src/kayenta/layout/formList.tsx +26 -0
  106. package/src/kayenta/layout/formRow.tsx +36 -0
  107. package/src/kayenta/layout/formattedDate.tsx +14 -0
  108. package/src/kayenta/layout/index.ts +2 -0
  109. package/src/kayenta/layout/keyValueList.less +20 -0
  110. package/src/kayenta/layout/keyValueList.tsx +114 -0
  111. package/src/kayenta/layout/list.less +9 -0
  112. package/src/kayenta/layout/list.spec.tsx +83 -0
  113. package/src/kayenta/layout/list.tsx +73 -0
  114. package/src/kayenta/layout/listDetail.tsx +33 -0
  115. package/src/kayenta/layout/radioChoice.tsx +29 -0
  116. package/src/kayenta/layout/styleguide.tsx +16 -0
  117. package/src/kayenta/layout/table/index.ts +5 -0
  118. package/src/kayenta/layout/table/nativeTable.tsx +51 -0
  119. package/src/kayenta/layout/table/nativeTableHeader.tsx +26 -0
  120. package/src/kayenta/layout/table/table.tsx +56 -0
  121. package/src/kayenta/layout/table/tableColumn.ts +7 -0
  122. package/src/kayenta/layout/table/tableHeader.tsx +23 -0
  123. package/src/kayenta/layout/tabs.tsx +26 -0
  124. package/src/kayenta/layout/titledSection.less +16 -0
  125. package/src/kayenta/layout/titledSection.tsx +20 -0
  126. package/src/kayenta/layout/titledSubsection.less +11 -0
  127. package/src/kayenta/layout/titledSubsection.tsx +22 -0
  128. package/src/kayenta/manualAnalysis/ManualAnalysisModal.tsx +716 -0
  129. package/src/kayenta/metricStore/atlas/atlasMetricConfigurer.tsx +130 -0
  130. package/src/kayenta/metricStore/atlas/index.ts +8 -0
  131. package/src/kayenta/metricStore/datadog/domain/IDatadogMetricDescriptor.ts +5 -0
  132. package/src/kayenta/metricStore/datadog/index.ts +9 -0
  133. package/src/kayenta/metricStore/datadog/metricConfigurer.tsx +90 -0
  134. package/src/kayenta/metricStore/datadog/metricTypeSelector.spec.tsx +59 -0
  135. package/src/kayenta/metricStore/datadog/metricTypeSelector.tsx +73 -0
  136. package/src/kayenta/metricStore/graphite/domain/IGraphiteMetricDescriptor.ts +5 -0
  137. package/src/kayenta/metricStore/graphite/index.ts +8 -0
  138. package/src/kayenta/metricStore/graphite/metricConfigurer.tsx +54 -0
  139. package/src/kayenta/metricStore/graphite/metricTypeSelector.tsx +80 -0
  140. package/src/kayenta/metricStore/graphite/typeahead.less +3 -0
  141. package/src/kayenta/metricStore/index.ts +8 -0
  142. package/src/kayenta/metricStore/metricStoreConfig.service.ts +12 -0
  143. package/src/kayenta/metricStore/newrelic/domain/INewRelicMetricDescriptor.ts +5 -0
  144. package/src/kayenta/metricStore/newrelic/index.ts +8 -0
  145. package/src/kayenta/metricStore/newrelic/metricConfigurer.tsx +58 -0
  146. package/src/kayenta/metricStore/prometheus/domain/IPrometheusCanaryMetricSetQueryConfig.ts +14 -0
  147. package/src/kayenta/metricStore/prometheus/domain/IPrometheusMetricDescriptor.ts +5 -0
  148. package/src/kayenta/metricStore/prometheus/index.ts +12 -0
  149. package/src/kayenta/metricStore/prometheus/metricConfigurer.tsx +157 -0
  150. package/src/kayenta/metricStore/prometheus/metricTypeSelector.less +5 -0
  151. package/src/kayenta/metricStore/prometheus/metricTypeSelector.spec.tsx +62 -0
  152. package/src/kayenta/metricStore/prometheus/metricTypeSelector.tsx +144 -0
  153. package/src/kayenta/metricStore/prometheus/queryTypeSelectors.spec.ts +61 -0
  154. package/src/kayenta/metricStore/prometheus/queryTypeSelectors.ts +38 -0
  155. package/src/kayenta/metricStore/signalfx/domain/ISignalFxCanaryMetricSetQueryConfig.ts +7 -0
  156. package/src/kayenta/metricStore/signalfx/index.ts +8 -0
  157. package/src/kayenta/metricStore/signalfx/metricConfigurer.less +10 -0
  158. package/src/kayenta/metricStore/signalfx/metricConfigurer.tsx +187 -0
  159. package/src/kayenta/metricStore/stackdriver/domain/IStackdriverCanaryMetricSetQueryConfig.ts +9 -0
  160. package/src/kayenta/metricStore/stackdriver/domain/IStackdriverMetricDescriptor.ts +17 -0
  161. package/src/kayenta/metricStore/stackdriver/index.ts +12 -0
  162. package/src/kayenta/metricStore/stackdriver/metricConfigurer.tsx +144 -0
  163. package/src/kayenta/metricStore/stackdriver/metricTypeSelector.spec.tsx +92 -0
  164. package/src/kayenta/metricStore/stackdriver/metricTypeSelector.tsx +113 -0
  165. package/src/kayenta/middleware/actionInterceptor.ts +29 -0
  166. package/src/kayenta/middleware/asyncDispatch.ts +37 -0
  167. package/src/kayenta/middleware/epics.ts +211 -0
  168. package/src/kayenta/middleware/index.ts +3 -0
  169. package/src/kayenta/navigation/canary.states.stub.ts +28 -0
  170. package/src/kayenta/navigation/canary.states.ts +182 -0
  171. package/src/kayenta/reducers/app.ts +56 -0
  172. package/src/kayenta/reducers/asyncRequest.ts +5 -0
  173. package/src/kayenta/reducers/data.ts +169 -0
  174. package/src/kayenta/reducers/editingTemplate.ts +54 -0
  175. package/src/kayenta/reducers/group.ts +82 -0
  176. package/src/kayenta/reducers/index.ts +245 -0
  177. package/src/kayenta/reducers/prometheusMetricConfig.spec.ts +33 -0
  178. package/src/kayenta/reducers/prometheusMetricConfig.ts +56 -0
  179. package/src/kayenta/reducers/selectedConfig.spec.ts +190 -0
  180. package/src/kayenta/reducers/selectedConfig.ts +566 -0
  181. package/src/kayenta/reducers/selectedRun.ts +101 -0
  182. package/src/kayenta/reducers/signalFxMetricConfig.ts +36 -0
  183. package/src/kayenta/reducers/stackdriverMetricConfig.spec.ts +33 -0
  184. package/src/kayenta/reducers/stackdriverMetricConfig.ts +41 -0
  185. package/src/kayenta/reducers/templates.spec.ts +192 -0
  186. package/src/kayenta/reducers/validators.ts +118 -0
  187. package/src/kayenta/report/detail/allMetricResultsHeader.tsx +32 -0
  188. package/src/kayenta/report/detail/clickableHeader.tsx +21 -0
  189. package/src/kayenta/report/detail/colors.ts +47 -0
  190. package/src/kayenta/report/detail/detail.less +16 -0
  191. package/src/kayenta/report/detail/detail.tsx +48 -0
  192. package/src/kayenta/report/detail/detailLoader.tsx +55 -0
  193. package/src/kayenta/report/detail/graph/graph.tsx +37 -0
  194. package/src/kayenta/report/detail/graph/metricSetPairGraph.service.ts +35 -0
  195. package/src/kayenta/report/detail/graph/semiotic/boxplot.less +45 -0
  196. package/src/kayenta/report/detail/graph/semiotic/boxplot.tsx +283 -0
  197. package/src/kayenta/report/detail/graph/semiotic/chartHeader.tsx +19 -0
  198. package/src/kayenta/report/detail/graph/semiotic/chartLegend.less +26 -0
  199. package/src/kayenta/report/detail/graph/semiotic/chartLegend.tsx +42 -0
  200. package/src/kayenta/report/detail/graph/semiotic/circleIcon.tsx +16 -0
  201. package/src/kayenta/report/detail/graph/semiotic/config.less +5 -0
  202. package/src/kayenta/report/detail/graph/semiotic/config.ts +38 -0
  203. package/src/kayenta/report/detail/graph/semiotic/customAxisTickLabel.tsx +17 -0
  204. package/src/kayenta/report/detail/graph/semiotic/declarations/labella.d.ts +16 -0
  205. package/src/kayenta/report/detail/graph/semiotic/declarations/react-container-dimensions.d.ts +3 -0
  206. package/src/kayenta/report/detail/graph/semiotic/declarations/semiotic.d.ts +160 -0
  207. package/src/kayenta/report/detail/graph/semiotic/differenceArea.less +17 -0
  208. package/src/kayenta/report/detail/graph/semiotic/differenceArea.tsx +186 -0
  209. package/src/kayenta/report/detail/graph/semiotic/histogram.less +22 -0
  210. package/src/kayenta/report/detail/graph/semiotic/histogram.tsx +251 -0
  211. package/src/kayenta/report/detail/graph/semiotic/index.tsx +19 -0
  212. package/src/kayenta/report/detail/graph/semiotic/noValidDataSign.less +5 -0
  213. package/src/kayenta/report/detail/graph/semiotic/noValidDataSign.tsx +10 -0
  214. package/src/kayenta/report/detail/graph/semiotic/secondaryTSXAxis.less +6 -0
  215. package/src/kayenta/report/detail/graph/semiotic/secondaryTSXAxis.tsx +58 -0
  216. package/src/kayenta/report/detail/graph/semiotic/semiotic.service.ts +32 -0
  217. package/src/kayenta/report/detail/graph/semiotic/semioticGraph.less +53 -0
  218. package/src/kayenta/report/detail/graph/semiotic/semioticGraph.tsx +49 -0
  219. package/src/kayenta/report/detail/graph/semiotic/timeSeries.less +42 -0
  220. package/src/kayenta/report/detail/graph/semiotic/timeSeries.tsx +473 -0
  221. package/src/kayenta/report/detail/graph/semiotic/tooltip.tsx +55 -0
  222. package/src/kayenta/report/detail/graph/semiotic/utils.ts +90 -0
  223. package/src/kayenta/report/detail/graphTypeSelector.less +4 -0
  224. package/src/kayenta/report/detail/graphTypeSelector.tsx +50 -0
  225. package/src/kayenta/report/detail/groupScores.tsx +68 -0
  226. package/src/kayenta/report/detail/header.less +70 -0
  227. package/src/kayenta/report/detail/header.tsx +39 -0
  228. package/src/kayenta/report/detail/headerArrow.tsx +13 -0
  229. package/src/kayenta/report/detail/loadStates.tsx +31 -0
  230. package/src/kayenta/report/detail/metricResultActions.less +29 -0
  231. package/src/kayenta/report/detail/metricResultActions.tsx +87 -0
  232. package/src/kayenta/report/detail/metricResultClassification.tsx +22 -0
  233. package/src/kayenta/report/detail/metricResultDetail.tsx +20 -0
  234. package/src/kayenta/report/detail/metricResultDetailLayout.tsx +19 -0
  235. package/src/kayenta/report/detail/metricResultDeviation.tsx +25 -0
  236. package/src/kayenta/report/detail/metricResultStats.less +9 -0
  237. package/src/kayenta/report/detail/metricResultStats.tsx +120 -0
  238. package/src/kayenta/report/detail/metricResults.less +12 -0
  239. package/src/kayenta/report/detail/metricResults.tsx +52 -0
  240. package/src/kayenta/report/detail/metricResultsClassificationFilters.tsx +65 -0
  241. package/src/kayenta/report/detail/metricResultsColumns.tsx +27 -0
  242. package/src/kayenta/report/detail/metricResultsList.less +44 -0
  243. package/src/kayenta/report/detail/metricResultsList.tsx +120 -0
  244. package/src/kayenta/report/detail/metricSetPairLoadStates.tsx +22 -0
  245. package/src/kayenta/report/detail/multipleResultsTable.tsx +81 -0
  246. package/src/kayenta/report/detail/reportException.tsx +57 -0
  247. package/src/kayenta/report/detail/reportExplanation.less +12 -0
  248. package/src/kayenta/report/detail/reportExplanation.tsx +32 -0
  249. package/src/kayenta/report/detail/reportMetadata.tsx +167 -0
  250. package/src/kayenta/report/detail/reportScores.less +47 -0
  251. package/src/kayenta/report/detail/reportScores.tsx +80 -0
  252. package/src/kayenta/report/detail/score.tsx +33 -0
  253. package/src/kayenta/report/detail/sourceLinks.tsx +69 -0
  254. package/src/kayenta/report/list/configLink.tsx +32 -0
  255. package/src/kayenta/report/list/executionList.less +7 -0
  256. package/src/kayenta/report/list/loadStates.tsx +32 -0
  257. package/src/kayenta/report/list/pipelineLink.tsx +15 -0
  258. package/src/kayenta/report/list/reportLink.tsx +33 -0
  259. package/src/kayenta/report/list/table.tsx +309 -0
  260. package/src/kayenta/report/report.tsx +11 -0
  261. package/src/kayenta/selectors/filterTemplatesSelectors.ts +87 -0
  262. package/src/kayenta/selectors/index.ts +62 -0
  263. package/src/kayenta/service/canaryConfig.service.ts +122 -0
  264. package/src/kayenta/service/canaryRun.service.ts +60 -0
  265. package/src/kayenta/service/delegateFactory.ts +24 -0
  266. package/src/kayenta/service/metricsServiceMetadata.service.ts +9 -0
  267. package/src/kayenta/stages/kayentaStage/AnalysisType.spec.tsx +47 -0
  268. package/src/kayenta/stages/kayentaStage/AnalysisType.tsx +49 -0
  269. package/src/kayenta/stages/kayentaStage/CanaryExecutionLabel.tsx +26 -0
  270. package/src/kayenta/stages/kayentaStage/analysisType.component.ts +12 -0
  271. package/src/kayenta/stages/kayentaStage/canaryRunSummaries.component.ts +12 -0
  272. package/src/kayenta/stages/kayentaStage/canaryRunSummaries.less +5 -0
  273. package/src/kayenta/stages/kayentaStage/canaryRunSummaries.tsx +136 -0
  274. package/src/kayenta/stages/kayentaStage/forAnalysisType.component.ts +45 -0
  275. package/src/kayenta/stages/kayentaStage/kayentaStage.controller.ts +789 -0
  276. package/src/kayenta/stages/kayentaStage/kayentaStage.html +528 -0
  277. package/src/kayenta/stages/kayentaStage/kayentaStage.less +5 -0
  278. package/src/kayenta/stages/kayentaStage/kayentaStage.transformer.ts +179 -0
  279. package/src/kayenta/stages/kayentaStage/kayentaStage.ts +221 -0
  280. package/src/kayenta/stages/kayentaStage/kayentaStageConfigSection.component.ts +21 -0
  281. package/src/kayenta/stages/kayentaStage/kayentaStageExecutionDetails.controller.ts +88 -0
  282. package/src/kayenta/stages/kayentaStage/kayentaStageExecutionDetails.html +114 -0
  283. package/src/kayenta/stages/kayentaStage/kayentaStageExecutionDetails.less +6 -0
  284. package/src/kayenta/stages/kayentaStage/stageTypes.ts +5 -0
  285. package/src/kayenta/utils/duration.spec.ts +69 -0
  286. package/src/kayenta/utils/duration.ts +48 -0
  287. package/src/lazy.ts +29 -0
  288. package/src/stub.ts +60 -0
  289. package/tsconfig.json +11 -0
@@ -0,0 +1,42 @@
1
+ @import './config.less';
2
+
3
+ .time-series {
4
+ .xyframe-matte {
5
+ fill: @background-color;
6
+ }
7
+
8
+ .zoom-icon {
9
+ bottom: 10px;
10
+ left: 30px;
11
+ position: absolute;
12
+ color: var(--color-dove-gray);
13
+ }
14
+
15
+ .tooltip-dual-axis-row {
16
+ margin-bottom: 10px;
17
+ }
18
+
19
+ .tooltip-ts {
20
+ font-weight: 600;
21
+ }
22
+
23
+ .axis-title.x.axis.bottom {
24
+ text {
25
+ transform: translateY(-8px);
26
+ }
27
+ }
28
+
29
+ .canary-dual-axis {
30
+ .axis-label,
31
+ .axis-title text {
32
+ fill: @canary-color;
33
+ }
34
+ }
35
+
36
+ .baseline-dual-axis {
37
+ .axis-label,
38
+ .axis-title text {
39
+ fill: @baseline-color;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,473 @@
1
+ import classNames from 'classnames';
2
+ import { scaleUtc } from 'd3-scale';
3
+ import { curveStepAfter } from 'd3-shape';
4
+ import { IMetricSetPair } from 'kayenta/domain/IMetricSetPair';
5
+ import * as React from 'react';
6
+ import { IMinimapProps, IXYFrameHoverBaseArgs, IXYFrameProps, MinimapXYFrame, XYFrame } from 'semiotic';
7
+
8
+ import { SETTINGS, timestamp } from '@spinnaker/core';
9
+
10
+ import ChartHeader from './chartHeader';
11
+ import ChartLegend from './chartLegend';
12
+ import CircleIcon from './circleIcon';
13
+ import { vizConfig } from './config';
14
+ import CustomAxisTickLabel from './customAxisTickLabel';
15
+ import DifferenceArea from './differenceArea';
16
+ import SecondaryTSXAxis from './secondaryTSXAxis';
17
+ import { IMargin, ISemioticChartProps, ITooltip } from './semiotic.service';
18
+ import Tooltip from './tooltip';
19
+ import * as utils from './utils';
20
+
21
+ import './timeSeries.less';
22
+
23
+ const { defaultTimeZone } = SETTINGS;
24
+
25
+ interface IDataPoint {
26
+ value: number | null;
27
+ timestampMillisActual: number;
28
+ timestampMillisNormalizedMain: number;
29
+ timestampMillisNormalizedMinimap: number;
30
+ }
31
+
32
+ interface IChartDataSet {
33
+ color: string;
34
+ label: string;
35
+ coordinates: IDataPoint[];
36
+ coordinatesUnfiltered: IDataPoint[];
37
+ }
38
+
39
+ interface IChartData {
40
+ dataSets: IChartDataSet[];
41
+ xExtentNormalizedMinimap: number[];
42
+ xExtentNormalizedMain: number[];
43
+ millisSetNormalizedMain: number[];
44
+ millisSetNormalizedMinimap: number[];
45
+ }
46
+
47
+ interface ITimeSeriesState {
48
+ tooltip: ITooltip;
49
+ userBrushExtent: Date[] | null;
50
+ showGroup: { [group: string]: boolean };
51
+ graphs: { [graph: string]: JSX.Element };
52
+ }
53
+
54
+ interface ITooltipDataPoint {
55
+ color: string;
56
+ label: string;
57
+ value: number | undefined;
58
+ ts: number;
59
+ }
60
+
61
+ interface IDataSetsAttributes {
62
+ startTimeMillisOffset: number;
63
+ maxDataCount: number;
64
+ shouldDisplayMinimap: boolean;
65
+ shouldUseSecondaryXAxis: boolean;
66
+ }
67
+
68
+ /*There are 3 vizualisations inside this component:
69
+ 1. The main Timeseries chart
70
+ 2. The minimap chart for users to zoom/ pan the main timeseries. Only show this if there are many data points
71
+ 3. The difference chart that displays the diff between canary and baseline
72
+ */
73
+ export default class TimeSeries extends React.Component<ISemioticChartProps, ITimeSeriesState> {
74
+ public state: ITimeSeriesState = {
75
+ tooltip: null,
76
+ userBrushExtent: null,
77
+ showGroup: {
78
+ baseline: true,
79
+ canary: true,
80
+ },
81
+ graphs: {
82
+ // a standalone timeseries chart if data points are few, otherwise it also includes a brushable/ zoomable minimap
83
+ line: null,
84
+ // graph that shows the delta between canary and baseline
85
+ differenceArea: null,
86
+ },
87
+ };
88
+
89
+ private mainMinimapHeight = 320; // total height of the main & minimap (if applicable)
90
+
91
+ private marginMain: IMargin = {
92
+ top: 10,
93
+ left: 60,
94
+ right: 20,
95
+ };
96
+
97
+ private marginMinimap: IMargin = {
98
+ top: 0,
99
+ bottom: 0,
100
+ left: 60,
101
+ right: 20,
102
+ };
103
+
104
+ public componentDidMount() {
105
+ this.createGraphs();
106
+ }
107
+
108
+ // Only reconstruct the graph components when necessary
109
+ public componentDidUpdate(prevProps: ISemioticChartProps, prevState: ITimeSeriesState) {
110
+ const { metricSetPair, parentWidth } = this.props;
111
+ const { userBrushExtent, showGroup } = this.state;
112
+ if (
113
+ metricSetPair !== prevProps.metricSetPair ||
114
+ parentWidth !== prevProps.parentWidth ||
115
+ userBrushExtent !== prevState.userBrushExtent ||
116
+ showGroup !== prevState.showGroup
117
+ ) {
118
+ this.createGraphs();
119
+ }
120
+ }
121
+
122
+ /*
123
+ * Generate chart Data
124
+ * In cases where the start Millis is different betwen canary and baseline, we want to:
125
+ * 1) If data point count is different, extend the shorter dataset to match the longer one (i.e. match the x extent)
126
+ * 2) normalize canary timestamp to match baseline timestamps (needed for the tooltip's
127
+ * voronoi overlay logic in semiotic to work properly)
128
+ */
129
+ private getChartData = (dataSetsAttr: IDataSetsAttributes, metricSetPair: IMetricSetPair) => {
130
+ const { maxDataCount } = dataSetsAttr;
131
+ const { showGroup } = this.state;
132
+ const { dataGroupMap, colors } = vizConfig;
133
+ const { scopes, values } = metricSetPair;
134
+ const { userBrushExtent } = this.state;
135
+ const groups = ['baseline', 'canary'];
136
+ const isOnlyCanarySelected = showGroup.canary && !showGroup.baseline;
137
+ const stepMillis = scopes.control.stepMillis;
138
+
139
+ /*
140
+ * To support dual-axis, use 'normalized' ts supplied to semiotic for both canary and baseline.
141
+ * We store the actual ts as 'actuals' (which can be different from the normalized ts for canary).
142
+ */
143
+ const dataSets: IChartDataSet[] = groups.map((g: string) => {
144
+ const dataPoints: IDataPoint[] = Array(maxDataCount)
145
+ .fill(0)
146
+ .map((_, i: number) => {
147
+ const name = dataGroupMap[g];
148
+ const timestampMillisActual = scopes[name].startTimeMillis + i * stepMillis;
149
+
150
+ // if only canary is selected, norm'd ts is based on canary's ts.
151
+ const timestampMillisNormalizedMain = isOnlyCanarySelected
152
+ ? scopes[dataGroupMap['canary']].startTimeMillis + i * stepMillis
153
+ : scopes[dataGroupMap['baseline']].startTimeMillis + i * stepMillis;
154
+
155
+ // minimap always shows both data sets, so norm'd ts is always based on baseline's ts
156
+ const timestampMillisNormalizedMinimap = scopes[dataGroupMap['baseline']].startTimeMillis + i * stepMillis;
157
+
158
+ return {
159
+ timestampMillisActual, // original ts for this data point
160
+ timestampMillisNormalizedMain,
161
+ timestampMillisNormalizedMinimap,
162
+ value: values[name][i],
163
+ };
164
+ });
165
+
166
+ return {
167
+ label: g,
168
+ color: colors[g],
169
+ coordinates: dataPoints.filter((d) => typeof d.value === 'number'),
170
+ coordinatesUnfiltered: dataPoints,
171
+ };
172
+ });
173
+
174
+ const millisSetNormalizedMinimap: number[] = dataSets[0].coordinatesUnfiltered.map(
175
+ (c: any) => c.timestampMillisNormalizedMinimap,
176
+ );
177
+
178
+ const millisSetNormalizedMain: number[] = (!userBrushExtent
179
+ ? dataSets[0].coordinatesUnfiltered
180
+ : dataSets[0].coordinatesUnfiltered.filter((c: IDataPoint) => {
181
+ return (
182
+ c.timestampMillisNormalizedMinimap >= userBrushExtent[0].valueOf() &&
183
+ c.timestampMillisNormalizedMinimap <= userBrushExtent[1].valueOf()
184
+ );
185
+ })
186
+ ).map((c: IDataPoint) => c.timestampMillisNormalizedMain);
187
+
188
+ return {
189
+ dataSets,
190
+ millisSetNormalizedMain,
191
+ millisSetNormalizedMinimap,
192
+ xExtentNormalizedMain: [millisSetNormalizedMain[0], millisSetNormalizedMain[millisSetNormalizedMain.length - 1]],
193
+ xExtentNormalizedMinimap: [
194
+ millisSetNormalizedMinimap[0],
195
+ millisSetNormalizedMinimap[millisSetNormalizedMinimap.length - 1],
196
+ ],
197
+ };
198
+ };
199
+
200
+ // common chart props that are used for both the main XYFrame and Minimap (if applicable)
201
+ private createCommonChartProps = () => {
202
+ return {
203
+ lineType: {
204
+ type: 'line',
205
+ interpolator: curveStepAfter,
206
+ },
207
+ lineStyle: (ds: IChartDataSet) => ({
208
+ stroke: ds.color,
209
+ strokeWidth: 2,
210
+ strokeOpacity: 0.8,
211
+ }),
212
+ yAccessor: 'value',
213
+ xScaleType: scaleUtc(),
214
+ baseMarkProps: { transitionDuration: { default: 200, fill: 200 } },
215
+ };
216
+ };
217
+
218
+ private createLineChartProps = (
219
+ chartData: IChartData,
220
+ dataSetsAttributes: IDataSetsAttributes,
221
+ commonChartProps: IXYFrameProps<IChartDataSet, IDataPoint>,
222
+ ) => {
223
+ const { axisTickLineHeight, axisTickLabelHeight, axisLabelHeight, minimapHeight } = vizConfig.timeSeries;
224
+
225
+ const { parentWidth } = this.props;
226
+ const { showGroup } = this.state;
227
+
228
+ const { shouldUseSecondaryXAxis, shouldDisplayMinimap } = dataSetsAttributes;
229
+ const { dataSets, xExtentNormalizedMain, xExtentNormalizedMinimap, millisSetNormalizedMain } = chartData;
230
+
231
+ // if secondary axis is needed, we need more bottom margin to fit both axes
232
+ const totalXAxisHeight = shouldUseSecondaryXAxis
233
+ ? 2 * (axisTickLabelHeight + axisLabelHeight) + axisTickLineHeight
234
+ : axisTickLabelHeight;
235
+
236
+ const lineChartProps = {
237
+ ...commonChartProps,
238
+ lines: dataSets.filter((ds: IChartDataSet) => showGroup[ds.label]), // only show selected groups
239
+ hoverAnnotation: [
240
+ {
241
+ type: 'x',
242
+ disable: ['connector', 'note'],
243
+ },
244
+ {
245
+ type: 'vertical-points',
246
+ threshold: 0.1,
247
+ r: () => 5,
248
+ },
249
+ ],
250
+ customHoverBehavior: this.createChartHoverHandler(dataSets, dataSetsAttributes),
251
+ xExtent: xExtentNormalizedMain,
252
+ axes: [
253
+ {
254
+ orient: 'left',
255
+ label: 'metric value',
256
+ tickFormat: (d: number) => utils.formatMetricValue(d),
257
+ },
258
+ {
259
+ orient: 'bottom',
260
+ label: shouldUseSecondaryXAxis ? 'Baseline' : undefined,
261
+ tickValues: utils.calculateDateTimeTicks(millisSetNormalizedMain),
262
+ tickFormat: (d: number) => <CustomAxisTickLabel millis={d} />,
263
+ className: shouldUseSecondaryXAxis ? 'baseline-dual-axis' : '',
264
+ },
265
+ ],
266
+ margin: { ...this.marginMain, bottom: totalXAxisHeight },
267
+ matte: true,
268
+ size: [parentWidth, shouldDisplayMinimap ? this.mainMinimapHeight - minimapHeight : this.mainMinimapHeight],
269
+ xAccessor: (d: IDataPoint) => new Date(d.timestampMillisNormalizedMain),
270
+ };
271
+
272
+ if (shouldDisplayMinimap) {
273
+ return {
274
+ ...lineChartProps,
275
+ minimap: {
276
+ ...commonChartProps,
277
+ lines: dataSets,
278
+ yBrushable: false,
279
+ brushEnd: this.onBrushEnd,
280
+ size: [parentWidth, minimapHeight],
281
+ axes: [
282
+ {
283
+ orient: 'left',
284
+ tickFormat: (): void => null,
285
+ },
286
+ {
287
+ orient: 'bottom',
288
+ },
289
+ ],
290
+ margin: this.marginMinimap,
291
+ xAccessor: (d: IDataPoint) => new Date(d.timestampMillisNormalizedMinimap),
292
+ xExtent: xExtentNormalizedMinimap,
293
+ } as IMinimapProps<IChartDataSet, IDataPoint>,
294
+ };
295
+ } else {
296
+ return lineChartProps as IXYFrameProps<IChartDataSet, IDataPoint>;
297
+ }
298
+ };
299
+
300
+ // construct the graph JSX components and store them as states
301
+ private createGraphs = () => {
302
+ const { metricSetPair, parentWidth } = this.props;
303
+ const { showGroup } = this.state;
304
+ const { minimapDataPointsThreshold, minimapHeight } = vizConfig.timeSeries;
305
+
306
+ /*
307
+ * Generate the data needed for the graph components
308
+ */
309
+ const baselineStartTimeMillis = metricSetPair.scopes.control.startTimeMillis;
310
+ const canaryStartTimeMillis = metricSetPair.scopes.experiment.startTimeMillis;
311
+ const isStartTimeMillisEqual = baselineStartTimeMillis === canaryStartTimeMillis;
312
+
313
+ // Top level attributes to determine how data is formatted & which chart components to include
314
+ const dataSetsAttributes = {
315
+ startTimeMillisOffset: canaryStartTimeMillis - baselineStartTimeMillis,
316
+ maxDataCount: Math.max(metricSetPair.values.control.length, metricSetPair.values.experiment.length),
317
+ shouldDisplayMinimap: metricSetPair.values.control.length > minimapDataPointsThreshold,
318
+ shouldUseSecondaryXAxis: !isStartTimeMillisEqual && showGroup.canary && showGroup.baseline,
319
+ };
320
+
321
+ const chartData = this.getChartData(dataSetsAttributes, metricSetPair);
322
+
323
+ /*
324
+ * Build the visualization components
325
+ */
326
+ const commonChartProps = this.createCommonChartProps();
327
+ const lineChartProps = this.createLineChartProps(chartData, dataSetsAttributes, commonChartProps);
328
+
329
+ const line = (
330
+ <>
331
+ <div className="time-series-chart">
332
+ {dataSetsAttributes.shouldDisplayMinimap ? (
333
+ <MinimapXYFrame {...lineChartProps} />
334
+ ) : (
335
+ <XYFrame {...lineChartProps} />
336
+ )}
337
+ </div>
338
+ {dataSetsAttributes.shouldUseSecondaryXAxis ? (
339
+ <SecondaryTSXAxis
340
+ margin={{ left: this.marginMain.left, right: this.marginMain.right, top: 0, bottom: 0 }}
341
+ width={parentWidth}
342
+ millisSet={chartData.millisSetNormalizedMain.map(
343
+ (ms: number) => ms + dataSetsAttributes.startTimeMillisOffset,
344
+ )}
345
+ axisLabel="canary"
346
+ bottomOffset={dataSetsAttributes.shouldDisplayMinimap ? minimapHeight : 0}
347
+ />
348
+ ) : null}
349
+ {dataSetsAttributes.shouldDisplayMinimap ? (
350
+ <div className="zoom-icon">
351
+ <i className="fas fa-search-plus" />
352
+ </div>
353
+ ) : null}
354
+ </>
355
+ );
356
+
357
+ const differenceArea =
358
+ showGroup.baseline && showGroup.canary ? (
359
+ <DifferenceArea {...this.props} millisSetBaseline={chartData.millisSetNormalizedMinimap} />
360
+ ) : null;
361
+
362
+ this.setState({
363
+ graphs: {
364
+ line,
365
+ differenceArea,
366
+ },
367
+ });
368
+ };
369
+
370
+ private onLegendClickHandler = (group: string) => {
371
+ const showGroup = this.state.showGroup;
372
+ this.setState({
373
+ showGroup: { ...showGroup, [group]: !showGroup[group] },
374
+ });
375
+ };
376
+
377
+ // function factory to create a custom hover handler function based on the datasets
378
+ private createChartHoverHandler = (dataSets: IChartDataSet[], dataSetsAttr: IDataSetsAttributes) => {
379
+ const { shouldUseSecondaryXAxis } = dataSetsAttr;
380
+ return (d: (IXYFrameHoverBaseArgs<IDataPoint> & IDataPoint) | undefined) => {
381
+ if (d && d.timestampMillisNormalizedMain) {
382
+ const tooltipData = dataSets.map(
383
+ (ds: IChartDataSet): ITooltipDataPoint => {
384
+ const coord = ds.coordinatesUnfiltered.find(
385
+ (c: IDataPoint) => c.timestampMillisNormalizedMain === d.timestampMillisNormalizedMain,
386
+ );
387
+ return {
388
+ color: ds.color,
389
+ label: ds.label,
390
+ ts: coord.timestampMillisActual,
391
+ value: coord.value,
392
+ };
393
+ },
394
+ );
395
+
396
+ let tooltipRows = tooltipData
397
+ .concat()
398
+ .sort((a: ITooltipDataPoint, b: ITooltipDataPoint) => b.value - a.value)
399
+ .map((o: ITooltipDataPoint) => {
400
+ // if there's a ts offset, timestamp should be displayed for each group
401
+ const tsRow = shouldUseSecondaryXAxis ? <div className="tooltip-ts">{timestamp(o.ts)}</div> : null;
402
+
403
+ return (
404
+ <div key={o.label} className={classNames({ 'tooltip-dual-axis-row': shouldUseSecondaryXAxis })}>
405
+ {tsRow}
406
+ <div id={o.label}>
407
+ <CircleIcon group={o.label} />
408
+ <span>{`${o.label}: `}</span>
409
+ <span>{utils.formatMetricValue(o.value)}</span>
410
+ </div>
411
+ </div>
412
+ );
413
+ });
414
+
415
+ if (tooltipData.length === 2) {
416
+ const canaryMinusBaseline = tooltipData[1].value - tooltipData[0].value;
417
+ tooltipRows = tooltipRows.concat([
418
+ <div id="diff" key="diff" className="tooltip-row">
419
+ <span>Canary - Baseline: </span>
420
+ <span>{utils.formatMetricValue(canaryMinusBaseline)}</span>
421
+ </div>,
422
+ ]);
423
+ }
424
+
425
+ const tooltipContent = (
426
+ <div>
427
+ {/* if no dual axes, display timestamp row at the top level */}
428
+ {shouldUseSecondaryXAxis ? null : (
429
+ <div key="ts" className="tooltip-ts">
430
+ {timestamp(tooltipData[0].ts)}
431
+ </div>
432
+ )}
433
+ {tooltipRows}
434
+ </div>
435
+ );
436
+ this.setState({
437
+ tooltip: {
438
+ content: tooltipContent,
439
+ x: d.voronoiX + this.marginMain.left,
440
+ y: d.voronoiY + this.marginMain.top,
441
+ },
442
+ });
443
+ } else {
444
+ this.setState({ tooltip: null });
445
+ }
446
+ };
447
+ };
448
+
449
+ // Handle user brush action event from semiotic
450
+ private onBrushEnd = (e: Date[]) => {
451
+ this.setState({
452
+ userBrushExtent: e,
453
+ });
454
+ };
455
+
456
+ public render() {
457
+ const { metricSetPair } = this.props;
458
+ const { showGroup, tooltip } = this.state;
459
+ const { line, differenceArea } = this.state.graphs;
460
+
461
+ return (
462
+ <div className="time-series">
463
+ <ChartHeader metric={metricSetPair.name} />
464
+ <ChartLegend showGroup={showGroup} isClickable={true} onClickHandler={this.onLegendClickHandler} />
465
+ <div className="graph-container">
466
+ {line}
467
+ <Tooltip {...tooltip} />
468
+ </div>
469
+ {differenceArea}
470
+ </div>
471
+ );
472
+ }
473
+ }
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import ReactTooltip from 'react-tooltip';
3
+
4
+ export interface ITooltipProps {
5
+ x?: number;
6
+ y?: number;
7
+ content?: JSX.Element;
8
+ // unique ids are needed if we want to show multiple tooltip objects at once
9
+ id?: string;
10
+ }
11
+
12
+ /*
13
+ * Custom tooltip component to render on top of any svg component.
14
+ * It creates an invisible pixel and uses react-tooltip library
15
+ * to point to it.
16
+ */
17
+ export default class Tooltip extends React.Component<ITooltipProps> {
18
+ private tooltipTarget: HTMLDivElement;
19
+
20
+ public componentDidUpdate(prevProps: ITooltipProps) {
21
+ const target = this.tooltipTarget;
22
+ if (prevProps.content && !this.props.content) {
23
+ ReactTooltip.hide(target);
24
+ } else if (this.props.content) {
25
+ ReactTooltip.show(target);
26
+ }
27
+ }
28
+
29
+ public render() {
30
+ const { x, y, content, id = 'tooltip' } = this.props;
31
+ const tooltipTargetStyle = {
32
+ left: x ? x : 0,
33
+ top: y ? y : 0,
34
+ height: 1,
35
+ width: 1,
36
+ opacity: 0,
37
+ pointerEvents: 'none',
38
+ position: 'absolute',
39
+ zIndex: 10,
40
+ } as React.CSSProperties;
41
+
42
+ const containerStyle = {
43
+ pointerEvents: 'none',
44
+ } as React.CSSProperties;
45
+
46
+ return (
47
+ <div style={containerStyle}>
48
+ <div data-tip={true} data-for={id} style={tooltipTargetStyle} ref={(el) => (this.tooltipTarget = el)} />
49
+ <ReactTooltip id={id} type="light" border={true}>
50
+ {content ? content : null}
51
+ </ReactTooltip>
52
+ </div>
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,90 @@
1
+ import { quantile } from 'd3-array';
2
+ import { format } from 'd3-format';
3
+ import { scaleUtc } from 'd3-scale';
4
+ import { DateTime } from 'luxon';
5
+
6
+ import { SETTINGS } from '@spinnaker/core';
7
+
8
+ import { ISummaryStatistics } from './semiotic.service';
9
+
10
+ /*
11
+ Formatter for any metric values. If the value is not a valid number (typically NaN),
12
+ we want to display "N/A" to highlight that there was no valid measurement.
13
+ Else, the format is made as close as possible to Atlas graph formats:
14
+ SI notation is used when the exponent is between -24 and 24 (inclusive),
15
+ and exponential notation otherwise. Both with 3 digits of precision
16
+ */
17
+ export const formatMetricValue = (value: any) => {
18
+ if (typeof value !== 'number' || isNaN(value)) {
19
+ return 'N/A';
20
+ } else if (Math.abs(value) >= Math.pow(10, 25)) {
21
+ return format('-.3~e')(value);
22
+ } else {
23
+ return format('-.3~s')(value);
24
+ }
25
+ };
26
+
27
+ const fromMillis = (millis: number) => {
28
+ const luxonOpts = SETTINGS.feature.displayTimestampsInUserLocalTime ? undefined : { zone: SETTINGS.defaultTimeZone };
29
+ return DateTime.fromMillis(millis, luxonOpts);
30
+ };
31
+
32
+ /*
33
+ Formatter for timestamps. If the timestamp value is a date boundary (e.g. 04-01-2019 00:00:00),
34
+ an additional date label is returned, otherwise only a single label showing hour & minute is returned
35
+ */
36
+ export const dateTimeTickFormatter = (d: number) => {
37
+ const datetime = fromMillis(d);
38
+ if (datetime.hour === 0 && datetime.minute === 0) {
39
+ return [datetime.toFormat('HH:mm'), datetime.toFormat('MMM DD')];
40
+ } else {
41
+ return [datetime.toFormat('HH:mm')];
42
+ }
43
+ };
44
+
45
+ // function to choose the ideal tick values on the x-axis of a timeseries
46
+ // D3 has a smart tick generator, e.g. it'll tend to show midnight if the data crosses
47
+ // date boundaries. This way we can label date change as date, and other ticks as HH:mm
48
+ export const calculateDateTimeTicks = (millisSet: number[]) => {
49
+ const minMillis = millisSet[0];
50
+ const maxMillis = millisSet[millisSet.length - 1];
51
+
52
+ /*
53
+ * since d3 scale doesn't support custom timezone, we have to:
54
+ * 1)shift the UTC domain based on the tz,
55
+ * 2)have d3 calculate the ideal tick values as usual, and
56
+ * 3)shift back the result
57
+ */
58
+ const offsetMillis = fromMillis(minMillis).offset * 60000;
59
+ const minMillisShifted = minMillis + offsetMillis;
60
+ const maxMillisShifted = maxMillis + offsetMillis;
61
+ const scale = scaleUtc().domain([new Date(minMillisShifted), new Date(maxMillisShifted)]);
62
+ const ticks = scale.ticks(6).map((d: Date) => d.valueOf() - offsetMillis);
63
+ return ticks;
64
+ };
65
+
66
+ export const calculateSummaryStatistics = (values: number[]): ISummaryStatistics => {
67
+ const output = {
68
+ min: {
69
+ value: quantile(values, 0.0),
70
+ label: 'Minimum',
71
+ },
72
+ q1area: {
73
+ value: quantile(values, 0.25),
74
+ label: '25th %-ile',
75
+ },
76
+ median: {
77
+ value: quantile(values, 0.5),
78
+ label: 'Median',
79
+ },
80
+ q3area: {
81
+ value: quantile(values, 0.75),
82
+ label: '75th %-ile',
83
+ },
84
+ max: {
85
+ value: quantile(values, 1.0),
86
+ label: 'Maximum',
87
+ },
88
+ };
89
+ return output;
90
+ };
@@ -0,0 +1,4 @@
1
+ .graph-type-selector {
2
+ margin-left: 60px;
3
+ margin-top: 6px;
4
+ }