Compare commits

..

216 Commits

Author SHA1 Message Date
vargburz b3e7f943ad 0.7.8 && render segments if exists 2 months ago
vargburz a8408a748c 0.7.7 && handle context menu 3 months ago
vargburz 6e6fa7dc8e 0.7.6 && update vue with markers and conf 3 months ago
rozetko f67ce313dd 0.7.5 3 months ago
rozetko c1c3b77dac Merge pull request 'area generator: support `invert` option for y/y1 axes' (#70) from area-generator-fix-invert into main 3 months ago
rozetko 53c233b969 area generator: support `invert` option for y/y1 axes 3 months ago
vargburz ebcb3185cd 0.7.4 version 3 months ago
vargburz 6422b49bf0 Merge pull request 'update area render type' (#69) from update-area-type-render into main 3 months ago
vargburz 3f9342b5e1 remove log 3 months ago
vargburz bb46fe79be update area render type 3 months ago
rozetko 0c3b404155 0.7.3 4 months ago
rozetko 85cd952b9f hotfix 4 months ago
rozetko 701feaaf2f 0.7.2 4 months ago
rozetko ebb49adf82 markers: render tooltip in center 4 months ago
rozetko f38829787c 0.7.1 4 months ago
rozetko 6549415feb markers hotfix 4 months ago
rozetko 117c0af469 Merge pull request 'better tooltips for markers' (#68) from better-markers-tooltips into main 4 months ago
rozetko aac8f6f5ac bump version to `0.7.0` 4 months ago
rozetko f4354d7a77 better tooltips for markers 4 months ago
rozetko 529f439ac9 0.6.21 5 months ago
rozetko 121f7a0305 fix renderDots not working for right y-axis 5 months ago
rozetko 7e721ab880 0.6.20 5 months ago
rozetko 3663d3eaaa upd chartwerk core to 0.6.25 5 months ago
Coin de Gamma bd598d6a0b Merge pull request '0.6.19' (#64) from 0.6.19 into main 6 months ago
glitch4347 4620ef7aa9 0.6.19 6 months ago
rozetko 44a4e6ce74 Merge pull request 'segment select' (#63) from segment-select into main 6 months ago
glitch4347 04a01b75a1 opacity param 6 months ago
glitch4347 bf3aab2db1 segment select 6 months ago
Coin de Gamma 348a289f1a Merge pull request '0.6.18' (#61) from 0.6.18 into main 8 months ago
glitch4347 17a2fdf94e 0.6.18 8 months ago
Coin de Gamma dc89f6cbb9 Merge pull request 'basic react component implementatino is sep project' (#59) from better-react-component-#58 into main 8 months ago
glitch4347 547c15c0b6 optimise excludes in libs in webpack 8 months ago
glitch4347 32d122ba4d back dev build 8 months ago
glitch4347 9c165f4e1f basic react component implementatino is sep project 8 months ago
Coin de Gamma ec2bd27acd Merge pull request '0.6.17' (#54) from 0.6.17 into main 8 months ago
glitch4347 815144d209 update core 0.6.23 8 months ago
glitch4347 1a80ca2cab 0.6.17 8 months ago
Coin de Gamma ee08db2f54 Merge pull request 'use events instead of eventsCallbacks' (#53) from core-0.6.23 into main 8 months ago
glitch4347 884fed6582 use events instead of eventsCallbacks 8 months ago
glitch4347 8aca72ce04 zoom out with ranges logic 9 months ago
Coin de Gamma c62a1c386f Merge pull request 'rename demo->basic, live and vertical && update readme' (#47) from rename-examples into main 9 months ago
glitch4347 de6237891c rename demo->basic, live and vertical && update readme 9 months ago
rozetko 7bcd0d298a 0.6.16 10 months ago
rozetko ee2f02be96 upd core 10 months ago
rozetko 456b888fe1 0.6.15 10 months ago
rozetko a34c0574e4 hotfix 10 months ago
rozetko 73dbf0ea67 0.6.14 10 months ago
rozetko ff5973b106 Merge pull request 'updateData: update segments and markers' (#44) from update-segments-and-markers-on-update-data into main 10 months ago
rozetko 878e86d849 updateData: update segments and markers 10 months ago
vargburz 1e8f8d0cb3 0.6.13 10 months ago
rozetko 39412e5f65 Merge pull request 'roken shared crosshair #42' (#43) from broken-shared-crosshair-#42 into main 10 months ago
rozetko 37da147343 Merge pull request '0.6.19 core usdage' (#41) from build-fail-on-core-0.6.18-udpate-#40 into main 10 months ago
glitch4347 0dac1c390e fix 10 months ago
glitch4347 07f454fe51 0.6.19 core usdage 11 months ago
vargburz 88d6b2b24a Merge pull request 'click-event' (#39) from click-event into main 11 months ago
glitch4347 837004c2ad onclick 11 months ago
glitch4347 e46c7bdfb6 some commit 11 months ago
rozetko ee7a57685f 0.6.12 11 months ago
rozetko 963011b274 Merge pull request 'pod fix' (#37) from strange-updates into main 11 months ago
glitch4347 283f0995e8 pod fix 11 months ago
rozetko 19689a68cd Merge pull request 'mouse-over-and-react' (#36) from mouse-over-and-react into main 11 months ago
glitch4347 79b759569f simpler example 11 months ago
glitch4347 d66da50c1a rm line 11 months ago
glitch4347 d9d1b5e501 example with mouse over and refactor react component events 11 months ago
Coin de Gamma b8cd14e4b8 Merge pull request 'marker-callback-#25' (#35) from marker-callback-#25 into main 12 months ago
glitch4347 e470d58611 fx Marker callback type 12 months ago
glitch4347 26d5956c17 markers conf in react 12 months ago
glitch4347 43241bbcb7 markers conf + events 12 months ago
glitch4347 82fc9a10f1 markers conf + new example 12 months ago
rozetko 57ae85c20e Merge pull request 'react-component-marker-and-segment-feature-#28' (#33) from react-component-marker-and-segment-feature-#28 into main 12 months ago
glitch4347 98e4e42fc4 callbacks 12 months ago
glitch4347 6fbf833150 event callback back 12 months ago
glitch4347 a0e27d5efd rm My_CONST 12 months ago
glitch4347 727f8cac86 codefix 12 months ago
glitch4347 869488d405 codestyle 12 months ago
glitch4347 d97b0e9b0c markers++ 12 months ago
glitch4347 11ab01a7d2 markers++ 12 months ago
glitch4347 326c666435 component refactorinmg begin 12 months ago
rozetko 33cf970d5f Merge pull request 'basic segments impl' (#27) from segments-feature-#19 into main 12 months ago
glitch4347 03a562c0d2 codestytle fix 12 months ago
glitch4347 38b6eb2326 basic segments impl 12 months ago
rozetko af9aaf6d82 Merge pull request 'markers-feature-#18' (#23) from markers-feature-#18 into main 12 months ago
glitch4347 bf679019fc payload comment 12 months ago
glitch4347 86099c57b3 pointe events 12 months ago
glitch4347 9a6c5cd7f4 markers++ 12 months ago
glitch4347 aadcd013bf markers++ 12 months ago
glitch4347 a5c737fa81 marker data continue 12 months ago
rozetko c451df6e3b 0.6.11 12 months ago
rozetko 3ff6f7b8e8 crosshair hotfix 12 months ago
glitch4347 1d1a34f9bf markers begin 12 months ago
glitch4347 8346bceedf markers examples begin 12 months ago
vargburz 1ca84c53f8 Merge pull request '0.6.10 version' (#21) from version-0.6.10 into main 12 months ago
vargburz 85793a7965 0.6.10 version 12 months ago
vargburz 7e4a895e93 Merge pull request 'add copy-webpack-=plugin' (#20) from update-copy-webpack into main 12 months ago
glitch4347 f1c3ef6f1d add copy-webpack-=plugin 12 months ago
rozetko 6a0ea4303e Merge pull request 'add react component and build for that' (#16) from react-component-attempt-2 into main 12 months ago
glitch4347 08b128ea91 codestyle fixes 12 months ago
glitch4347 f1582fc610 rm comment 12 months ago
glitch4347 e98997995f add react component and build for that 12 months ago
rozetko e809046098 Merge pull request 'Developement how to and d3 resolution' (#14) from dev-how-to-#4 into main 12 months ago
glitch4347 833780105a fx type in conf 12 months ago
glitch4347 2e156ad46b add node_resolutinos for linking case & update readme 12 months ago
vargburz f100af0ee8 Merge pull request 'hotfix: call mouseOver and mouseMove to render crosshair on init and zoomin' (#10) from hidden-crosshair-on-first-render-and-zoomin into main 12 months ago
vargburz 927855ad45 Merge pull request 'set git repo in package.json' (#12) from set-git-package into main 12 months ago
glitch4347 a6841a83b0 set git repo in package.json 12 months ago
glitch4347 5c39f741ff hotfix: call mouseOver and mouseMove to render crosshair on init and zoomin 1 year ago
rozetko a9f5d074ce Merge pull request 'Update dependencies' (#3) from upd-deps into main 1 year ago
rozetko e3d261aa30 0.6.9 1 year ago
rozetko 4dc9c5290d upd dependencies 1 year ago
vargburz d3597763c5 0.6.8 2 years ago
Alexander Velikiy 517c014629 Merge branch 'hightlight-datapoint-for-right-y-axis' into 'main' 2 years ago
vargburz d763d8591b use right y series on mouse move 2 years ago
vargburz 7d9f38fa3c 0.6.7 2 years ago
Alexander Velikiy 60904493dd Merge branch 'make-y-oriented-series-work' into 'main' 2 years ago
vargburz 903817cb9c apply yOriented series for line pod 2 years ago
vargburz 8825f8b0ba Merge pull request '#DOC [Readme] add browser including' (#2) from doc/add_browser_including into main 2 years ago
Eugene Petukhov db30589898 #DOC [Readme] add browser including v2 2 years ago
Eugene Petukhov f701b29bab #DOC [Readme] add browser including 2 years ago
vargburz b7c6bb7fd8 Merge pull request 'sync-gitlab 0.6.6' (#1) from sync-gitlab into main 2 years ago
rozetko fa632d35a9 Merge branch 'do-not-highlight-nil-metric' into 'main' 2 years ago
rozetko a448f62fc9 minor fix 2 years ago
rozetko cf5722484a 0.6.6 2 years ago
rozetko 49c5c3d462 fix non-existing metrics being highlighted on hover 2 years ago
vargburz 0a04633b43 return latest core vesrion 3 years ago
vargburz dd6485bef5 static core version 3 years ago
vargburz b0b2b48f4d 0.6.3 3 years ago
vargburz b082e71822 fix parent class 3 years ago
vargburz c93bcddee4 0.6.2 3 years ago
vargburz 7cc885e2c7 0.6.1 3 years ago
vargburz 550ca9a807 test 3 years ago
vargburz 15587d9bfa 0.6.0 3 years ago
Alexander Velikiy 9bf20c4169 Merge branch 'update-line-with-models' into 'main' 3 years ago
vargburz b65b04c574 use core 060 3 years ago
vargburz 70966b323f fix live demo 3 years ago
vargburz ca3dc0b8f1 remame 3 years ago
vargburz f9d505fd92 remove strange options 3 years ago
vargburz 463bce47c6 fix defaults 3 years ago
vargburz f593206a98 fix type 3 years ago
vargburz e7248806b0 models 3 years ago
rozetko 88074bc683 yarn migrate to 2 3 years ago
rozetko b79b590efb 0.5.1 3 years ago
rozetko cafa719a66 upd chartwerk core to latest 3 years ago
rozetko 998a13c1ba Merge branch '0.5.0-beta2' into 'main' 3 years ago
rozetko 38d0c11bfc 0.5.0 3 years ago
rozetko 1eec6ba665 upd core 3 years ago
rozetko a5e6654742 0.5.0-beta3 3 years ago
rozetko f2369e2745 upd core 3 years ago
rozetko 4586ea94f9 0.5.0-beta2 3 years ago
rozetko 012434093c external core dep 3 years ago
rozetko 7e4dc84f45 rm dist and package lock 3 years ago
vargburz 22da5ce798 0.4.15 3 years ago
Alexander Velikiy 521a4adc25 Merge branch 'area-opation' into 'main' 3 years ago
vargburz 13dc3f8abe use new core fix fixed resize 3 years ago
vargburz c9931aba33 are option 3 years ago
rozetko 323d4c677e 0.4.14 3 years ago
rozetko 45965dd825 upd core 3 years ago
rozetko a47cb94980 0.4.13 3 years ago
rozetko 0817073ef8 upd core: 0.3.9 3 years ago
vargburz 788374111b 0.4.12 3 years ago
Alexander Velikiy a488abacbd Merge branch 'rerendering-core' into 'main' 3 years ago
vargburz be30b9e9f8 new core 3 years ago
rozetko 85436b735a 0.4.11 3 years ago
rozetko 45cf408bb8 upd core 3 years ago
rozetko 76e9ff78cf 0.4.10 3 years ago
rozetko 4c970950e2 Merge branch 'double-click-fix' into 'main' 3 years ago
dv4mp1r3 6dce9d17eb built new dist 3 years ago
dv4mp1r3 ce7b3df610 added new version of @chartwerk/core 3 years ago
dv4mp1r3 847a3bce56 updated package-lock.json 3 years ago
dv4mp1r3 bbfea95deb code style fix 3 years ago
dv4mp1r3 9db0d99511 added d3 event type check 3 years ago
dv4mp1r3 e72cd8e319 wip 3 years ago
Alexander Velikiy b23918fcf9 Merge branch 'scroll-gap-fix' into 'main' 3 years ago
vargburz d6be304cb0 0.4.9 vers 3 years ago
vargburz 55ee802dec use 0.3.5 core 3 years ago
vargburz 923fd31263 upd dist 3 years ago
vargburz 4b9174414f use new core 3 years ago
Alexander Velikiy efc2bb7f8b Merge branch 'new-options-for-line-pod' into 'main' 3 years ago
Alexander Velikiy 71cecfe507 New options for line pod 3 years ago
Alexander Velikiy 3357e44927 crosshair behavior after zoom out 3 years ago
vargburz 562183a2c8 fix hidden crosshait after zoom out 3 years ago
Alexander Velikiy 4e9ef37a50 Zoom out on dbl click 3 years ago
Alexander Velikiy 7836de8833 Zoom out on dbl click 3 years ago
Alexander Velikiy c30bc9fa8b Core with corrected scroll 3 years ago
Alexander Velikiy 751750ed82 Core with corrected scroll 3 years ago
Alexander Velikiy 8330415043 Merge branch 'use-core-with-updated-state' into 'main' 3 years ago
vargburz 78b18a15d0 add note 3 years ago
vargburz e681f777a0 dist upd 3 years ago
vargburz 2420ac3251 update demo with an examples of zoomin/zoomout 3 years ago
vargburz 7b07cdfb47 update appendData && use new core 3 years ago
Alexey Velikiy e7b0956e80 Merge branch 'use-matric-container-from-core' into 'main' 3 years ago
vargburz cfa481388d 0.4.2 -> 0.4.3 3 years ago
vargburz 16618409a2 use metric container from core 3 years ago
Alexey Velikiy fb82616e9c Merge branch 'use-core-0.3.0' into 'main' 3 years ago
vargburz 62e329b69b 0.4.2 version 3 years ago
vargburz 884cbd9217 Core 0.3.0 3 years ago
Alexey Velikiy 6ba2b6138f 0.4.1 3 years ago
Alexey Velikiy d01e64b049 Merge branch 'missing-hovering-circles-#6' into 'main' 3 years ago
Alexey Velikiy 8d41aad0d0 fx points 3 years ago
Alexey Velikiy fa11d09f56 Merge branch 'rm-spaces' into 'main' 3 years ago
Alexey Velikiy 28e7d1441d rm spaces 3 years ago
Alexey Velikiy 44307558ab 0.4.0 3 years ago
Alexey Velikiy bd18abd7fd Merge branch 'time-value-order-#5' into 'main' 3 years ago
vargburz a8cfbc1115 lineGenerator fix 3 years ago
Alexey Velikiy 8f49dc681d time/value order change attempt 3 years ago
Alexander Velikiy b8270a4381 Merge branch 'rm-console-log' into 'main' 3 years ago
Alexander Velikiy 2a7c5dbb08 Merge branch 'broken-examples-after-renaming-#3' into 'main' 3 years ago
Alexey Velikiy e5a9d9747f build dist/index.js 3 years ago
Alexey Velikiy d446edea45 rm console.log 3 years ago
Alexey Velikiy 3d9ed19bdc fx usage of LinePod in examples 3 years ago
Alexander Velikiy e740b4f9e1 Merge branch 'linepod-classname' into 'main' 3 years ago
Alexey Velikiy 61a643548a LinePod classname 3 years ago
Alexander Velikiy 01cc3e4dfa Merge branch 'package-json-update' into 'main' 3 years ago
Alexey Velikiy 3193fa8728 license + dep to core 3 years ago
Alexander Velikiy 24fe34a2e2 Merge branch 'line-pod' into 'main' 3 years ago
Alexey Velikiy f848841bc7 line-pod 3 years ago
Alexander Velikiy 1081f3a809 Merge branch 'target-master' into 'main' 3 years ago
  1. 9
      .gitignore
  2. 786
      .yarn/releases/yarn-3.2.1.cjs
  3. 3
      .yarnrc.yml
  4. 214
      LICENSE
  5. 41
      README.md
  6. 14
      build/webpack.base.conf.js
  7. 2
      build/webpack.dev.conf.js
  8. 3
      build/webpack.prod.conf.js
  9. 99
      dist/index.d.ts
  10. 9
      dist/index.js
  11. 16
      dist/types.d.ts
  12. 42
      examples/area.html
  13. 45
      examples/basic.html
  14. 36
      examples/demo.html
  15. 48
      examples/demo_live.html
  16. 59
      examples/live.html
  17. 51
      examples/markers.html
  18. 47
      examples/markers_select.html
  19. 33
      examples/mouse_click.html
  20. 29
      examples/mouse_move.html
  21. 33
      examples/right_click.html
  22. 41
      examples/segments.html
  23. 46
      examples/segments_select.html
  24. 10
      examples/vertical.html
  25. 37
      examples/zoom_out.html
  26. 4615
      package-lock.json
  27. 43
      package.json
  28. 26
      react/build/webpack.base.conf.js
  29. 8
      react/build/webpack.dev.conf.js
  30. 6
      react/build/webpack.prod.conf.js
  31. 23
      react/package.json
  32. 74
      react/src/index.tsx
  33. 23
      react/tsconfig.json
  34. 1322
      react/yarn.lock
  35. 139
      src/components/markers.ts
  36. 73
      src/components/segments.ts
  37. 602
      src/index.ts
  38. 21
      src/models/line_series.ts
  39. 19
      src/models/marker.ts
  40. 11
      src/models/segment.ts
  41. 49
      src/types.ts
  42. 5
      tsconfig.json
  43. 2056
      yarn.lock

9
.gitignore vendored

@ -1 +1,10 @@
node_modules
dist
# yarn
.yarn
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

786
.yarn/releases/yarn-3.2.1.cjs vendored

File diff suppressed because one or more lines are too long

3
.yarnrc.yml

@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

214
LICENSE

@ -1,201 +1,13 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Copyright 2020-2021 CorpGlory Inc.
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
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.

41
README.md

@ -1,2 +1,43 @@
# Chartwerk Line Pod
## Including
### Browser
#### Script tag
```html
<script src="https://unpkg.com/@chartwerk/line-pod@latest/dist/index.dev.js" type="text/javascript"></script>
```
#### Example
```html
<div id="line-chart" class="chart-block" style="width: 100%; height: 400px;"></div>
<script src="https://unpkg.com/@chartwerk/line-pod@latest/dist/index.dev.js" type="text/javascript"></script>
<script>
new LinePod(document.getElementById('yourDivId', yourData, yourOptions)).render();
</script>
```
#### Other examples
* [Basic](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/basic.html)
* [Live](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/live.html)
* [Vertical](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/vertical.html)
* [Segments](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/segments.html)
* [Markers](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/markers.html)
### Development
If you want to link `core` then clone it to same directory and then run
```
yarn link ../core
```
but don't add `resolutions` logic to `package.json`
then run
```
yarn install
yarn dev
```

14
build/webpack.base.conf.js

@ -1,4 +1,5 @@
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
function resolve(dir) {
@ -8,7 +9,14 @@ function resolve(dir) {
module.exports = {
context: resolve('src'),
entry: './index.ts',
plugins: [],
plugins: [
new CopyPlugin({
patterns: [
{ from: "../react/dist/index.js", to: "react/index.js" },
{ from: "../react/dist/index.d.ts", to: "react/index.d.ts" },
],
})
],
module: {
rules: [
{
@ -25,6 +33,10 @@ module.exports = {
},
resolve: {
extensions: ['.ts', '.js'],
// this is necessary for resolution of external libs like d3 in dev mode
// when core is linked: webpack will take d3 from this node_modules but not from
// internal so you get one version of d3
modules: [path.resolve(__dirname, '../node_modules'), 'node_modules']
},
output: {
filename: 'index.js',

2
build/webpack.dev.conf.js

@ -2,7 +2,7 @@ const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.devtool = 'inline-source-map';
conf.watch = true;
conf.mode = 'development';
conf.output.filename = 'index.dev.js';
module.exports = conf;

3
build/webpack.prod.conf.js

@ -2,5 +2,8 @@ const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.mode = 'production';
conf.externals = [
'@chartwerk/core', 'd3', 'lodash'
];
module.exports = baseWebpackConfig;

99
dist/index.d.ts vendored

@ -1,99 +0,0 @@
import { ChartwerkPod, TickOrientation, TimeFormat } from '@chartwerk/core';
import { LineTimeSerie, LineOptions, Mode } from './types';
export declare class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions> {
lineGenerator: any;
metricContainer: any;
constructor(_el: HTMLElement, _series?: LineTimeSerie[], _options?: LineOptions);
renderMetrics(): void;
initLineGenerator(): void;
appendData(data: [number, number][]): void;
_renderDots(datapoints: number[][], serieIdx: number): void;
_renderLines(datapoints: number[][], serieIdx: number): void;
_renderMetric(datapoints: number[][], metricOptions: {
color: string;
confidence: number;
target: string;
mode: Mode;
serieIdx: number;
renderDots: boolean;
renderLines: boolean;
}): void;
updateCrosshair(): void;
appendCrosshairCircles(): void;
appendCrosshairCircle(serieIdx: number): void;
renderSharedCrosshair(values: {
x?: number;
y?: number;
}): void;
hideSharedCrosshair(): void;
moveCrosshairLine(xPosition: number, yPosition: number): void;
moveCrosshairCircle(xPosition: number, yPosition: number, serieIdx: number): void;
hideCrosshairCircle(serieIdx: number): void;
getClosestDatapoint(serie: LineTimeSerie, xValue: number, yValue: number): [number, number];
getClosestIndex(datapoints: [number, number][], xValue: number, yValue: number): number;
getValueInterval(columnIdx: number): number | undefined;
onMouseMove(): void;
findAndHighlightDatapoints(xValue: number, yValue: number): {
value: [number, number];
color: string;
label: string;
}[];
isOutOfRange(closestDatapoint: [number, number], xValue: number, yValue: number, useOutOfRange?: boolean): boolean;
onMouseOver(): void;
onMouseOut(): void;
}
export declare const VueChartwerkLineChartObject: {
render(createElement: any): any;
mixins: {
props: {
id: {
type: StringConstructor;
required: boolean;
};
series: {
type: ArrayConstructor;
required: boolean;
default: () => any[];
};
options: {
type: ObjectConstructor;
required: boolean;
default: () => {};
};
};
watch: {
id(): void;
series(): void;
options(): void;
};
mounted(): void;
destroyed(): void;
methods: {
render(): void;
renderSharedCrosshair(values: {
x?: number;
y?: number;
}): void;
hideSharedCrosshair(): void;
onPanningRescale(event: any): void;
renderChart(): void;
appendEvents(): void;
zoomIn(range: any): void;
zoomOut(centers: any): void;
mouseMove(evt: any): void;
mouseOut(): void;
onLegendClick(idx: any): void;
panningEnd(range: any): void;
panning(range: any): void;
contextMenu(evt: any): void;
sharedCrosshairMove(event: any): void;
renderEnd(): void;
};
}[];
methods: {
render(): void;
renderSharedCrosshair(values: any): void;
hideSharedCrosshair(): void;
};
};
export { LineTimeSerie, LineOptions, Mode, TickOrientation, TimeFormat };

9
dist/index.js vendored

File diff suppressed because one or more lines are too long

16
dist/types.d.ts vendored

@ -1,16 +0,0 @@
import { TimeSerie, Options } from '@chartwerk/core';
declare type LineTimeSerieParams = {
confidence: number;
mode: Mode;
maxLength: number;
renderDots: boolean;
renderLines: boolean;
useOutOfRange: boolean;
};
export declare enum Mode {
STANDARD = "Standard",
CHARGE = "Charge"
}
export declare type LineTimeSerie = TimeSerie & Partial<LineTimeSerieParams>;
export declare type LineOptions = Options;
export {};

42
examples/area.html

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
let options = {
renderLegend: false, usePanning: false,
axis: {
x: { format: 'numeric', range: [0, 100] },
y: { invert: true, range: [0, 100] },
y1: { isActive: true, format: 'numeric', range: [0, 1000] },
},
zoomEvents: {
mouse: { zoom: { isActive: false, orientation: 'horizontal' } },
scroll: { zoom: { isActive: false, orientation: 'horizontal' } }
},
}
const data1 = [[0,0], [35, 40], [65, 60], [100, 100]];
const data2 = [[0,0], [35, 50], [65, 65], [80, 100]];
const data3 = [[0,0], [35, 20], [65, 50], [100, 80]];
const data4 = [[0,900], [35, 800], [65, 700], [100, 600]];
var pod = new LinePod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green', renderArea: 'Above' },
{ target: 'test2', datapoints: data2, color: 'blue', renderArea: 'Below' },
{ target: 'test3', datapoints: data3, color: 'orange', renderArea: 'Below', yOrientation: 'right' },
{ target: 'test4', datapoints: data4, color: 'purple', renderArea: 'Above', yOrientation: 'right' },
],
options
);
pod.render();
</script>
</body>
</html>

45
examples/basic.html

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const arrayLength = 20;
const data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
const data2 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 10)]);
const data3 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 20) + 90]);
const zoomIn = (ranges) => { const xRange = ranges[0]; options.axis.x.range = xRange; pod.updateData(undefined, options); }
const panningEnd = (ranges) => { const xRange = ranges[0]; options.axis.x.range = xRange; pod.updateData(undefined, options); }
let options = {
renderLegend: false, usePanning: false,
axis: {
y: { invert: false, range: [0, 350] },
y1: { isActive: true, range: [0, 10], ticksCount: 8 },
x: { format: 'time' }
},
zoomEvents: {
mouse: { zoom: { isActive: true, orientation: 'horizontal' } },
scroll: { zoom: { isActive: true, orientation: 'horizontal' } }
},
eventsCallbacks: { zoomIn: zoomIn, panningEnd }
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green', dashArray: '5,3', class: 'first', renderArea: true },
{ target: 'test2', datapoints: data2, color: 'blue', yOrientation: 'right' },
{ target: 'test3', datapoints: data3, color: 'orange' },
],
options
);
pod.render();
</script>
</body>
</html>

36
examples/demo.html

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 500px; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const arrayLength = 20;
const data1 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 40), startTime + idx * 10000]);
const data2 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 100), startTime + idx * 10000]);
const data3 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 20) + 90, startTime + idx * 10000]);
const zoomIn = (ranges) => { const range = ranges[0]; options.axis.x.range = range; pod.updateData(undefined, options) }
const zoomOut = (ranges) => { console.log('zoomout'); options.axis.x.range = undefined; pod.updateData(undefined, options) }
let options = {
renderLegend: false, usePanning: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } },
eventsCallbacks: { zoomIn: zoomIn, zoomOut }
}
var pod = new ChartwerkLineChart(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green' },
{ target: 'test2', datapoints: data2, color: 'blue' },
{ target: 'test3', datapoints: data3, color: 'orange' },
],
options
);
pod.render();
</script>
</body>
</html>

48
examples/demo_live.html

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 500px; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const arrayLength = 100;
const data1 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 40), startTime + idx * 10000]);
const data2 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 100), startTime + idx * 10000]);
const data3 = Array.from({ length: arrayLength }, (el, idx) => [Math.floor(Math.random() * 20) + 90, startTime + idx * 10000]);
let options = { renderLegend: false, usePanning: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } } };
var pod = new ChartwerkLineChart(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green', maxLength: arrayLength + 30, renderDots: true },
{ target: 'test2', datapoints: data2, color: 'blue', maxLength: arrayLength + 30, renderDots: true },
{ target: 'test3', datapoints: data3, color: 'orange', maxLength: arrayLength + 30, renderDots: true },
],
options
);
pod.render();
let rerenderIdx = arrayLength;
var test = setInterval(() => {
rerenderIdx += 1;
const d1 = [Math.floor(Math.random() * 40), startTime + rerenderIdx * 10000];
const d2 = [Math.floor(Math.random() * 100), startTime + rerenderIdx * 10000];
const d3 = [Math.floor(Math.random() * 20) + 90, startTime + rerenderIdx * 10000];
console.time('rerender');
pod.appendData([d1, d2, d3]);
console.timeEnd('rerender');
if(rerenderIdx > arrayLength + 100) {
clearInterval(test);
}
}, 1000);
</script>
</body>
</html>

59
examples/live.html

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 500px; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const arrayLength = 100;
this.isZoomed = false; // TODO: temporary hack to have zoomin|zoomout with `appendData`. It will be moved to Pod.
var data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
var data2 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 100)]);
var data3 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 20) + 90]);
const zoomIn = (ranges) => { const xRange = ranges[0]; options.axis.x.range = xRange; pod.updateData(undefined, options); this.isZoomed = true }
const zoomOut = (ranges) => { options.axis.x.range = undefined; pod.updateData(undefined, options); this.isZoomed = false }
let options = { renderLegend: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } }, eventsCallbacks: { zoomIn: zoomIn, zoomOut } };
var pod = new LinePod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green', maxLength: arrayLength + 30, renderDots: true },
{ target: 'test2', datapoints: data2, color: 'blue', maxLength: arrayLength + 30, renderDots: true },
{ target: 'test3', datapoints: data3, color: 'orange', maxLength: arrayLength + 30, renderDots: true },
],
options
);
pod.render();
let rerenderIdx = arrayLength;
var test = setInterval(() => {
rerenderIdx += 1;
const d1 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 20) + 90];
const d2 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 100)];
const d3 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 20) + 90];
console.log('d1', data1)
const shouldRerender = !this.isZoomed;
console.time('rerender');
data1.push(d1);
data2.push(d2);
data3.push(d3);
pod.updateData([
{ target: 'test1', datapoints: data1, color: 'green', maxLength: arrayLength + 30, renderDots: false },
{ target: 'test2', datapoints: data2, color: 'blue', maxLength: arrayLength + 30, renderDots: false },
{ target: 'test3', datapoints: data3, color: 'orange', maxLength: arrayLength + 30, renderDots: false },
]);
console.timeEnd('rerender');
if(rerenderIdx > arrayLength + 100) {
clearInterval(test);
}
}, 1000);
</script>
</body>
</html>

51
examples/markers.html

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const markersData1 = [3, 6, 9].map(el => ({
x: startTime + el * 1000,
color: 'red',
alwaysDisplay: false,
html: new Date(startTime).toISOString(),
}));
const markersData2 = [4, 11].map(el => ({
x: startTime + el * 1000,
color: 'blue',
alwaysDisplay: true,
html: new Date(startTime).toISOString(),
}));
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
{
series: [
{ data: markersData1 },
{ data: markersData2 },
]
}
);
pod.render();
</script>
</body>
</html>

47
examples/markers_select.html

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const markersData = [3, 6, 9].map(el => ({
x: startTime + el * 1000,
payload: el,
color: 'red',
}));
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
{
series: [
{ data: markersData },
],
events: {
onMouseMove: (el) => { console.log(el); },
onMouseOut: () => { console.log('mouse out'); }
}
}
);
pod.render();
</script>
</body>
</html>

33
examples/mouse_click.html

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: false },
pan: { isActive: false },
} },
eventsCallbacks: { mouseClick: console.log }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

29
examples/mouse_move.html

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
eventsCallbacks: { mouseMove: console.log }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

33
examples/right_click.html

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: false },
pan: { isActive: false },
} },
eventsCallbacks: { contextMenu: (position) => console.log('contextMenu', position) }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

41
examples/segments.html

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1100]);
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
undefined,
[
{
data: segmentsData,
color:'#FFE545'
}
]
);
pod.render();
</script>
</body>
</html>

46
examples/segments_select.html

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1100]);
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
undefined,
[
{
data: segmentsData,
color:'#FFE545',
select: true,
opacity: 0.4,
opacitySelect: 0.8,
onSelect: console.log,
onUnselect: console.log
}
]
);
pod.render();
</script>
</body>
</html>

10
examples/demo_vertical.html → examples/vertical.html

@ -4,7 +4,7 @@
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.js" type="text/javascript"></script>
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
@ -36,7 +36,7 @@
crosshair: { orientation: 'horizontal' },
margin: { top: 30, right: 20, bottom: 20, left: 50 }
}
var pod = new ChartwerkLineChart(
var pod = new LinePod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green' },
@ -61,13 +61,13 @@
}
function onPanning() {
console.log('panning', pod);
// console.log('panning', pod);
}
function createDatapoints(arrayLength, startTime, randomValue, randomOffset = 0) {
return Array.from({ length: arrayLength }, (el, idx) => [
startTime + idx * 10000, // y axis
Math.floor(Math.random() * randomValue) + randomOffset // x axis
Math.floor(Math.random() * randomValue) + randomOffset, // x axis
startTime + idx * 10000 // y axis
]);
}

37
examples/zoom_out.html

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: true, orientation: "horizontal" },
pan: { isActive: false },
}},
events: {
zoomOut: function(centers, ranges) {
console.log('zoomOut', centers, ranges);
}
}
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

4615
package-lock.json generated

File diff suppressed because it is too large Load Diff

43
package.json

@ -1,32 +1,37 @@
{
"name": "@chartwerk/line-chart",
"version": "0.2.4",
"name": "@chartwerk/line-pod",
"version": "0.7.8",
"description": "Chartwerk line chart",
"main": "dist/index.js",
"files": [
"/dist"
],
"scripts": {
"build": "webpack --config build/webpack.prod.conf.js",
"dev": "webpack --config build/webpack.dev.conf.js",
"test": "echo \"Error: no test specified\" && exit 1"
"build": "rm -rf dist && cd react && yarn build && cd .. && webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js",
"test": "echo \"Error: no test specified\" && exit 1",
"update-core": "yarn up @chartwerk/core && yarn up @chartwerk/core@latest"
},
"repository": {
"type": "git",
"url": "https://github.com/chartwerk/line-chart.git"
"url": "http://code.corpglory.net/chartwerk/line-pod.git"
},
"author": "CorpGlory",
"license": "Apache-2.0",
"license": "ISC",
"dependencies": {
"@chartwerk/core": "github:chartwerk/core#6e41013932bf16b05da2bc2757bcfaacba3cdb21"
"@chartwerk/core": "^0.6.26"
},
"devDependencies": {
"@types/d3": "5.16.4",
"@types/lodash": "^4.14.149",
"css-loader": "^3.4.2",
"d3": "5.16.0",
"lodash": "^4.17.15",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.1",
"typescript": "^3.8.3",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.3",
"typescript": "^5.1.3",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4"
},
"packageManager": "yarn@3.2.1",
"workspaces": [
"react/*"
]
}

26
react/build/webpack.base.conf.js

@ -0,0 +1,26 @@
const path = require('path');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname, '../dist'),
libraryTarget: 'umd',
umdNamedDefine: true,
},
externals: [
'@chartwerk/line-pod', 'react'
]
};

8
react/build/webpack.dev.conf.js

@ -0,0 +1,8 @@
const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.devtool = 'inline-source-map';
conf.mode = 'development';
conf.output.filename = 'index.dev.js';
module.exports = conf;

6
react/build/webpack.prod.conf.js

@ -0,0 +1,6 @@
const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.mode = 'production';
module.exports = baseWebpackConfig;

23
react/package.json

@ -0,0 +1,23 @@
{
"name": "line-pod-react",
"version": "0.0.1",
"description": "React wrapper around line-pod",
"main": "dist/index.js",
"repository": "http://code.corpglory.net/chartwerk/line-pod.git",
"author": "CorpGlory Inc.",
"license": "ISC",
"scripts": {
"build": "webpack --config build/webpack.prod.conf.js",
"dev": "webpack --config build/webpack.dev.conf.js"
},
"dependencies": {
"@chartwerk/line-pod": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.4.3",
"webpack": "^5.87.0"
}
}

74
react/src/index.tsx

@ -0,0 +1,74 @@
import { LineTimeSerie, LineOptions, LinePod } from '@chartwerk/line-pod';
import { MarkersConf } from '@chartwerk/line-pod/dist/models/marker';
import { SegmentSerie } from '@chartwerk/line-pod/dist/models/segment';
import React, { useEffect, useRef, useState } from 'react';
import _ from 'lodash';
export type ChartwerkLinePodProps = {
id?: string;
series: LineTimeSerie[];
options?: LineOptions;
markersConf?: MarkersConf,
segments?: SegmentSerie[],
className?: string;
}
export function ChartwerkLinePod(props: ChartwerkLinePodProps) {
const [pod, setPod] = useState<LinePod | null>(null);
const [hack, setHack] = useState<number | null>(null);
const chartRef = useRef(null);
const chart = chartRef.current;
useEffect(() => {
// this function will be called on component unmount
return () => {
if(pod === null) { return; }
// @ts-ignore
pod.removeEventListeners();
}
}, []);
useEffect(() => {
if(chart === null) { return; }
if(pod === null) {
const newPod = new LinePod(
// @ts-ignore
chart,
props.series,
props.options,
props.markersConf,
props.segments
);
setPod(newPod);
newPod.render();
} else {
if(props.markersConf) {
pod.updateMarkers(props.markersConf);
}
if(props.segments) {
pod.updateSegments(props.segments);
}
// TODO: actually it's wrong logic with updates
// because it creates new pod anyway
pod.updateData(props.series, props.options);
}
}, [chart, props.id, props.options, props.markersConf, props.segments]);
// TODO: it's a hack to render the LinePod right after the div appears in DOM
setTimeout(() => {
if(hack === null) {
setHack(1);
}
}, 1);
return (
<div id={props.id} className={props.className} ref={chartRef}></div>
);
}
export default ChartwerkLinePod;

23
react/tsconfig.json

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"rootDir": "./src",
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"declarationDir": "dist",
"allowSyntheticDefaultImports": true,
"inlineSourceMap": false,
"sourceMap": true,
"noEmitOnError": false,
"emitDecoratorMetadata": false,
"experimentalDecorators": true,
"noImplicitReturns": true,
"noImplicitThis": false,
"noImplicitUseStrict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"baseUrl": "./src",
"jsx": "react"
}
}

1322
react/yarn.lock

File diff suppressed because it is too large Load Diff

139
src/components/markers.ts

@ -0,0 +1,139 @@
import { MarkerElem, MarkersConf, MarkerSerie } from '../models/marker';
import { LineTimeSerie, LineOptions } from '../types';
import { Margin, PodState } from '@chartwerk/core';
import d3 from 'd3';
export class Markers {
private _layerContainer = null;
private _chartHeight = 0;
constructor(
private _chartContainer: d3.Selection<HTMLElement, unknown, null, undefined>,
private _markerConf: MarkersConf,
private _state: PodState<LineTimeSerie, LineOptions>,
private _margin: Margin,
) { }
clear() {
if(this._layerContainer !== null) {
this._layerContainer.remove();
}
this._chartContainer.selectAll('.marker-content').remove();
}
render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartHeight: number) {
this._chartHeight = chartHeight;
this._layerContainer = metricContainer
.append('g')
.attr('class', 'markers-layer');
for(const serie of this._markerConf.series) {
this.renderSerie(serie);
}
}
private _getLinePosition(marker: MarkerElem): number {
return this._state.xScale(marker.x);
}
private _renderCircle(marker: MarkerElem) {
const linePosition = this._getLinePosition(marker);
let circle = this._layerContainer.append('circle')
.attr('class', 'gap-circle')
.attr('stroke', marker.color)
.attr('stroke-width', '2px')
.attr('r', 4)
.attr('cx', linePosition)
.attr('cy', 5)
circle
.attr('pointer-events', 'all')
.style('cursor', 'pointer')
.on('mousemove', () => {
const onMouseMove = this._markerConf.events?.onMouseMove;
if(onMouseMove) {
onMouseMove(marker);
return
}
if(marker.alwaysDisplay) {
return;
}
this._chartContainer
.selectAll(`.marker-content-${marker.x}`)
.style('visibility', 'visible')
.style('z-index', 9999);
})
.on('mouseout', () => {
const onMouseOut = this._markerConf.events?.onMouseOut;
if(onMouseOut) {
onMouseOut()
return
}
if(marker.alwaysDisplay) {
return;
}
this._chartContainer
.selectAll(`.marker-content-${marker.x}`)
.style('visibility', 'hidden')
.style('z-index', 1);
});
}
private _renderLine(marker: MarkerElem) {
const linePosition = this._getLinePosition(marker);
this._layerContainer.append('line')
.attr('class', 'gap-line')
.attr('stroke', marker.color)
.attr('stroke-width', '1px')
.attr('stroke-opacity', '0.3')
.attr('stroke-dasharray', '4')
.attr('x1', linePosition)
.attr('x2', linePosition)
.attr('y1', 0)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.attr('y2', this._state.boxParams.height)
.attr('pointer-events', 'none');
}
private _renderTooltip(marker: MarkerElem) {
if(marker.html === undefined) {
return;
}
const linePosition = this._getLinePosition(marker);
const div = this._chartContainer
.append('div')
.attr('class', `marker-content marker-content-${marker.x}`)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.style('top', `${this._state.boxParams.height - this._chartHeight}px`)
.style('visibility', marker.alwaysDisplay ? 'visible' : 'hidden')
.style('position', 'absolute')
.style('border', '1px solid black')
.style('background-color', 'rgb(33, 37, 41)')
.style('color', 'rgb(255, 255, 255)')
.style('line-height', '1.55')
.style('font-size', '0.875rem')
.style('border-radius', '0.5rem')
.style('padding', 'calc(0.3125rem) 0.625rem')
.style('position', 'absolute')
.style('white-space', 'nowrap')
.style('pointer-events', 'none')
.style('z-index', 1)
.html(marker.html);
// align tooltip: center (we need it to be rendered first)
div.style('left', `${linePosition + this._margin.left - div.node().getBoundingClientRect().width / 2}px`)
}
protected renderSerie(serie: MarkerSerie) {
serie.data.forEach((marker: MarkerElem) => {
this._renderLine(marker);
this._renderCircle(marker);
this._renderTooltip(marker);
});
}
}

73
src/components/segments.ts

@ -0,0 +1,73 @@
import { SegmentSerie, SegmentElement } from "../models/segment";
import { PodState } from "@chartwerk/core";
import { LineTimeSerie, LineOptions } from "../types";
import * as d3 from "d3";
const OPACITY = 0.3;
const OPACITY_SELECT = 0.3;
export class Segments {
// TODO: more semantic name
private _d3Holder = null;
private _metricCon = null;
constructor(
private _series: SegmentSerie[],
private _state: PodState<LineTimeSerie, LineOptions>
) {
}
render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>) {
if(this._d3Holder !== null) {
this._d3Holder.remove();
}
this._d3Holder = metricContainer.append('g').attr('class', 'markers-layer');
for (const s of this._series) {
this.renderSerie(chartContainer, s);
}
}
protected renderSerie(chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>, serie: SegmentSerie) {
// TODO: it's hack with core, need to find a better way
const overlay = chartContainer.select('.overlay');
serie.data.forEach((d) => {
// @ts-ignore
const startPositionX = this._state.xScale(d[0]) as number;
// @ts-ignore
const endPositionX = this._state.xScale(d[1]) as number;
const width = endPositionX - startPositionX // Math.max(endPositionX - startPositionX, MIMIMUM_SEGMENT_WIDTH);
const opacity = serie.opacity || OPACITY;
const opacitySelect = serie.opacitySelect || OPACITY_SELECT;
this._d3Holder.append('rect')
.attr('class', 'segment')
.attr('x', startPositionX)
.attr('y', 0)
.attr('width', width)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.attr('height', this._state.boxParams.height)
.attr('opacity', opacity)
.style('fill', serie.color)
.on('mouseover', function() {
if(serie.select === true) {
d3.select(this).attr('opacity', opacitySelect);
if(serie.onSelect) {
serie.onSelect(d);
}
}
})
.on('mouseout', function(e) {
if(serie.select === true) {
d3.select(this).attr('opacity', opacity);
if(serie.onUnselect) {
serie.onUnselect(d);
}
}
})
.on('mousemove', function(e) {
var event = new MouseEvent('mousemove', d3.event);
overlay.node().dispatchEvent(event)
})
});
}
}

602
src/index.ts

@ -1,5 +1,11 @@
import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, CrosshairOrientation } from '@chartwerk/core';
import { LineTimeSerie, LineOptions, Mode } from './types';
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie, LineOptions, MouseObj, AreaType } from './types';
import { Markers } from './components/markers';
import { Segments } from './components/segments';
import { LineSeries } from './models/line_series';
import { MarkersConf } from './models/marker';
import { SegmentSerie } from './models/segment';
import * as d3 from 'd3';
import * as _ from 'lodash';
@ -8,277 +14,180 @@ const METRIC_CIRCLE_RADIUS = 1.5;
const CROSSHAIR_CIRCLE_RADIUS = 3;
const CROSSHAIR_BACKGROUND_RAIDUS = 9;
const CROSSHAIR_BACKGROUND_OPACITY = 0.3;
export class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions> {
lineGenerator = null;
metricContainer = null;
constructor(_el: HTMLElement, _series: LineTimeSerie[] = [], _options: LineOptions = {}) {
super(d3, _el, _series, _options);
type Generator = d3.Line<[number, number]> | d3.Area<[number, number]>;
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
private _markersLayer: Markers = null;
private _segmentsLayer: Segments = null;
constructor(
_el: HTMLElement,
_series: LineTimeSerie[] = [],
_options: LineOptions = {},
private _markersConf?: MarkersConf,
private _segmentSeries: SegmentSerie[] = [],
) {
super(_el, _series, _options);
this.series = new LineSeries(_series);
}
renderMetrics(): void {
override renderMetrics(): void {
this.clearAllMetrics();
this.updateCrosshair();
this.initLineGenerator();
this.updateEvents();
// TODO: seems that renderMetrics is not correct name
if(this.series.length === 0) {
if(!this.series.isSeriesAvailable) {
this.renderNoDataPointsMessage();
return;
}
for(const serie of this.series.visibleSeries) {
const generator = this.getRenderGenerator(serie.renderArea, serie.yOrientation);
this._renderMetric(serie, generator);
}
if(!_.isEmpty(this._markersConf)) {
this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state, this.margin);
this._markersLayer.render(this.metricContainer, this.height);
}
// TODO: move to core, and create only one container
// container for clip path
const clipContatiner = this.chartContainer
.append('g')
.attr('clip-path', `url(#${this.rectClipId})`)
.attr('class', 'metrics-container');
// container for panning
this.metricContainer = clipContatiner
.append('g')
.attr('class', ' metrics-rect')
for(let idx = 0; idx < this.series.length; ++idx) {
if(this.series[idx].visible === false) {
continue;
}
// TODO: use _.defaults same as in core
const confidence = this.series[idx].confidence || 0;
const mode = this.series[idx].mode || Mode.STANDARD;
const target = this.series[idx].target;
const renderDots = this.series[idx].renderDots !== undefined ? this.series[idx].renderDots : false;
const renderLines = this.series[idx].renderLines !== undefined ? this.series[idx].renderLines : true;
this._renderMetric(
this.series[idx].datapoints,
{
color: this.getSerieColor(idx),
confidence,
target,
mode,
serieIdx: idx,
renderDots,
renderLines,
}
);
if(!_.isEmpty(this._segmentSeries)) {
this._segmentsLayer = new Segments(this._segmentSeries, this.state);
this._segmentsLayer.render(this.metricContainer, this.chartContainer);
}
}
initLineGenerator(): void {
this.lineGenerator = this.d3.line()
.x(d => this.xScale(d[1]))
.y(d => this.yScale(d[0]));
clearAllMetrics(): void {
// TODO: temporary hack before it will be implemented in core.
this.chartContainer.selectAll('.metric-el').remove();
this._markersLayer?.clear();
}
public appendData(data: [number, number][]): void {
this.clearScaleCache();
for(let idx = 0; idx < this.series.length; ++idx) {
if(this.series[idx].visible === false) {
continue;
}
this.series[idx].datapoints.push(data[idx]);
const maxLength = this.series[idx].maxLength;
if(maxLength !== undefined && this.series[idx].datapoints.length > maxLength) {
this.series[idx].datapoints.shift();
}
protected updateEvents(): void {
// overlay - core component that is used to handle mouse events
if(!this.overlay) {
return;
}
for(let idx = 0; idx < this.series.length; ++idx) {
this.metricContainer.select(`.metric-path-${idx}`)
.datum(this.series[idx].datapoints)
.attr('d', this.lineGenerator);
if(this.series[idx].renderDots === true) {
this.metricContainer.selectAll(`.metric-circle-${idx}`)
.data(this.series[idx].datapoints)
.attr('cx', d => this.xScale(d[1]))
.attr('cy', d => this.yScale(d[0]));
this._renderDots([data[idx]], idx);
}
if(this.options._options.events?.contextMenu === undefined) {
return;
}
this.overlay.on('contextmenu', this.onContextMenu.bind(this));
}
this.renderXAxis();
this.renderYAxis();
this.renderGrid();
getRenderGenerator(renderArea: AreaType, yOrientation: yAxisOrientation): Generator {
const yScale = yOrientation === yAxisOrientation.LEFT ? this.state.yScale : this.state.y1Scale;
const yValueRange = yOrientation === yAxisOrientation.LEFT ? this.state.yValueRange : this.state.y1ValueRange;
const yAxisOptions = yOrientation === yAxisOrientation.LEFT ? this.options.axis.y : this.options.axis.y1;
const topChartBorder = !yAxisOptions.invert ? yScale(yValueRange[1]) : yScale(yValueRange[0]);
const bottomChartBorder = !yAxisOptions.invert ? yScale(yValueRange[0]) : yScale(yValueRange[1]);
switch(renderArea) {
case AreaType.NONE:
// return line generator
return d3.line()
.x(d => this.state.xScale(d[0]))
.y(d => yScale(d[1]));
case AreaType.ABOVE:
return d3.area()
.x(d => this.state.xScale(d[0]))
.y0(topChartBorder)
.y1(d => yScale(d[1]));
case AreaType.BELOW:
return d3.area()
.x(d => this.state.xScale(d[0]))
.y0(d => yScale(d[1]))
.y1(bottomChartBorder);
default:
throw new Error(`Unknown type of renderArea: ${renderArea}`);
}
}
_renderDots(datapoints: number[][], serieIdx: number): void {
_renderDots(serie: LineTimeSerie): void {
this.metricContainer.selectAll(null)
.data(datapoints)
.data(serie.datapoints)
.enter()
.append('circle')
.attr('class', `metric-circle-${serieIdx} metric-el`)
.attr('fill', this.getSerieColor(serieIdx))
.attr('class', `metric-circle-${serie.idx} metric-el ${serie.class}`)
.attr('fill', serie.color)
.attr('r', METRIC_CIRCLE_RADIUS)
.style('pointer-events', 'none')
.attr('cx', d => this.xScale(d[1]))
.attr('cy', d => this.yScale(d[0]));
.attr('cx', d => this.state.xScale(d[0]))
.attr('cy', d => this.state.getYScaleByOrientation(serie.yOrientation)(d[1]));
}
_renderLines(datapoints: number[][], serieIdx: number): void {
_renderLines(serie: LineTimeSerie, generator: Generator): void {
const fillColor = serie.renderArea !== AreaType.NONE ? serie.color : 'none';
const fillOpacity = serie.renderArea !== AreaType.NONE ? 0.5 : 'none';
this.metricContainer
.append('path')
.datum(datapoints)
.attr('class', `metric-path-${serieIdx} metric-el`)
.attr('fill', 'none')
.attr('stroke', this.getSerieColor(serieIdx))
.datum(serie.datapoints)
.attr('class', `metric-path-${serie.idx} metric-el ${serie.class}`)
.attr('fill', fillColor)
.attr('fill-opacity', fillOpacity)
.attr('stroke', serie.color)
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.7)
.attr('pointer-events', 'none')
.attr('d', this.lineGenerator);
}
_renderMetric(
datapoints: number[][],
metricOptions: {
color: string,
confidence: number,
target: string,
mode: Mode,
serieIdx: number,
renderDots: boolean,
renderLines: boolean,
}
): void {
if(_.includes(this.seriesTargetsWithBounds, metricOptions.target)) {
return;
}
if(metricOptions.mode === Mode.CHARGE) {
const dataPairs = this.d3.pairs(datapoints);
this.metricContainer.selectAll(null)
.data(dataPairs)
.enter()
.append('line')
.attr('x1', d => this.xScale(d[0][1]))
.attr('x2', d => this.xScale(d[1][1]))
.attr('y1', d => this.yScale(d[0][0]))
.attr('y2', d => this.yScale(d[1][0]))
.attr('stroke-opacity', 0.7)
.style('stroke-width', 1)
.style('stroke', d => {
if(d[1][0] > d[0][0]) {
return 'green';
} else if (d[1][0] < d[0][0]) {
return 'red';
} else {
return 'gray';
}
});
return;
}
if(metricOptions.renderLines === true) {
this._renderLines(datapoints, metricOptions.serieIdx);
}
if(metricOptions.renderDots === true) {
this._renderDots(datapoints, metricOptions.serieIdx);
}
let upperBoundDatapoints = [];
let lowerBoundDatapoints = [];
if(
this.options.bounds !== undefined &&
this.options.bounds.upper !== undefined &&
this.options.bounds.lower !== undefined
) {
this.series.forEach(serie => {
if(serie.target === this.formatedBound(this.options.bounds.upper, metricOptions.target)) {
upperBoundDatapoints = serie.datapoints;
}
if(serie.target === this.formatedBound(this.options.bounds.lower, metricOptions.target)) {
lowerBoundDatapoints = serie.datapoints;
}
});
}
.style('stroke-dasharray', serie.dashArray)
.attr('d', generator);
}
if(upperBoundDatapoints.length > 0 && lowerBoundDatapoints.length > 0) {
const zip = (arr1, arr2) => arr1.map((k, i) => [k[0],k[1], arr2[i][0]]);
const data = zip(upperBoundDatapoints, lowerBoundDatapoints);
this.metricContainer.append('path')
.datum(data)
.attr('fill', metricOptions.color)
.attr('stroke', 'none')
.attr('opacity', '0.3')
.attr('d', this.d3.area()
.x((d: number[]) => this.xScale(d[1]))
.y0((d: number[]) => this.yScale(d[0]))
.y1((d: number[]) => this.yScale(d[2]))
)
_renderMetric(serie: LineTimeSerie, generator: Generator): void {
if(serie.renderLines === true) {
this._renderLines(serie, generator);
}
if(metricOptions.confidence > 0) {
this.metricContainer.append('path')
.datum(datapoints)
.attr('fill', metricOptions.color)
.attr('stroke', 'none')
.attr('opacity', '0.3')
.attr('d', this.d3.area()
.x((d: [number, number]) => this.xScale(d[1]))
.y0((d: [number, number]) => this.yScale(d[0] + metricOptions.confidence))
.y1((d: [number, number]) => this.yScale(d[0] - metricOptions.confidence))
)
if(serie.renderDots === true) {
this._renderDots(serie);
}
}
updateCrosshair(): void {
// Base don't know anything about crosshair circles, It is only for line pod
this.crosshair.selectAll('circle').remove();
// Core doesn't know anything about crosshair circles, It is only for line pod
this.appendCrosshairCircles();
}
appendCrosshairCircles(): void {
// circle for each serie
this.series.forEach((serie: LineTimeSerie, serieIdx: number) => {
this.series.visibleSeries.forEach((serie: LineTimeSerie, serieIdx: number) => {
this.appendCrosshairCircle(serieIdx);
});
}
appendCrosshairCircle(serieIdx: number): void {
// TODO: cx, cy - hacks to hide circles after zoom out(can be replaced with display: none)
this.crosshair.append('circle')
.attr('class', `crosshair-circle-${serieIdx} crosshair-background`)
.attr('r', CROSSHAIR_BACKGROUND_RAIDUS)
.attr('cx', -CROSSHAIR_BACKGROUND_RAIDUS)
.attr('cy', -CROSSHAIR_BACKGROUND_RAIDUS)
.attr('clip-path', `url(#${this.rectClipId})`)
.attr('fill', this.getSerieColor(serieIdx))
.attr('fill', this.series.visibleSeries[serieIdx].color)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none');
this.crosshair
.append('circle')
.attr('cx', -CROSSHAIR_CIRCLE_RADIUS)
.attr('cy', -CROSSHAIR_CIRCLE_RADIUS)
.attr('class', `crosshair-circle-${serieIdx}`)
.attr('clip-path', `url(#${this.rectClipId})`)
.attr('fill', this.getSerieColor(serieIdx))
.attr('fill', this.series.visibleSeries[serieIdx].color)
.attr('r', CROSSHAIR_CIRCLE_RADIUS)
.style('pointer-events', 'none');
}
public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.onMouseOver(); // TODO: refactor to use it once
const eventX = this.xScale(values.x);
const eventY = this.yScale(values.y);
this.moveCrosshairLine(eventX, eventY);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.sharedCrosshairMove === undefined) {
console.log('Shared crosshair move, but there is no callback');
return;
}
this.options.eventsCallbacks.sharedCrosshairMove({
datapoints: datapoints,
eventX, eventY
});
}
public hideSharedCrosshair(): void {
this.crosshair.style('display', 'none');
}
// TODO: refactor to make xPosition and yPosition optional
// and trough error if they are provided for wrong orientation
moveCrosshairLine(xPosition: number, yPosition: number): void {
this.crosshair.style('display', null);
switch(this.options.crosshair.orientation) {
case CrosshairOrientation.VERTICAL:
this.crosshair.select('#crosshair-line-x')
@ -325,27 +234,29 @@ export class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions>
}
getClosestIndex(datapoints: [number, number][], xValue: number, yValue: number): number {
let columnIdx; // 0 for y value, 1 for x value
let value; // xValue ot y Value
let columnIdx; // 0 for x value, 1 for y value,
let value; // xValue to y Value
switch(this.options.crosshair.orientation) {
case CrosshairOrientation.HORIZONTAL:
case CrosshairOrientation.VERTICAL:
columnIdx = 0;
value = yValue;
value = xValue;
break;
case CrosshairOrientation.VERTICAL:
case CrosshairOrientation.HORIZONTAL:
columnIdx = 1;
value = xValue;
value = yValue;
break;
case CrosshairOrientation.BOTH:
// TODO: maybe use voronoi
columnIdx = 0;
columnIdx = 1;
value = yValue;
default:
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`);
}
// TODO: d3.bisect is not the best way. Use binary search
const bisectIndex = this.d3.bisector((d: [number, number]) => d[columnIdx]).left;
const bisectIndex = d3.bisector((d: [number, number]) => d[columnIdx]).left;
let closestIdx = bisectIndex(datapoints, value);
// TODO: refactor corner cases
if(closestIdx < 0) {
return 0;
@ -365,10 +276,10 @@ export class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions>
}
getValueInterval(columnIdx: number): number | undefined {
// columnIdx: 0 for y, 1 for x
// columnIdx: 1 for y, 0 for x
// inverval: x/y value interval between data points
// TODO: move it to base/state instead of timeInterval
const intervals = _.map(this.series, serie => {
const intervals = _.map(this.series.visibleSeries, serie => {
if(serie.datapoints.length < 2) {
return undefined;
}
@ -381,121 +292,234 @@ export class ChartwerkLineChart extends ChartwerkPod<LineTimeSerie, LineOptions>
return _.max(intervals);
}
onMouseMove(): void {
const eventX = this.d3.mouse(this.chartContainer.node())[0];
const eventY = this.d3.mouse(this.chartContainer.node())[1];
const xValue = this.xScale.invert(eventX); // mouse x position in xScale
const yValue = this.yScale.invert(eventY);
// TODO: isOutOfChart is a hack, use clip path correctly
if(this.isOutOfChart() === true) {
this.crosshair.style('display', 'none');
return;
}
this.moveCrosshairLine(eventX, eventY);
getMouseObj(): MouseObj {
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
const xValue = this.state.xScale.invert(eventX); // mouse x position in xScale
const yValue = this.state.yScale.invert(eventY);
const datapoints = this.findAndHighlightDatapoints(xValue, yValue);
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) {
console.log('Mouse move, but there is no callback');
return;
}
// TDOO: is shift key pressed
// TODO: is shift key pressed
// TODO: need to refactor this object
this.options.eventsCallbacks.mouseMove({
x: this.d3.event.pageX,
y: this.d3.event.pageY,
return {
x: d3.event.pageX,
y: d3.event.pageY,
xVal: xValue,
yVal: yValue,
series: datapoints,
chartX: eventX,
chartWidth: this.width
};
}
override onMouseMove(): void {
const obj = this.getMouseObj();
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
this.moveCrosshairLine(eventX, eventY);
// TODO: is shift key pressed
// TODO: need to refactor this object
this.options.callbackMouseMove(obj);
}
public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.showCrosshair();
this.moveCrosshairLine(
values.x ? this.state.xScale(values.x) : 0,
values.y ? this.state.yScale(values.y) : 0
);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
this.options.callbackSharedCrosshairMove({
datapoints: datapoints,
eventX: values.x ? this.state.xScale(values.x) : 0,
eventY: values.y ? this.state.yScale(values.y) : 0
});
}
override onMouseClick(): void {
this.options.callbackMouseClick(this.getMouseObj());
}
findAndHighlightDatapoints(xValue: number, yValue: number): { value: [number, number], color: string, label: string }[] {
if(this.series === undefined || this.series.length === 0) {
if(!this.series.isSeriesAvailable) {
return [];
}
let points = []; // datapoints in each metric that is closest to xValue/yValue position
this.series.forEach((serie: LineTimeSerie, serieIdx: number) => {
if(
serie.visible === false ||
_.includes(this.seriesTargetsWithBounds, serie.target)
) {
this.hideCrosshairCircle(serieIdx);
return;
}
this.series.visibleSeries.forEach((serie: LineTimeSerie) => {
const closestDatapoint = this.getClosestDatapoint(serie, xValue, yValue);
if(closestDatapoint === undefined || this.isOutOfRange(closestDatapoint, xValue, yValue, serie.useOutOfRange)) {
this.hideCrosshairCircle(serieIdx);
return;
if(_.isNil(closestDatapoint) || _.isNil(closestDatapoint[0])) {
this.hideCrosshairCircle(serie.idx);
} else {
const xPosition = this.state.xScale(closestDatapoint[0]);
let yPosition;
if(serie.yOrientation === yAxisOrientation.RIGHT) {
yPosition = this.state.y1Scale(closestDatapoint[1]);
} else {
yPosition = this.state.yScale(closestDatapoint[1]);
}
this.moveCrosshairCircle(xPosition, yPosition, serie.idx);
}
const yPosition = this.yScale(closestDatapoint[0]);
const xPosition = this.xScale(closestDatapoint[1]);
this.moveCrosshairCircle(xPosition, yPosition, serieIdx);
points.push({
value: closestDatapoint,
color: this.getSerieColor(serieIdx),
color: serie.color,
label: serie.alias || serie.target
});
});
return points;
}
isOutOfRange(closestDatapoint: [number, number], xValue: number, yValue: number, useOutOfRange = true): boolean {
// find is mouse position more than xRange/yRange from closest point
// TODO: refactor getValueInterval to remove this!
if(useOutOfRange === false) {
return false;
showCrosshair() {
this.crosshair.style('display', null);
this.crosshair.selectAll('.crosshair-circle')
.style('display', null);
}
hideCrosshair() {
this.crosshair.style('display', 'none');
this.crosshair.selectAll('.crosshair-circle')
.style('display', 'none');
}
override onMouseOver(): void {
this.showCrosshair();
this.onMouseMove();
}
override onMouseOut(): void {
this.hideCrosshair();
this.options.callbackMouseOut();
}
isDoubleClickActive(): boolean {
return this.options.doubleClickEvent.isActive;
}
protected onBrushEnd(): void {
super.onBrushEnd();
this.onMouseOver();
}
updateMarkers(markersConf: MarkersConf): void {
this._markersConf = markersConf;
}
updateSegments(segments: SegmentSerie[]): void {
this._segmentSeries = segments;
}
// methods below rewrite s, (move more methods here)
protected zoomOut(): void {
if(d3.event.type === 'dblclick' && !this.isDoubleClickActive()) {
return;
}
let columnIdx; // 0 for y value, 1 for x value
let value; // xValue ot y Value
switch(this.options.crosshair.orientation) {
case CrosshairOrientation.HORIZONTAL:
columnIdx = 0;
value = yValue;
// TODO: its not clear, why we use this orientation here. Mb its better to use separate option
const orientation: BrushOrientation = this.options.mouseZoomEvent.orientation;
const xInterval = this.state.xValueRange[1] - this.state.xValueRange[0];
const yInterval = this.state.yValueRange[1] - this.state.yValueRange[0];
switch(orientation) {
case BrushOrientation.HORIZONTAL:
this.state.xValueRange = [this.state.xValueRange[0] - xInterval / 2, this.state.xValueRange[1] + xInterval / 2];
break;
case CrosshairOrientation.VERTICAL:
columnIdx = 1;
value = xValue;
case BrushOrientation.VERTICAL:
this.state.yValueRange = [this.state.yValueRange[0] - yInterval / 2, this.state.yValueRange[1] + yInterval / 2];
break;
case BrushOrientation.RECTANGLE:
this.state.xValueRange = [this.state.xValueRange[0] - xInterval / 2, this.state.xValueRange[1] + xInterval / 2];
this.state.yValueRange = [this.state.yValueRange[0] - yInterval / 2, this.state.yValueRange[1] + yInterval / 2];
break;
case BrushOrientation.SQUARE:
this.state.xValueRange = [this.state.xValueRange[0] - xInterval / 2, this.state.xValueRange[1] + xInterval / 2];
this.state.yValueRange = [this.state.yValueRange[0] - yInterval / 2, this.state.yValueRange[1] + yInterval / 2];
break;
case CrosshairOrientation.BOTH:
// TODO: maybe use voronoi
columnIdx = 0;
value = yValue;
default:
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`);
throw new Error(`Unknown type of orientation: ${orientation}, path: options.zoomEvents.mouse.zoom.orientation`);;
}
// TODO: it shouldn't be here. Add smth like vue watchers in core components. Optimize!
this.renderMetrics();
this.renderXAxis();
this.renderYAxis();
this.renderGrid();
this.onMouseOver();
let xAxisMiddleValue: number = this.state.xScale.invert(this.width / 2);
let yAxisMiddleValue: number = this.state.yScale.invert(this.height / 2);
const centers = {
x: xAxisMiddleValue,
y: yAxisMiddleValue
}
// TODO: refactor core not to take _options explicitly
if(
this.options._options.events !== undefined &&
this.options._options.events.zoomOut !== undefined
) {
this.options._options.events.zoomOut(
centers,
[this.state.xValueRange, this.state.yValueRange]
);
}
const range = Math.abs(closestDatapoint[columnIdx] - value);
const interval = this.getValueInterval(columnIdx); // interval between points
// do not move crosshair circles, it mouse to far from closest point
return interval === undefined || range > interval / 2;
}
onMouseOver(): void {
this.crosshair.style('display', null);
this.crosshair.selectAll('.crosshair-circle')
.style('display', null);
protected onContextMenu(): void {
d3.event.preventDefault(); // do not open browser's context menu.
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
this.options._options.events.contextMenu({
x: this.state.xScale.invert(eventX),
y: this.state.yScale.invert(eventY),
});
}
onMouseOut(): void {
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) {
this.options.eventsCallbacks.mouseOut();
}
this.crosshair.style('display', 'none');
// override parent updateData method to provide markers and segments
protected updateLineData(
series?: LineTimeSerie[],
options?: LineOptions,
markersConf?: MarkersConf,
segments?: SegmentSerie[],
shouldRerender = true
): void {
this.updateMarkers(markersConf);
this.updateSegments(segments);
this.updateData(series, options, shouldRerender);
}
}
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-chart', VueChartwerkLineChartObject)
export const VueChartwerkLineChartObject = {
// alternative to `template: '<div class="chartwerk-line-chart" :id="id" />'`
// TODO: it should be moved to VUE folder
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
export const VueChartwerkLinePod = {
// alternative to `template: '<div class="chartwerk-line-pod" :id="id" />'`
props: {
markersConf: {
type: Object,
required: false,
default: function() { return {}; }
},
segments: {
type: Array,
required: false,
default: function() { return []; }
},
},
watch: {
markersConf() {
this.renderChart();
},
segments() {
this.renderChart();
},
},
render(createElement) {
return createElement(
'div',
{
class: { 'chartwerk-line-chart': true },
class: { 'chartwerk-line-pod': true },
attrs: { id: this.id }
}
);
@ -504,10 +528,10 @@ export const VueChartwerkLineChartObject = {
methods: {
render() {
if(this.pod === undefined) {
this.pod = new ChartwerkLineChart(document.getElementById(this.id), this.series, this.options);
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options, this.markersConf, this.segments);
this.pod.render();
} else {
this.pod.updateData(this.series, this.options);
this.pod.updateLineData(this.series, this.options, this.markersConf, this.segments);
}
},
renderSharedCrosshair(values) {
@ -519,4 +543,4 @@ export const VueChartwerkLineChartObject = {
}
};
export { LineTimeSerie, LineOptions, Mode, TickOrientation, TimeFormat };
export { LineTimeSerie, LineOptions, TimeFormat, LinePod, AreaType, MarkersConf, SegmentSerie };

21
src/models/line_series.ts

@ -0,0 +1,21 @@
import { CoreSeries, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie, AreaType } from '../types';
import * as _ from 'lodash';
const LINE_SERIE_DEFAULTS = {
maxLength: undefined,
renderDots: false,
renderLines: true,
dashArray: '0',
class: '',
renderArea: AreaType.NONE,
yOrientation: yAxisOrientation.LEFT,
};
export class LineSeries extends CoreSeries<LineTimeSerie> {
constructor(series: LineTimeSerie[]) {
super(series, _.clone(LINE_SERIE_DEFAULTS));
}
}

19
src/models/marker.ts

@ -0,0 +1,19 @@
export type MarkerElem = {
x: number;
color: string;
html?: string;
alwaysDisplay?: boolean;
payload?: any;
}
export type MarkerSerie = {
data: MarkerElem[];
}
export type MarkersConf = {
series: MarkerSerie[],
events?: {
onMouseMove?: (el: MarkerElem) => void;
onMouseOut?: () => void;
}
}

11
src/models/segment.ts

@ -0,0 +1,11 @@
export type SegmentElement = [number, number, any?];
export type SegmentSerie = {
color: string;
data: SegmentElement[] // [from, to, payload?] payload is any data for tooltip,
select?: boolean,
opacity?: number,
opacitySelect?: number,
onSelect?: (SegmentElement) => void
onUnselect?: (SegmentElement) => void
}

49
src/types.ts

@ -1,16 +1,41 @@
import { TimeSerie, Options } from '@chartwerk/core';
import { Serie, Options } from '@chartwerk/core';
import { AxisRange } from '@chartwerk/core/dist/types';
type LineTimeSerieParams = {
confidence: number,
mode: Mode,
maxLength: number,
renderDots: boolean,
renderLines: boolean, // TODO: refactor same as scatter-pod
useOutOfRange: boolean, // It's temporary hack. Need to refactor getValueInterval() method
maxLength: number;
renderDots: boolean;
renderLines: boolean; // TODO: refactor same as scatter-pod
dashArray: string; // dasharray attr, only for lines
class: string; // option to add custom class to each serie element
renderArea: AreaType; // default is none
}
export enum Mode {
STANDARD = 'Standard',
CHARGE = 'Charge'
export enum AreaType {
NONE = 'None',
ABOVE = 'Above',
BELOW = 'Below',
}
export type LineTimeSerie = Serie & Partial<LineTimeSerieParams>;
export type LineOptions = Options & {
events? : {
zoomOut?: (centers: {
x: number;
y: number;
}, range: AxisRange[]) => void;
contextMenu?: (position: {
x: number;
y: number;
}) => void;
}
}
export type MouseObj = {
x: number,
y: number,
xVal: number,
yVal: number,
series: { value: [number, number], color: string, label: string }[],
chartX: number,
chartWidth: number
}
export type LineTimeSerie = TimeSerie & Partial<LineTimeSerieParams>;
export type LineOptions = Options;

5
tsconfig.json

@ -17,6 +17,7 @@
"noImplicitUseStrict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"baseUrl": "./src"
}
"baseUrl": "./src",
},
"include": ["src/**/*"]
}

2056
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save