You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

747 regels
18 KiB

  1. import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils';
  2. import { getStringWidth } from './helpers';
  3. import { DOT_OVERLAY_SIZE_INCR, PERCENTAGE_BAR_DEFAULT_DEPTH } from './constants';
  4. import { lightenDarkenColor } from './colors';
  5. export const AXIS_TICK_LENGTH = 6;
  6. const LABEL_MARGIN = 4;
  7. const LABEL_MAX_CHARS = 15;
  8. export const FONT_SIZE = 10;
  9. const BASE_LINE_COLOR = '#dadada';
  10. const FONT_FILL = '#555b51';
  11. function $(expr, con) {
  12. return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
  13. }
  14. export function createSVG(tag, o) {
  15. var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
  16. for (var i in o) {
  17. var val = o[i];
  18. if (i === "inside") {
  19. $(val).appendChild(element);
  20. }
  21. else if (i === "around") {
  22. var ref = $(val);
  23. ref.parentNode.insertBefore(element, ref);
  24. element.appendChild(ref);
  25. } else if (i === "styles") {
  26. if(typeof val === "object") {
  27. Object.keys(val).map(prop => {
  28. element.style[prop] = val[prop];
  29. });
  30. }
  31. } else {
  32. if(i === "className") { i = "class"; }
  33. if(i === "innerHTML") {
  34. element['textContent'] = val;
  35. } else {
  36. element.setAttribute(i, val);
  37. }
  38. }
  39. }
  40. return element;
  41. }
  42. function renderVerticalGradient(svgDefElem, gradientId) {
  43. return createSVG('linearGradient', {
  44. inside: svgDefElem,
  45. id: gradientId,
  46. x1: 0,
  47. x2: 0,
  48. y1: 0,
  49. y2: 1
  50. });
  51. }
  52. function setGradientStop(gradElem, offset, color, opacity) {
  53. return createSVG('stop', {
  54. 'inside': gradElem,
  55. 'style': `stop-color: ${color}`,
  56. 'offset': offset,
  57. 'stop-opacity': opacity
  58. });
  59. }
  60. export function makeSVGContainer(parent, className, width, height) {
  61. return createSVG('svg', {
  62. className: className,
  63. inside: parent,
  64. width: width,
  65. height: height
  66. });
  67. }
  68. export function makeSVGDefs(svgContainer) {
  69. return createSVG('defs', {
  70. inside: svgContainer,
  71. });
  72. }
  73. export function makeSVGGroup(className, transform='', parent=undefined) {
  74. let args = {
  75. className: className,
  76. transform: transform
  77. };
  78. if(parent) args.inside = parent;
  79. return createSVG('g', args);
  80. }
  81. export function wrapInSVGGroup(elements, className='') {
  82. let g = createSVG('g', {
  83. className: className
  84. });
  85. elements.forEach(e => g.appendChild(e));
  86. return g;
  87. }
  88. export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) {
  89. return createSVG('path', {
  90. className: className,
  91. d: pathStr,
  92. styles: {
  93. stroke: stroke,
  94. fill: fill,
  95. 'stroke-width': strokeWidth
  96. }
  97. });
  98. }
  99. export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  100. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  101. let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
  102. return `M${center.x} ${center.y}
  103. L${arcStartX} ${arcStartY}
  104. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  105. ${arcEndX} ${arcEndY} z`;
  106. }
  107. export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  108. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  109. let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y];
  110. return `M${center.x} ${center.y}
  111. L${arcStartX} ${arcStartY}
  112. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  113. ${arcEndX} ${midArc} z
  114. L${arcStartX} ${midArc}
  115. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  116. ${arcEndX} ${arcEndY} z`;
  117. }
  118. export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  119. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  120. let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
  121. return `M${arcStartX} ${arcStartY}
  122. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  123. ${arcEndX} ${arcEndY}`;
  124. }
  125. export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
  126. let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
  127. let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y];
  128. return `M${arcStartX} ${arcStartY}
  129. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  130. ${arcEndX} ${midArc}
  131. M${arcStartX} ${midArc}
  132. A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
  133. ${arcEndX} ${arcEndY}`;
  134. }
  135. export function makeGradient(svgDefElem, color, lighter = false) {
  136. let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
  137. let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
  138. let opacities = [1, 0.6, 0.2];
  139. if(lighter) {
  140. opacities = [0.4, 0.2, 0];
  141. }
  142. setGradientStop(gradientDef, "0%", color, opacities[0]);
  143. setGradientStop(gradientDef, "50%", color, opacities[1]);
  144. setGradientStop(gradientDef, "100%", color, opacities[2]);
  145. return gradientId;
  146. }
  147. export function percentageBar(x, y, width, height,
  148. depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
  149. let args = {
  150. className: 'percentage-bar',
  151. x: x,
  152. y: y,
  153. width: width,
  154. height: height,
  155. fill: fill,
  156. styles: {
  157. 'stroke': lightenDarkenColor(fill, -25),
  158. // Diabolically good: https://stackoverflow.com/a/9000859
  159. // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
  160. 'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
  161. 'stroke-width': depth
  162. },
  163. };
  164. return createSVG("rect", args);
  165. }
  166. export function funnelSlice(className, startPositions, height, fill='none') {
  167. const endPosition = []
  168. let [point_a, point_b] = startPositions[0]
  169. // For an equilateral triangle, the angles are always 60 deg.
  170. // The end points on the polygons can be created using the following formula
  171. //
  172. // end_point_x = start_x + height
  173. // end_point_y = start_y +/- height * 1/2
  174. //
  175. // b
  176. // _______________________________
  177. // \ |_| /
  178. // \ | /
  179. // \ | h /
  180. // \ | /
  181. // \|____________________/
  182. //
  183. // b = h * cos(60 deg)
  184. //
  185. endPosition[0] = [point_a[0] + height, point_a[1] + height * 0.5]
  186. endPosition[1] = [point_b[0] + height, point_b[1] - height * 0.5]
  187. }
  188. export function heatSquare(className, x, y, size, fill='none', data={}) {
  189. let args = {
  190. className: className,
  191. x: x,
  192. y: y,
  193. width: size,
  194. height: size,
  195. fill: fill
  196. };
  197. Object.keys(data).map(key => {
  198. args[key] = data[key];
  199. });
  200. return createSVG("rect", args);
  201. }
  202. export function legendBar(x, y, size, fill='none', label, truncate=false) {
  203. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  204. let args = {
  205. className: 'legend-bar',
  206. x: 0,
  207. y: 0,
  208. width: size,
  209. height: '2px',
  210. fill: fill
  211. };
  212. let text = createSVG('text', {
  213. className: 'legend-dataset-text',
  214. x: 0,
  215. y: 0,
  216. dy: (FONT_SIZE * 2) + 'px',
  217. 'font-size': (FONT_SIZE * 1.2) + 'px',
  218. 'text-anchor': 'start',
  219. fill: FONT_FILL,
  220. innerHTML: label
  221. });
  222. let group = createSVG('g', {
  223. transform: `translate(${x}, ${y})`
  224. });
  225. group.appendChild(createSVG("rect", args));
  226. group.appendChild(text);
  227. return group;
  228. }
  229. export function legendDot(x, y, size, fill='none', label, truncate=false) {
  230. label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
  231. let args = {
  232. className: 'legend-dot',
  233. cx: 0,
  234. cy: 0,
  235. r: size,
  236. fill: fill
  237. };
  238. let text = createSVG('text', {
  239. className: 'legend-dataset-text',
  240. x: 0,
  241. y: 0,
  242. dx: (FONT_SIZE) + 'px',
  243. dy: (FONT_SIZE/3) + 'px',
  244. 'font-size': (FONT_SIZE * 1.2) + 'px',
  245. 'text-anchor': 'start',
  246. fill: FONT_FILL,
  247. innerHTML: label
  248. });
  249. let group = createSVG('g', {
  250. transform: `translate(${x}, ${y})`
  251. });
  252. group.appendChild(createSVG("circle", args));
  253. group.appendChild(text);
  254. return group;
  255. }
  256. export function makeText(className, x, y, content, options = {}) {
  257. let fontSize = options.fontSize || FONT_SIZE;
  258. let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
  259. let fill = options.fill || FONT_FILL;
  260. let textAnchor = options.textAnchor || 'start';
  261. return createSVG('text', {
  262. className: className,
  263. x: x,
  264. y: y,
  265. dy: dy + 'px',
  266. 'font-size': fontSize + 'px',
  267. fill: fill,
  268. 'text-anchor': textAnchor,
  269. innerHTML: content
  270. });
  271. }
  272. function makeVertLine(x, label, y1, y2, options={}) {
  273. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  274. let l = createSVG('line', {
  275. className: 'line-vertical ' + options.className,
  276. x1: 0,
  277. x2: 0,
  278. y1: y1,
  279. y2: y2,
  280. styles: {
  281. stroke: options.stroke
  282. }
  283. });
  284. let text = createSVG('text', {
  285. x: 0,
  286. y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
  287. dy: FONT_SIZE + 'px',
  288. 'font-size': FONT_SIZE + 'px',
  289. 'text-anchor': 'middle',
  290. innerHTML: label + ""
  291. });
  292. let line = createSVG('g', {
  293. transform: `translate(${ x }, 0)`
  294. });
  295. line.appendChild(l);
  296. line.appendChild(text);
  297. return line;
  298. }
  299. function makeHoriLine(y, label, x1, x2, options={}) {
  300. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  301. if(!options.lineType) options.lineType = '';
  302. if (options.shortenNumbers) label = shortenLargeNumber(label);
  303. let className = 'line-horizontal ' + options.className +
  304. (options.lineType === "dashed" ? "dashed": "");
  305. let l = createSVG('line', {
  306. className: className,
  307. x1: x1,
  308. x2: x2,
  309. y1: 0,
  310. y2: 0,
  311. styles: {
  312. stroke: options.stroke
  313. }
  314. });
  315. let text = createSVG('text', {
  316. x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
  317. y: 0,
  318. dy: (FONT_SIZE / 2 - 2) + 'px',
  319. 'font-size': FONT_SIZE + 'px',
  320. 'text-anchor': x1 < x2 ? 'end' : 'start',
  321. innerHTML: label+""
  322. });
  323. let line = createSVG('g', {
  324. transform: `translate(0, ${y})`,
  325. 'stroke-opacity': 1
  326. });
  327. if(text === 0 || text === '0') {
  328. line.style.stroke = "rgba(27, 31, 35, 0.6)";
  329. }
  330. line.appendChild(l);
  331. line.appendChild(text);
  332. return line;
  333. }
  334. export function yLine(y, label, width, options={}) {
  335. if(!options.pos) options.pos = 'left';
  336. if(!options.offset) options.offset = 0;
  337. if(!options.mode) options.mode = 'span';
  338. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  339. if(!options.className) options.className = '';
  340. let x1 = -1 * AXIS_TICK_LENGTH;
  341. let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
  342. if(options.mode === 'tick' && options.pos === 'right') {
  343. x1 = width + AXIS_TICK_LENGTH;
  344. x2 = width;
  345. }
  346. // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
  347. x1 += options.offset;
  348. x2 += options.offset;
  349. return makeHoriLine(y, label, x1, x2, {
  350. stroke: options.stroke,
  351. className: options.className,
  352. lineType: options.lineType,
  353. shortenNumbers: options.shortenNumbers
  354. });
  355. }
  356. export function xLine(x, label, height, options={}) {
  357. if(!options.pos) options.pos = 'bottom';
  358. if(!options.offset) options.offset = 0;
  359. if(!options.mode) options.mode = 'span';
  360. if(!options.stroke) options.stroke = BASE_LINE_COLOR;
  361. if(!options.className) options.className = '';
  362. // Draw X axis line in span/tick mode with optional label
  363. // y2(span)
  364. // |
  365. // |
  366. // x line |
  367. // |
  368. // |
  369. // ---------------------+-- y2(tick)
  370. // |
  371. // y1
  372. let y1 = height + AXIS_TICK_LENGTH;
  373. let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
  374. if(options.mode === 'tick' && options.pos === 'top') {
  375. // top axis ticks
  376. y1 = -1 * AXIS_TICK_LENGTH;
  377. y2 = 0;
  378. }
  379. return makeVertLine(x, label, y1, y2, {
  380. stroke: options.stroke,
  381. className: options.className,
  382. lineType: options.lineType
  383. });
  384. }
  385. export function yMarker(y, label, width, options={}) {
  386. if(!options.labelPos) options.labelPos = 'right';
  387. let x = options.labelPos === 'left' ? LABEL_MARGIN
  388. : width - getStringWidth(label, 5) - LABEL_MARGIN;
  389. let labelSvg = createSVG('text', {
  390. className: 'chart-label',
  391. x: x,
  392. y: 0,
  393. dy: (FONT_SIZE / -2) + 'px',
  394. 'font-size': FONT_SIZE + 'px',
  395. 'text-anchor': 'start',
  396. innerHTML: label+""
  397. });
  398. let line = makeHoriLine(y, '', 0, width, {
  399. stroke: options.stroke || BASE_LINE_COLOR,
  400. className: options.className || '',
  401. lineType: options.lineType
  402. });
  403. line.appendChild(labelSvg);
  404. return line;
  405. }
  406. export function yRegion(y1, y2, width, label, options={}) {
  407. // return a group
  408. let height = y1 - y2;
  409. let rect = createSVG('rect', {
  410. className: `bar mini`, // remove class
  411. styles: {
  412. fill: `rgba(228, 234, 239, 0.49)`,
  413. stroke: BASE_LINE_COLOR,
  414. 'stroke-dasharray': `${width}, ${height}`
  415. },
  416. // 'data-point-index': index,
  417. x: 0,
  418. y: 0,
  419. width: width,
  420. height: height
  421. });
  422. if(!options.labelPos) options.labelPos = 'right';
  423. let x = options.labelPos === 'left' ? LABEL_MARGIN
  424. : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
  425. let labelSvg = createSVG('text', {
  426. className: 'chart-label',
  427. x: x,
  428. y: 0,
  429. dy: (FONT_SIZE / -2) + 'px',
  430. 'font-size': FONT_SIZE + 'px',
  431. 'text-anchor': 'start',
  432. innerHTML: label+""
  433. });
  434. let region = createSVG('g', {
  435. transform: `translate(0, ${y2})`
  436. });
  437. region.appendChild(rect);
  438. region.appendChild(labelSvg);
  439. return region;
  440. }
  441. export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
  442. let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
  443. y -= offset;
  444. if(height === 0) {
  445. height = meta.minHeight;
  446. y -= meta.minHeight;
  447. }
  448. let rect = createSVG('rect', {
  449. className: `bar mini`,
  450. style: `fill: ${color}`,
  451. 'data-point-index': index,
  452. x: x,
  453. y: y,
  454. width: width,
  455. height: height
  456. });
  457. label += "";
  458. if(!label && !label.length) {
  459. return rect;
  460. } else {
  461. rect.setAttribute('y', 0);
  462. rect.setAttribute('x', 0);
  463. let text = createSVG('text', {
  464. className: 'data-point-value',
  465. x: width/2,
  466. y: 0,
  467. dy: (FONT_SIZE / 2 * -1) + 'px',
  468. 'font-size': FONT_SIZE + 'px',
  469. 'text-anchor': 'middle',
  470. innerHTML: label
  471. });
  472. let group = createSVG('g', {
  473. 'data-point-index': index,
  474. transform: `translate(${x}, ${y})`
  475. });
  476. group.appendChild(rect);
  477. group.appendChild(text);
  478. return group;
  479. }
  480. }
  481. export function datasetDot(x, y, radius, color, label='', index=0) {
  482. let dot = createSVG('circle', {
  483. style: `fill: ${color}`,
  484. 'data-point-index': index,
  485. cx: x,
  486. cy: y,
  487. r: radius
  488. });
  489. label += "";
  490. if(!label && !label.length) {
  491. return dot;
  492. } else {
  493. dot.setAttribute('cy', 0);
  494. dot.setAttribute('cx', 0);
  495. let text = createSVG('text', {
  496. className: 'data-point-value',
  497. x: 0,
  498. y: 0,
  499. dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
  500. 'font-size': FONT_SIZE + 'px',
  501. 'text-anchor': 'middle',
  502. innerHTML: label
  503. });
  504. let group = createSVG('g', {
  505. 'data-point-index': index,
  506. transform: `translate(${x}, ${y})`
  507. });
  508. group.appendChild(dot);
  509. group.appendChild(text);
  510. return group;
  511. }
  512. }
  513. export function getPaths(xList, yList, color, options={}, meta={}) {
  514. let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
  515. let pointsStr = pointsList.join("L");
  516. // Spline
  517. if (options.spline)
  518. pointsStr = getSplineCurvePointsStr(xList, yList);
  519. let path = makePath("M"+pointsStr, 'line-graph-path', color);
  520. // HeatLine
  521. if(options.heatline) {
  522. let gradient_id = makeGradient(meta.svgDefs, color);
  523. path.style.stroke = `url(#${gradient_id})`;
  524. }
  525. let paths = {
  526. path: path
  527. };
  528. // Region
  529. if(options.regionFill) {
  530. let gradient_id_region = makeGradient(meta.svgDefs, color, true);
  531. let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
  532. paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
  533. }
  534. return paths;
  535. }
  536. export let makeOverlay = {
  537. 'bar': (unit) => {
  538. let transformValue;
  539. if(unit.nodeName !== 'rect') {
  540. transformValue = unit.getAttribute('transform');
  541. unit = unit.childNodes[0];
  542. }
  543. let overlay = unit.cloneNode();
  544. overlay.style.fill = '#000000';
  545. overlay.style.opacity = '0.4';
  546. if(transformValue) {
  547. overlay.setAttribute('transform', transformValue);
  548. }
  549. return overlay;
  550. },
  551. 'dot': (unit) => {
  552. let transformValue;
  553. if(unit.nodeName !== 'circle') {
  554. transformValue = unit.getAttribute('transform');
  555. unit = unit.childNodes[0];
  556. }
  557. let overlay = unit.cloneNode();
  558. let radius = unit.getAttribute('r');
  559. let fill = unit.getAttribute('fill');
  560. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  561. overlay.setAttribute('fill', fill);
  562. overlay.style.opacity = '0.6';
  563. if(transformValue) {
  564. overlay.setAttribute('transform', transformValue);
  565. }
  566. return overlay;
  567. },
  568. 'heat_square': (unit) => {
  569. let transformValue;
  570. if(unit.nodeName !== 'circle') {
  571. transformValue = unit.getAttribute('transform');
  572. unit = unit.childNodes[0];
  573. }
  574. let overlay = unit.cloneNode();
  575. let radius = unit.getAttribute('r');
  576. let fill = unit.getAttribute('fill');
  577. overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
  578. overlay.setAttribute('fill', fill);
  579. overlay.style.opacity = '0.6';
  580. if(transformValue) {
  581. overlay.setAttribute('transform', transformValue);
  582. }
  583. return overlay;
  584. }
  585. };
  586. export let updateOverlay = {
  587. 'bar': (unit, overlay) => {
  588. let transformValue;
  589. if(unit.nodeName !== 'rect') {
  590. transformValue = unit.getAttribute('transform');
  591. unit = unit.childNodes[0];
  592. }
  593. let attributes = ['x', 'y', 'width', 'height'];
  594. Object.values(unit.attributes)
  595. .filter(attr => attributes.includes(attr.name) && attr.specified)
  596. .map(attr => {
  597. overlay.setAttribute(attr.name, attr.nodeValue);
  598. });
  599. if(transformValue) {
  600. overlay.setAttribute('transform', transformValue);
  601. }
  602. },
  603. 'dot': (unit, overlay) => {
  604. let transformValue;
  605. if(unit.nodeName !== 'circle') {
  606. transformValue = unit.getAttribute('transform');
  607. unit = unit.childNodes[0];
  608. }
  609. let attributes = ['cx', 'cy'];
  610. Object.values(unit.attributes)
  611. .filter(attr => attributes.includes(attr.name) && attr.specified)
  612. .map(attr => {
  613. overlay.setAttribute(attr.name, attr.nodeValue);
  614. });
  615. if(transformValue) {
  616. overlay.setAttribute('transform', transformValue);
  617. }
  618. },
  619. 'heat_square': (unit, overlay) => {
  620. let transformValue;
  621. if(unit.nodeName !== 'circle') {
  622. transformValue = unit.getAttribute('transform');
  623. unit = unit.childNodes[0];
  624. }
  625. let attributes = ['cx', 'cy'];
  626. Object.values(unit.attributes)
  627. .filter(attr => attributes.includes(attr.name) && attr.specified)
  628. .map(attr => {
  629. overlay.setAttribute(attr.name, attr.nodeValue);
  630. });
  631. if(transformValue) {
  632. overlay.setAttribute('transform', transformValue);
  633. }
  634. },
  635. };