Browse Source

UI: Implement pie chart.

pull/10622/head
Igor Kulikov 2 years ago
parent
commit
d315f5ce77
  1. 1
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 2
      application/src/main/data/json/system/widget_types/doughnut.json
  3. 2
      application/src/main/data/json/system/widget_types/horizontal_doughnut.json
  4. 26
      application/src/main/data/json/system/widget_types/pie.json
  5. 6
      application/src/main/data/json/system/widget_types/pie___chart_js.json
  6. 6
      application/src/main/data/json/system/widget_types/pie___flot.json
  7. 2
      application/src/main/resources/thingsboard.yml
  8. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  9. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  10. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html
  11. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html
  12. 25
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts
  13. 276
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.html
  14. 343
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts
  15. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html
  16. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html
  17. 14
      ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts
  18. 37
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html
  19. 505
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts
  20. 80
      ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts
  21. 57
      ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts
  22. 24
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html
  23. 37
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html
  24. 28
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss
  25. 200
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts
  26. 122
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts
  27. 374
      ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts
  28. 86
      ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts
  29. 131
      ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts
  30. 126
      ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts
  31. 194
      ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts
  32. 16
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  33. 66
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts
  34. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html
  35. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html
  36. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts
  37. 184
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.html
  38. 182
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts
  39. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html
  40. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html
  41. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.html
  42. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.ts
  43. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  44. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  45. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  46. 9
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  47. 4
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  48. 12
      ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md
  49. 47
      ui-ngx/src/assets/locale/locale.constant-en_US.json

1
application/src/main/data/json/system/widget_bundles/charts.json

@ -20,6 +20,7 @@
"charts.timeseries_bars_flot",
"cards.aggregated_value_card",
"charts.bars",
"pie",
"charts.pie",
"charts.pie_chart_js",
"doughnut",

2
application/src/main/data/json/system/widget_types/doughnut.json

@ -17,7 +17,7 @@
"settingsDirective": "tb-doughnut-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-doughnut-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"totalValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1\"},\"totalValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showLegend\":true,\"legendPosition\":\"bottom\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.38)\",\"legendValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"legendValueColor\":\"rgba(0, 0, 0, 0.87)\",\"showTooltip\":true,\"tooltipValueType\":\"percentage\",\"tooltipValueDecimals\":0,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
},
"externalId": null,
"tags": [

2
application/src/main/data/json/system/widget_types/horizontal_doughnut.json

@ -17,7 +17,7 @@
"settingsDirective": "tb-doughnut-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-doughnut-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"totalValueFont\":{\"family\":\"Roboto\",\"size\":24,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"1\"},\"totalValueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"showLegend\":true,\"legendPosition\":\"right\",\"legendLabelFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"legendLabelColor\":\"rgba(0, 0, 0, 0.38)\",\"legendValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"20px\"},\"legendValueColor\":\"rgba(0, 0, 0, 0.87)\",\"showTooltip\":true,\"tooltipValueType\":\"percentage\",\"tooltipValueDecimals\":0,\"tooltipValueFont\":{\"family\":\"Roboto\",\"size\":13,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"16px\"},\"tooltipValueColor\":\"rgba(0, 0, 0, 0.76)\",\"tooltipBackgroundColor\":\"rgba(255, 255, 255, 0.76)\",\"tooltipBackgroundBlur\":4,\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind power\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar power\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{},\"title\":\"Doughnut\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"donut_large\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}"
},
"externalId": null,
"tags": [

26
application/src/main/data/json/system/widget_types/pie.json

@ -0,0 +1,26 @@
{
"fqn": "pie",
"name": "Pie",
"deprecated": false,
"image": "tb-image:cGllX2NoYXJ0LnN2Zw==:IlBpZSIgc3lzdGVtIHdpZGdldCBpbWFnZQ==;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAxIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMSAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2RfNDUzOF8zNTMwNikiPgo8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgcng9IjIuMzg4MDYiIGZpbGw9IndoaXRlIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz4KPHBhdGggZD0iTTE0OC45NzUgMTI5LjQ5N0MxNTYuNzc0IDEyMS42OTggMTYyLjYyIDExMi4xNjYgMTY2LjAzNiAxMDEuNjc4TDk5LjQ3NzUgODBMMTQ4Ljk3NSAxMjkuNDk3WiIgZmlsbD0iI0ZGREUzMCIgc3Ryb2tlPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOTkuNDc3NSAxMEM4Ny45NjYgMTAgNzYuNjMyMSAxMi44MzkgNjYuNDc5OCAxOC4yNjU1QzU2LjMyNzUgMjMuNjkyIDQ3LjY3MDIgMzEuNTM4NiA0MS4yNzQ3IDQxLjExMDFDMzQuODc5MiA1MC42ODE2IDMwLjk0MjkgNjEuNjgyNyAyOS44MTQ2IDczLjEzODhDMjguNjg2MyA4NC41OTQ5IDMwLjQwMDcgOTYuMTUyNSAzNC44MDYgMTA2Ljc4OEMzOS4yMTEzIDExNy40MjMgNDYuMTcxNCAxMjYuODA4IDU1LjA3IDEzNC4xMTFDNjMuOTY4NiAxNDEuNDE0IDc0LjUzMDggMTQ2LjQwOSA4NS44MjEyIDE0OC42NTVDOTcuMTExNiAxNTAuOTAxIDEwOC43ODIgMTUwLjMyNyAxMTkuNzk3IDE0Ni45ODZDMTMwLjgxMyAxNDMuNjQ0IDE0MC44MzUgMTM3LjYzNyAxNDguOTc1IDEyOS40OTdMOTkuNDc3NSA4MFYxMFoiIGZpbGw9IiMwODg3MkIiIHN0cm9rZT0id2hpdGUiLz4KPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjFfZF80NTM4XzM1MzA2KSI+CjxwYXRoIGQ9Ik0xNjcuNTYyIDEwMi4wNTdDMTcxLjAxNCA5MS4yNTc1IDE3MS44ODIgNzkuNzg5OCAxNzAuMDk2IDY4LjU4ODVDMTY4LjMxMSA1Ny4zODcxIDE2My45MjIgNDYuNzY5MiAxNTcuMjg4IDM3LjYwMDJDMTUwLjY1MyAyOC40MzEyIDE0MS45NjEgMjAuOTcwNiAxMzEuOTIgMTUuODI2NkMxMjEuODc5IDEwLjY4MjYgMTEwLjc3MiA4LjAwMDc4IDk5LjUwNjQgNy45OTk3Nkw5OS41IDc5Ljk5OThMMTY3LjU2MiAxMDIuMDU3WiIgZmlsbD0iI0ZGNEQ1QSIvPgo8cGF0aCBkPSJNMTY3LjU2MiAxMDIuMDU3QzE3MS4wMTQgOTEuMjU3NSAxNzEuODgyIDc5Ljc4OTggMTcwLjA5NiA2OC41ODg1QzE2OC4zMTEgNTcuMzg3MSAxNjMuOTIyIDQ2Ljc2OTIgMTU3LjI4OCAzNy42MDAyQzE1MC42NTMgMjguNDMxMiAxNDEuOTYxIDIwLjk3MDYgMTMxLjkyIDE1LjgyNjZDMTIxLjg3OSAxMC42ODI2IDExMC43NzIgOC4wMDA3OCA5OS41MDY0IDcuOTk5NzZMOTkuNSA3OS45OTk4TDE2Ny41NjIgMTAyLjA1N1oiIHN0cm9rZT0id2hpdGUiLz4KPC9nPgo8L2c+CjxkZWZzPgo8ZmlsdGVyIGlkPSJmaWx0ZXIwX2RfNDUzOF8zNTMwNiIgeD0iLTcuMTY0MTgiIHk9Ii00Ljc3NjEyIiB3aWR0aD0iMjE0LjMyOCIgaGVpZ2h0PSIxNzQuMzI4IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIiByZXN1bHQ9ImhhcmRBbHBoYSIvPgo8ZmVPZmZzZXQgZHk9IjIuMzg4MDYiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMy41ODIwOSIvPgo8ZmVDb21wb3NpdGUgaW4yPSJoYXJkQWxwaGEiIG9wZXJhdG9yPSJvdXQiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMDYgMCIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluMj0iQmFja2dyb3VuZEltYWdlRml4IiByZXN1bHQ9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93XzQ1MzhfMzUzMDYiIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjxmaWx0ZXIgaWQ9ImZpbHRlcjFfZF80NTM4XzM1MzA2IiB4PSI4NyIgeT0iLTQuNTAwMjQiIHdpZHRoPSI5Ni41IiBoZWlnaHQ9IjExOS4xODciIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMTI3IDAiIHJlc3VsdD0iaGFyZEFscGhhIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjYiLz4KPGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+CjxmZUNvbG9yTWF0cml4IHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjI1IDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNDUzOF8zNTMwNiIvPgo8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd180NTM4XzM1MzA2IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=",
"description": "Displays the latest values of the attributes or time-series data in a pie chart. Supports numeric values only.",
"descriptor": {
"type": "latest",
"sizeX": 5,
"sizeY": 4,
"resources": [],
"templateHtml": "<tb-pie-chart-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-pie-chart-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.pieChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.pieChartWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n previewWidth: '500px',\n previewHeight: '380px',\n embedTitlePanel: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'windPower', label: 'Wind', type: 'timeseries', color: '#08872B' },\n { name: 'solarPower', label: 'Solar', type: 'timeseries', color: '#FF4D5A' },\n { name: 'hydroelectricPower', label: 'Hydroelectric', type: 'timeseries', color: '#FFDE30' }];\n }\n };\n};\n\nself.onDestroy = function() {\n};\n\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "tb-pie-chart-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-pie-chart-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Wind\",\"color\":\"#08872B\",\"settings\":{},\"_hash\":0.7227918773301678,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Solar\",\"color\":\"#FF4D5A\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Hydroelectric\",\"color\":\"#FFDE30\",\"settings\":{},\"_hash\":0.7051898468567794,\"funcBody\":\"if (prevValue === 0) {\\n prevValue = Math.random() * 200;\\n}\\nvar value = prevValue + Math.random() * 20 - 10;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 200) {\\n\\tvalue = 200;\\n}\\nreturn value;\",\"decimals\":0,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0px\",\"settings\":{},\"title\":\"Pie\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":null,\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{\"headerButton\":[]},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"pie_chart\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}"
},
"tags": [
"pie chart",
"pie"
]
}

6
application/src/main/data/json/system/widget_types/pie___chart_js.json

File diff suppressed because one or more lines are too long

6
application/src/main/data/json/system/widget_types/pie___flot.json

File diff suppressed because one or more lines are too long

2
application/src/main/resources/thingsboard.yml

@ -193,7 +193,7 @@ ui:
# Help parameters
help:
# Base URL for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.6.3}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.7}"
# Database telemetry parameters
database:

1
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss

@ -21,6 +21,7 @@
right: 0;
bottom: 0;
background: #fff;
z-index: 1;
}
.widget-preview-section {
position: absolute;

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -117,6 +117,9 @@ import {
import {
StatusWidgetBasicConfigComponent
} from '@home/components/widget/config/basic/indicator/status-widget-basic-config.component';
import {
PieChartBasicConfigComponent
} from '@home/components/widget/config/basic/chart/pie-chart-basic-config.component';
@NgModule({
declarations: [
@ -155,7 +158,8 @@ import {
TimeSeriesChartBasicConfigComponent,
ComparisonKeyRowComponent,
ComparisonKeysTableComponent,
StatusWidgetBasicConfigComponent
StatusWidgetBasicConfigComponent,
PieChartBasicConfigComponent
],
imports: [
CommonModule,
@ -196,7 +200,8 @@ import {
SliderBasicConfigComponent,
ToggleButtonBasicConfigComponent,
TimeSeriesChartBasicConfigComponent,
StatusWidgetBasicConfigComponent
StatusWidgetBasicConfigComponent,
PieChartBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -231,5 +236,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-slider-basic-config': SliderBasicConfigComponent,
'tb-toggle-button-basic-config': ToggleButtonBasicConfigComponent,
'tb-time-series-chart-basic-config': TimeSeriesChartBasicConfigComponent,
'tb-status-widget-basic-config': StatusWidgetBasicConfigComponent
'tb-status-widget-basic-config': StatusWidgetBasicConfigComponent,
'tb-pie-chart-basic-config': PieChartBasicConfigComponent
};

4
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.html

@ -306,9 +306,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

13
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.html

@ -59,7 +59,7 @@
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.doughnut.auto-scale' | translate }}
{{ 'widgets.latest-chart.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
@ -99,12 +99,12 @@
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="clockwise">
{{ 'widgets.doughnut.clockwise-layout' | translate }}
{{ 'widgets.latest-chart.clockwise-layout' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sortSeries">
{{ 'widgets.doughnut.sort-series' | translate }}
{{ 'widgets.latest-chart.sort-series' | translate }}
</mat-slide-toggle>
</div>
<div [fxShow]="totalEnabled" class="tb-form-row space-between">
@ -194,8 +194,8 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tooltipValueType">
<mat-option *ngFor="let type of doughnutTooltipValueTypes" [value]="type">
{{ doughnutTooltipValueTypeTranslationMap.get(type) | translate }}
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type">
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@ -229,6 +229,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-echarts-animation-settings
formControlName="animation">
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

25
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts

@ -31,7 +31,7 @@ import {
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import { formatValue, isDefinedAndNotNull, isUndefined, mergeDeep } from '@core/utils';
import {
getTimewindowConfig,
setTimewindowConfig
@ -42,12 +42,14 @@ import {
doughnutLayoutImages,
doughnutLayouts,
doughnutLayoutTranslations,
DoughnutTooltipValueType,
doughnutTooltipValueTypes,
doughnutTooltipValueTypeTranslations,
DoughnutWidgetSettings,
horizontalDoughnutLayoutImages
} from '@home/components/widget/lib/chart/doughnut-widget.models';
import {
LatestChartTooltipValueType,
latestChartTooltipValueTypes,
latestChartTooltipValueTypeTranslations
} from '@home/components/widget/lib/chart/latest-chart.models';
@Component({
selector: 'tb-doughnut-basic-config',
@ -87,9 +89,9 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
legendPositionTranslationMap = legendPositionTranslationMap;
doughnutTooltipValueTypes = doughnutTooltipValueTypes;
latestChartTooltipValueTypes = latestChartTooltipValueTypes;
doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations;
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations;
doughnutWidgetConfigForm: UntypedFormGroup;
@ -125,7 +127,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: DoughnutWidgetSettings = {...doughnutDefaultSettings(this.horizontal), ...(configData.config.settings || {})};
const settings: DoughnutWidgetSettings = mergeDeep<DoughnutWidgetSettings>({} as DoughnutWidgetSettings,
doughnutDefaultSettings(this.horizontal), configData.config.settings as DoughnutWidgetSettings);
this.doughnutWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
@ -151,6 +154,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
animation: [settings.animation, []],
showLegend: [settings.showLegend, []],
legendPosition: [settings.legendPosition, []],
legendLabelFont: [settings.legendLabelFont, []],
@ -202,6 +207,8 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.animation = config.animation;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendPosition = config.legendPosition;
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont;
@ -329,9 +336,9 @@ export class DoughnutBasicConfigComponent extends BasicWidgetConfigComponent {
}
private _tooltipValuePreviewFn(): string {
const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value;
const tooltipValueType: LatestChartTooltipValueType = this.doughnutWidgetConfigForm.get('tooltipValueType').value;
const decimals: number = this.doughnutWidgetConfigForm.get('tooltipValueDecimals').value;
if (tooltipValueType === DoughnutTooltipValueType.percentage) {
if (tooltipValueType === LatestChartTooltipValueType.percentage) {
return formatValue(35, decimals, '%', false);
} else {
const units: string = this.doughnutWidgetConfigForm.get('units').value;

276
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.html

@ -0,0 +1,276 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
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.
-->
<ng-container [formGroup]="pieChartWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDatasourceLabel
hideDataKeys
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.chart.series' | translate }}"
addKeyTitle="{{ 'widgets.chart.add-series' | translate }}"
keySettingsTitle="{{ 'widgets.chart.series-settings' | translate }}"
removeKeyTitle="{{ 'widgets.chart.remove-series' | translate }}"
noKeysText="{{ 'widgets.chart.no-series' | translate }}"
requiredKeysText="{{ 'widgets.chart.no-series-error' | translate }}"
hideUnits
hideDecimals
hideDataKeyUnits
hideDataKeyDecimals
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="series">
</tb-data-keys-panel>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="showLabel">
{{ 'widgets.pie-chart.label' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="labelPosition">
<mat-option *ngFor="let position of pieChartLabelPositions" [value]="position">
{{ pieChartLabelPositionTranslationMap.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-font-settings formControlName="labelFont"
disabledLineHeight
forceSizeUnit="px"
[previewText]="'Wind<br/>55%'">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="labelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.pie-chart.border' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.pie-chart.radius' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="radius" type="number" min="0" max="100" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>%</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="titleFont"
clearButton
[previewText]="pieChartWidgetConfigForm.get('title').value"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-material-icon-select asBoxInput
iconClearButton
[color]="pieChartWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<tb-color-input asBoxInput
colorClearButton
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="clockwise">
{{ 'widgets.latest-chart.clockwise-layout' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sortSeries">
{{ 'widgets.latest-chart.sort-series' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-unit-input
formControlName="units">
</tb-unit-input>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetConfigForm.get('showLegend').value" [disabled]="!pieChartWidgetConfigForm.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'legend.position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="legendPosition">
<mat-option *ngFor="let pos of legendPositions" [value]="pos">
{{ legendPositionTranslationMap.get(pos) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'legend.label' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendLabelFont"
previewText="Wind">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendLabelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'legend.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendValueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendValueColor">
</tb-color-input>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetConfigForm.get('showTooltip').value" [disabled]="!pieChartWidgetConfigForm.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.tooltip' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between column-xs">
<div>{{ 'tooltip.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tooltipValueType">
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type">
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipValueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="tooltipValueFont"
[previewText]="tooltipValuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipBackgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-blur' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<tb-echarts-animation-settings
formControlName="animation">
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
<div class="tb-form-row space-between column-lt-md">
<div translate>widget-config.show-card-buttons</div>
<mat-chip-listbox multiple formControlName="cardButtons">
<mat-chip-option value="fullscreen">{{ 'fullscreen.fullscreen' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

343
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/pie-chart-basic-config.component.ts

@ -0,0 +1,343 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DataKey,
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
legendPositions,
legendPositionTranslationMap,
WidgetConfig
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { formatValue, isUndefined, mergeDeep } from '@core/utils';
import {
getTimewindowConfig,
setTimewindowConfig
} from '@home/components/widget/config/timewindow-config-panel.component';
import {
LatestChartTooltipValueType,
latestChartTooltipValueTypes,
latestChartTooltipValueTypeTranslations
} from '@home/components/widget/lib/chart/latest-chart.models';
import {
pieChartWidgetDefaultSettings,
PieChartWidgetSettings
} from '@home/components/widget/lib/chart/pie-chart-widget.models';
import {
pieChartLabelPositions,
pieChartLabelPositionTranslations
} from '@home/components/widget/lib/chart/pie-chart.models';
@Component({
selector: 'tb-pie-chart-basic-config',
templateUrl: './pie-chart-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class PieChartBasicConfigComponent extends BasicWidgetConfigComponent {
public get datasource(): Datasource {
const datasources: Datasource[] = this.pieChartWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
public get displayTimewindowConfig(): boolean {
const datasources = this.pieChartWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.pieChartWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
pieChartLabelPositions = pieChartLabelPositions;
pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations;
legendPositions = legendPositions;
legendPositionTranslationMap = legendPositionTranslationMap;
latestChartTooltipValueTypes = latestChartTooltipValueTypes;
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations;
pieChartWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.pieChartWidgetConfigForm;
}
protected defaultDataKeys(configData: WidgetConfigComponentData): DataKey[] {
return [{ name: 'windPower', label: 'Wind', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#08872B' },
{ name: 'solarPower', label: 'Solar', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FF4D5A' },
{ name: 'hydroelectricPower', label: 'Hydroelectric', type: DataKeyType.timeseries, units: '', decimals: 0, color: '#FFDE30' }];
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: PieChartWidgetSettings = mergeDeep<PieChartWidgetSettings>({} as PieChartWidgetSettings,
pieChartWidgetDefaultSettings, configData.config.settings as PieChartWidgetSettings);
this.pieChartWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
series: [this.getSeries(configData.config.datasources), []],
showLabel: [settings.showLabel, []],
labelPosition: [settings.labelPosition, []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
borderWidth: [settings.borderWidth, [Validators.min(0)]],
borderColor: [settings.borderColor, []],
radius: [settings.radius, [Validators.min(0), Validators.max(100)]],
clockwise: [settings.clockwise, []],
sortSeries: [settings.sortSeries, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
titleColor: [configData.config.titleColor, []],
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
animation: [settings.animation, []],
showLegend: [settings.showLegend, []],
legendPosition: [settings.legendPosition, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendValueFont: [settings.legendValueFont, []],
legendValueColor: [settings.legendValueColor, []],
showTooltip: [settings.showTooltip, []],
tooltipValueType: [settings.tooltipValueType, []],
tooltipValueDecimals: [settings.tooltipValueDecimals, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig);
this.widgetConfig.config.datasources = config.datasources;
this.setSeries(config.series, this.widgetConfig.config.datasources);
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.title = config.title;
this.widgetConfig.config.titleFont = config.titleFont;
this.widgetConfig.config.titleColor = config.titleColor;
this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.showLabel = config.showLabel;
this.widgetConfig.config.settings.labelPosition = config.labelPosition;
this.widgetConfig.config.settings.labelFont = config.labelFont;
this.widgetConfig.config.settings.labelColor = config.labelColor;
this.widgetConfig.config.settings.borderWidth = config.borderWidth;
this.widgetConfig.config.settings.borderColor = config.borderColor;
this.widgetConfig.config.settings.radius = config.radius;
this.widgetConfig.config.settings.clockwise = config.clockwise;
this.widgetConfig.config.settings.sortSeries = config.sortSeries;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.animation = config.animation;
this.widgetConfig.config.settings.showLegend = config.showLegend;
this.widgetConfig.config.settings.legendPosition = config.legendPosition;
this.widgetConfig.config.settings.legendLabelFont = config.legendLabelFont;
this.widgetConfig.config.settings.legendLabelColor = config.legendLabelColor;
this.widgetConfig.config.settings.legendValueFont = config.legendValueFont;
this.widgetConfig.config.settings.legendValueColor = config.legendValueColor;
this.widgetConfig.config.settings.showTooltip = config.showTooltip;
this.widgetConfig.config.settings.tooltipValueType = config.tooltipValueType;
this.widgetConfig.config.settings.tooltipValueDecimals = config.tooltipValueDecimals;
this.widgetConfig.config.settings.tooltipValueFont = config.tooltipValueFont;
this.widgetConfig.config.settings.tooltipValueColor = config.tooltipValueColor;
this.widgetConfig.config.settings.tooltipBackgroundColor = config.tooltipBackgroundColor;
this.widgetConfig.config.settings.tooltipBackgroundBlur = config.tooltipBackgroundBlur;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showLabel', 'showTitle', 'showTitleIcon', 'showLegend', 'showTooltip'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showLabel: boolean = this.pieChartWidgetConfigForm.get('showLabel').value;
const showTitle: boolean = this.pieChartWidgetConfigForm.get('showTitle').value;
const showTitleIcon: boolean = this.pieChartWidgetConfigForm.get('showTitleIcon').value;
const showLegend: boolean = this.pieChartWidgetConfigForm.get('showLegend').value;
const showTooltip: boolean = this.pieChartWidgetConfigForm.get('showTooltip').value;
if (showLabel) {
this.pieChartWidgetConfigForm.get('labelPosition').enable();
this.pieChartWidgetConfigForm.get('labelFont').enable();
this.pieChartWidgetConfigForm.get('labelColor').enable();
} else {
this.pieChartWidgetConfigForm.get('labelPosition').disable();
this.pieChartWidgetConfigForm.get('labelFont').disable();
this.pieChartWidgetConfigForm.get('labelColor').disable();
}
if (showTitle) {
this.pieChartWidgetConfigForm.get('title').enable();
this.pieChartWidgetConfigForm.get('titleFont').enable();
this.pieChartWidgetConfigForm.get('titleColor').enable();
this.pieChartWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false});
if (showTitleIcon) {
this.pieChartWidgetConfigForm.get('titleIcon').enable();
this.pieChartWidgetConfigForm.get('iconColor').enable();
} else {
this.pieChartWidgetConfigForm.get('titleIcon').disable();
this.pieChartWidgetConfigForm.get('iconColor').disable();
}
} else {
this.pieChartWidgetConfigForm.get('title').disable();
this.pieChartWidgetConfigForm.get('titleFont').disable();
this.pieChartWidgetConfigForm.get('titleColor').disable();
this.pieChartWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false});
this.pieChartWidgetConfigForm.get('titleIcon').disable();
this.pieChartWidgetConfigForm.get('iconColor').disable();
}
if (showLegend) {
this.pieChartWidgetConfigForm.get('legendPosition').enable();
this.pieChartWidgetConfigForm.get('legendLabelFont').enable();
this.pieChartWidgetConfigForm.get('legendLabelColor').enable();
this.pieChartWidgetConfigForm.get('legendValueFont').enable();
this.pieChartWidgetConfigForm.get('legendValueColor').enable();
} else {
this.pieChartWidgetConfigForm.get('legendPosition').disable();
this.pieChartWidgetConfigForm.get('legendLabelFont').disable();
this.pieChartWidgetConfigForm.get('legendLabelColor').disable();
this.pieChartWidgetConfigForm.get('legendValueFont').disable();
this.pieChartWidgetConfigForm.get('legendValueColor').disable();
}
if (showTooltip) {
this.pieChartWidgetConfigForm.get('tooltipValueType').enable();
this.pieChartWidgetConfigForm.get('tooltipValueDecimals').enable();
this.pieChartWidgetConfigForm.get('tooltipValueFont').enable();
this.pieChartWidgetConfigForm.get('tooltipValueColor').enable();
this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').enable();
this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').enable();
} else {
this.pieChartWidgetConfigForm.get('tooltipValueType').disable();
this.pieChartWidgetConfigForm.get('tooltipValueDecimals').disable();
this.pieChartWidgetConfigForm.get('tooltipValueFont').disable();
this.pieChartWidgetConfigForm.get('tooltipValueColor').disable();
this.pieChartWidgetConfigForm.get('tooltipBackgroundColor').disable();
this.pieChartWidgetConfigForm.get('tooltipBackgroundBlur').disable();
}
}
private getSeries(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
return datasources[0].dataKeys || [];
}
return [];
}
private setSeries(series: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].dataKeys = series;
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.pieChartWidgetConfigForm.get('units').value;
const decimals: number = this.pieChartWidgetConfigForm.get('decimals').value;
return formatValue(110, decimals, units, false);
}
private _tooltipValuePreviewFn(): string {
const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetConfigForm.get('tooltipValueType').value;
const decimals: number = this.pieChartWidgetConfigForm.get('tooltipValueDecimals').value;
if (tooltipValueType === LatestChartTooltipValueType.percentage) {
return formatValue(35, decimals, '%', false);
} else {
const units: string = this.pieChartWidgetConfigForm.get('units').value;
return formatValue(110, decimals, units, false);
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.html

@ -367,9 +367,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

4
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html

@ -328,9 +328,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

14
ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts

@ -23,15 +23,17 @@ import {
textStyle
} from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
import { EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
import {
echartsAnimationDefaultSettings,
EChartsAnimationSettings,
EChartsTooltipWidgetSettings
} from '@home/components/widget/lib/chart/echarts-widget.models';
import { DeepPartial } from '@shared/models/common';
import {
defaultTimeSeriesChartXAxisSettings,
defaultTimeSeriesChartYAxisSettings,
SeriesFillSettings,
SeriesFillType,
timeSeriesChartAnimationDefaultSettings,
TimeSeriesChartAnimationSettings,
timeSeriesChartGridDefaultSettings,
TimeSeriesChartGridSettings,
TimeSeriesChartKeySettings,
@ -63,7 +65,7 @@ export interface BarChartWithLabelsWidgetSettings extends EChartsTooltipWidgetSe
grid: TimeSeriesChartGridSettings;
yAxis: TimeSeriesChartYAxisSettings;
xAxis: TimeSeriesChartXAxisSettings;
animation: TimeSeriesChartAnimationSettings;
animation: EChartsAnimationSettings;
thresholds: TimeSeriesChartThreshold[];
showLegend: boolean;
legendPosition: LegendPosition;
@ -116,8 +118,8 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{showTicks: false, showSplitLines: false} as TimeSeriesChartXAxisSettings),
animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
timeSeriesChartAnimationDefaultSettings),
animation: mergeDeep({} as EChartsAnimationSettings,
echartsAnimationDefaultSettings),
thresholds: [],
showLegend: true,
legendPosition: LegendPosition.top,

37
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.html

@ -1,37 +0,0 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
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.
-->
<div class="tb-doughnut-panel" [style]="backgroundStyle$ | async">
<div class="tb-doughnut-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #doughnutContent class="tb-doughnut-content" [class]="legendClass">
<div #doughnutShape class="tb-doughnut-shape">
</div>
<div *ngIf="showLegend" #doughnutLegend class="tb-doughnut-legend">
<div class="tb-doughnut-legend-item" *ngFor="let legendItem of legendItems" [class]="{'pointer': !legendItem.total && legendItem.hasValue}"
(mouseenter)="onLegendItemEnter(legendItem)"
(mouseleave)="onLegendItemLeave(legendItem)"
(click)="toggleLegendItem(legendItem)">
<div class="tb-doughnut-legend-item-label">
<div class="tb-doughnut-legend-item-label-circle" [style]="{background: (legendItem.enabled && legendItem.hasValue) ? legendItem.color : null}"></div>
<div [style]="(legendItem.enabled && legendItem.hasValue) ? legendLabelStyle : disabledLegendLabelStyle">{{ legendItem.label }}</div>
</div>
<div [style]="(legendItem.enabled && legendItem.hasValue) ? legendValueStyle : disabledLegendValueStyle" class="tb-doughnut-legend-item-value">{{ legendItem.value }}</div>
</div>
</div>
</div>
</div>

505
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.ts

@ -14,85 +14,32 @@
/// limitations under the License.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import {
doughnutDefaultSettings,
DoughnutLayout,
DoughnutTooltipValueType,
doughnutPieChartSettings,
DoughnutWidgetSettings
} from '@home/components/widget/lib/chart/doughnut-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle,
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { ResizeObserver } from '@juggle/resize-observer';
import { WidgetComponent } from '@home/components/widget/widget.component';
import * as echarts from 'echarts/core';
import { TranslateService } from '@ngx-translate/core';
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries';
import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils';
import { SVG, Svg, Text } from '@svgdotjs/svg.js';
import { DataKey, LegendPosition } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models';
const shapeSize = 134;
const shapeSegmentWidth = 13.4;
interface DoughnutDataItem {
id: number;
dataKey: DataKey;
value: number;
hasValue: boolean;
enabled: boolean;
}
interface DoughnutLegendItem {
id: number;
color: string;
label: string;
value: string;
hasValue: boolean;
enabled: boolean;
total?: boolean;
}
import { isDefinedAndNotNull } from '@core/utils';
import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart';
import {
LatestChartComponent,
LatestChartComponentCallbacks
} from '@home/components/widget/lib/chart/latest-chart.component';
@Component({
selector: 'tb-doughnut-widget',
templateUrl: './doughnut-widget.component.html',
styleUrls: ['./doughnut-widget.component.scss'],
templateUrl: './latest-chart-widget.component.html',
styleUrls: [],
encapsulation: ViewEncapsulation.None
})
export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
export class DoughnutWidgetComponent implements OnInit {
@ViewChild('doughnutContent', {static: false})
doughnutContent: ElementRef<HTMLElement>;
@ViewChild('doughnutShape', {static: false})
doughnutShape: ElementRef<HTMLElement>;
@ViewChild('doughnutLegend', {static: false})
doughnutLegend: ElementRef<HTMLElement>;
settings: DoughnutWidgetSettings;
@ViewChild('latestChart')
latestChart: LatestChartComponent;
@Input()
ctx: WidgetContext;
@ -100,45 +47,12 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit
@Input()
widgetTitlePanel: TemplateRef<any>;
showLegend: boolean;
legendClass: string;
totalValueColor: ColorProcessor;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
legendItems: DoughnutLegendItem[];
legendLabelStyle: ComponentStyle;
legendValueStyle: ComponentStyle;
disabledLegendLabelStyle: ComponentStyle;
disabledLegendValueStyle: ComponentStyle;
private shapeResize$: ResizeObserver;
private legendHorizontal: boolean;
private decimals = 0;
private units = '';
private total = 0;
private totalText = 'N/A';
private scale = 1;
private dataItems: DoughnutDataItem[] = [];
settings: DoughnutWidgetSettings;
private drawDoughnutPending = false;
private showTotal = false;
private doughnutChart: ECharts;
private doughnutOptions: EChartsOption;
private svgShape: Svg;
private totalTextNode: Text;
callbacks: LatestChartComponentCallbacks;
constructor(private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
private widgetComponent: WidgetComponent,
private renderer: Renderer2,
private translate: TranslateService,
private cd: ChangeDetectorRef) {
constructor(private widgetComponent: WidgetComponent,
private translate: TranslateService) {
}
ngOnInit(): void {
@ -146,390 +60,19 @@ export class DoughnutWidgetComponent implements OnInit, OnDestroy, AfterViewInit
const horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.ctx.$scope.doughnutWidget = this;
this.settings = {...doughnutDefaultSettings(horizontal), ...this.ctx.settings};
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
this.showLegend = this.settings.showLegend;
this.showTotal = this.settings.layout === DoughnutLayout.with_total;
if (this.showTotal) {
this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor);
}
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
if (this.showLegend) {
this.legendItems = [];
this.legendClass = `legend-${this.settings.legendPosition}`;
this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition);
this.legendLabelStyle = textStyle(this.settings.legendLabelFont);
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont);
this.legendLabelStyle.color = this.settings.legendLabelColor;
this.legendValueStyle = textStyle(this.settings.legendValueFont);
this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont);
this.legendValueStyle.color = this.settings.legendValueColor;
}
let counter = 0;
if (this.ctx.datasources.length) {
for (const datasource of this.ctx.datasources) {
const dataKeys = datasource.dataKeys;
for (const dataKey of dataKeys) {
const id = counter++;
this.dataItems.push({
id,
dataKey,
value: 0,
hasValue: false,
enabled: true
});
if (this.showLegend) {
this.legendItems.push(
{
id,
value: '--',
label: dataKey.label,
color: dataKey.color,
enabled: true,
hasValue: false
}
);
}
}
this.callbacks = {
createChart: (chartShape, renderer) => {
const settings = doughnutPieChartSettings(this.settings);
return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true);
}
}
if (this.settings.sortSeries) {
this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label));
if (this.showLegend) {
this.legendItems.sort((a, b) => a.label.localeCompare(b.label));
}
}
if (this.showLegend && !this.showTotal) {
this.legendItems.push(
{
id: null,
value: '--',
label: this.translate.instant('widgets.doughnut.total'),
color: 'rgba(0, 0, 0, 0.06)',
enabled: true,
hasValue: false,
total: true
}
);
}
}
ngAfterViewInit() {
if (this.drawDoughnutPending) {
this.drawDoughnut();
}
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.doughnutChart) {
this.doughnutChart.dispose();
}
};
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
if (this.doughnutShape) {
this.drawDoughnut();
} else {
this.drawDoughnutPending = true;
}
this.cd.detectChanges();
this.latestChart?.onInit();
}
public onDataUpdated() {
for (const dsData of this.ctx.data) {
let value = 0;
const tsValue = dsData.data[0];
const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey);
if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) {
value = tsValue[1];
dataItem.hasValue = true;
dataItem.value = Number(value);
} else {
dataItem.hasValue = false;
dataItem.value = 0;
}
}
this.updateSeriesData();
if (this.showLegend) {
this.cd.detectChanges();
if (this.legendHorizontal) {
setTimeout(() => {
this.onResize();
});
}
}
}
private updateSeriesData(renderTotal = true) {
this.total = 0;
this.totalText = 'N/A';
let hasValue = false;
const seriesData: PieDataItemOption[] = [];
const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue);
for (const dataItem of this.dataItems) {
if (dataItem.enabled && dataItem.hasValue) {
hasValue = true;
this.total += dataItem.value;
seriesData.push(
{id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}}
);
if (enabledDataItems.length > 1) {
seriesData.push({
value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true}
});
}
}
if (this.showLegend) {
const legendItem = this.legendItems.find(item => item.id === dataItem.id);
if (dataItem.hasValue) {
legendItem.hasValue = true;
legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false);
} else {
legendItem.hasValue = false;
legendItem.value = '--';
}
}
}
for (let i= 1; i < seriesData.length; i+=2) {
seriesData[i].value = this.total / 100;
}
if (this.showTotal || this.showLegend) {
if (hasValue) {
this.totalText = formatValue(this.total, this.decimals, this.units, false);
if (this.showLegend && !this.showTotal) {
this.legendItems[this.legendItems.length - 1].hasValue = true;
this.legendItems[this.legendItems.length - 1].value = this.totalText;
}
} else if (this.showLegend && !this.showTotal) {
this.legendItems[this.legendItems.length - 1].hasValue = false;
this.legendItems[this.legendItems.length - 1].value = '--';
}
}
this.doughnutOptions.series[0].data = seriesData;
this.doughnutChart.setOption(this.doughnutOptions);
if (this.showTotal) {
this.totalValueColor.update(this.total);
if (renderTotal) {
this.renderTotal();
}
}
}
public onLegendItemEnter(item: DoughnutLegendItem) {
if (!item.total && item.enabled && item.hasValue) {
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
if (dataIndex > -1) {
this.doughnutChart.dispatchAction({
type: 'highlight',
dataIndex
});
}
}
}
public onLegendItemLeave(item: DoughnutLegendItem) {
if (!item.total && item.enabled && item.hasValue) {
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
if (dataIndex > -1) {
this.doughnutChart.dispatchAction({
type: 'downplay',
dataIndex
});
}
}
this.latestChart?.onDataUpdated();
}
public toggleLegendItem(item: DoughnutLegendItem) {
if (!item.total && item.hasValue) {
const enable = !item.enabled;
const dataItem = this.dataItems.find(d => d.id === item.id);
if (dataItem) {
dataItem.enabled = enable;
this.updateSeriesData();
item.enabled = enable;
if (enable) {
const dataIndex = this.doughnutOptions.series[0].data.findIndex(d => d.id === item.id);
if (dataIndex > -1) {
this.doughnutChart.dispatchAction({
type: 'highlight',
dataIndex
});
}
}
}
}
}
private drawDoughnut() {
echartsModule.init();
const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height;
const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight);
const innerRadius = size / 2 - shapeSegmentWidth;
const outerRadius = size / 2;
this.doughnutChart = echarts.init(this.doughnutShape.nativeElement, null, {
renderer: 'svg',
width: this.settings.autoScale ? shapeSize : undefined,
height: this.settings.autoScale ? shapeSize : undefined,
});
this.doughnutOptions = {
tooltip: {
trigger: this.settings.showTooltip ? 'item' : 'none',
confine: false,
appendTo: 'body',
},
series: [
{
type: 'pie',
clockwise: this.settings.clockwise,
radius: [innerRadius, outerRadius],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: '50%',
borderWidth: 0,
borderColor: '#fff'
},
label: {
show: false
},
emphasis: {
scale: false,
itemStyle: {
borderColor: '#fff',
borderWidth: 2,
shadowColor: 'rgba(0, 0, 0, 0.24)',
shadowBlur: 8
},
label: {
show: false
}
}
}
]
};
if (this.settings.showTooltip) {
this.doughnutOptions.series[0].tooltip = {
formatter: (params) => {
if (!params.name) {
return null;
}
let value: string;
if (this.settings.tooltipValueType === DoughnutTooltipValueType.percentage) {
const percents = params.value / this.total * 100;
value = formatValue(percents, this.settings.tooltipValueDecimals, '%', false);
} else {
value = formatValue(params.value, this.settings.tooltipValueDecimals, this.units, false);
}
const textElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(textElement, 'display', 'inline-flex');
this.renderer.setStyle(textElement, 'align-items', 'center');
this.renderer.setStyle(textElement, 'gap', '8px');
const labelElement: HTMLElement = this.renderer.createElement('div');
this.renderer.appendChild(labelElement, this.renderer.createText(params.name));
this.renderer.setStyle(labelElement, 'font-family', 'Roboto');
this.renderer.setStyle(labelElement, 'font-size', '11px');
this.renderer.setStyle(labelElement, 'font-style', 'normal');
this.renderer.setStyle(labelElement, 'font-weight', '400');
this.renderer.setStyle(labelElement, 'line-height', '16px');
this.renderer.setStyle(labelElement, 'letter-spacing', '0.25px');
this.renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)');
const valueElement: HTMLElement = this.renderer.createElement('div');
this.renderer.appendChild(valueElement, this.renderer.createText(value));
this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family);
this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit);
this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style);
this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight);
this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight);
this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor);
this.renderer.appendChild(textElement, labelElement);
this.renderer.appendChild(textElement, valueElement);
return textElement;
},
padding: [4, 8],
backgroundColor: this.settings.tooltipBackgroundColor,
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`
};
this.doughnutOptions.series[0].tooltip.position = (pos) => [pos[0] + 10, pos[1] + 10];
}
this.updateSeriesData(false);
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'overflow', 'visible');
if (this.settings.autoScale) {
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'position', 'absolute');
}
this.renderer.setStyle(this.doughnutChart.getDom().firstChild.firstChild, 'overflow', 'visible');
this.svgShape = SVG(this.doughnutChart.getDom().firstChild.firstChild).toRoot();
if (this.showTotal) {
this.totalTextNode = this.svgShape.text('').font({
family: 'Roboto',
leading: 1
}).attr({'text-anchor': 'middle'});
this.renderTotal();
}
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.doughnutContent.nativeElement);
this.onResize();
}
private renderTotal() {
this.totalTextNode.text(add => {
add.tspan(this.translate.instant('widgets.doughnut.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)');
add.tspan('').newLine().font({size: '4px'});
add.tspan(this.totalText).newLine().font(
{family: this.settings.totalValueFont.family,
size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit,
weight: this.settings.totalValueFont.weight,
style: this.settings.totalValueFont.style}
).fill(this.totalValueColor.color);
}).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2);
}
private onResize() {
if (this.legendHorizontal) {
this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', null);
this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', null);
this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', null);
}
const shapeWidth = this.doughnutShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.doughnutShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight);
if (this.legendHorizontal) {
this.renderer.setStyle(this.doughnutShape.nativeElement, 'max-width', `${size}px`);
this.renderer.setStyle(this.doughnutShape.nativeElement, 'min-width', `${size}px`);
this.renderer.setStyle(this.doughnutLegend.nativeElement, 'flex', '1');
}
if (!this.settings.autoScale) {
const innerRadius = size / 2 - shapeSegmentWidth;
const outerRadius = size / 2;
this.doughnutOptions.series[0].radius = [innerRadius, outerRadius];
this.doughnutChart.setOption(this.doughnutOptions);
} else {
this.scale = size / shapeSize;
this.renderer.setStyle(this.doughnutChart.getDom().firstChild, 'transform', `scale(${this.scale})`);
}
if (!this.settings.autoScale) {
this.doughnutChart.resize();
}
if (this.showTotal) {
this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2,
(this.settings.autoScale ? shapeSize : shapeHeight) / 2);
}
}
}

80
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts

@ -14,14 +14,16 @@
/// limitations under the License.
///
import {
BackgroundSettings,
BackgroundType,
ColorSettings,
constantColor,
Font
} from '@shared/models/widget-settings.models';
import { BackgroundType, ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
import { pieChartAnimationDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models';
import { DeepPartial } from '@shared/models/common';
import {
LatestChartTooltipValueType,
LatestChartWidgetSettings
} from '@home/components/widget/lib/chart/latest-chart.models';
import { mergeDeep } from '@core/utils';
import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
export enum DoughnutLayout {
default = 'default',
@ -51,41 +53,11 @@ export const horizontalDoughnutLayoutImages = new Map<DoughnutLayout, string>(
]
);
export enum DoughnutTooltipValueType {
absolute = 'absolute',
percentage = 'percentage'
}
export const doughnutTooltipValueTypes = Object.keys(DoughnutTooltipValueType) as DoughnutTooltipValueType[];
export const doughnutTooltipValueTypeTranslations = new Map<DoughnutTooltipValueType, string>(
[
[DoughnutTooltipValueType.absolute, 'widgets.doughnut.tooltip-value-type-absolute'],
[DoughnutTooltipValueType.percentage, 'widgets.doughnut.tooltip-value-type-percentage']
]
);
export interface DoughnutWidgetSettings {
export interface DoughnutWidgetSettings extends LatestChartWidgetSettings {
layout: DoughnutLayout;
autoScale: boolean;
clockwise: boolean;
sortSeries: boolean;
totalValueFont: Font;
totalValueColor: ColorSettings;
showLegend: boolean;
legendPosition: LegendPosition;
legendLabelFont: Font;
legendLabelColor: string;
legendValueFont: Font;
legendValueColor: string;
showTooltip: boolean;
tooltipValueType: DoughnutTooltipValueType;
tooltipValueDecimals: number;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
background: BackgroundSettings;
}
export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSettings => ({
@ -102,6 +74,8 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
lineHeight: '1'
},
totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
animation: mergeDeep({} as EChartsAnimationSettings,
pieChartAnimationDefaultSettings),
showLegend: true,
legendPosition: horizontal ? LegendPosition.right : LegendPosition.bottom,
legendLabelFont: {
@ -123,7 +97,7 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
},
legendValueColor: 'rgba(0, 0, 0, 0.87)',
showTooltip: true,
tooltipValueType: DoughnutTooltipValueType.percentage,
tooltipValueType: LatestChartTooltipValueType.percentage,
tooltipValueDecimals: 0,
tooltipValueFont: {
family: 'Roboto',
@ -146,3 +120,31 @@ export const doughnutDefaultSettings = (horizontal: boolean): DoughnutWidgetSett
}
}
});
export const doughnutPieChartSettings = (settings: DoughnutWidgetSettings): DeepPartial<PieChartSettings> => ({
autoScale: settings.autoScale,
doughnut: true,
clockwise: settings.clockwise,
sortSeries: settings.sortSeries,
showTotal: settings.layout === DoughnutLayout.with_total,
animation: settings.animation,
showLegend: settings.showLegend,
totalValueFont: settings.totalValueFont,
totalValueColor: settings.totalValueColor,
showLabel: false,
borderWidth: 0,
borderColor: '#fff',
borderRadius: '50%',
emphasisScale: false,
emphasisBorderWidth: 2,
emphasisBorderColor: '#fff',
emphasisShadowColor: 'rgba(0, 0, 0, 0.24)',
emphasisShadowBlur: 8,
showTooltip: settings.showTooltip,
tooltipValueType: settings.tooltipValueType,
tooltipValueDecimals: settings.tooltipValueDecimals,
tooltipValueFont: settings.tooltipValueFont,
tooltipValueColor: settings.tooltipValueColor,
tooltipBackgroundColor: settings.tooltipBackgroundColor,
tooltipBackgroundBlur: settings.tooltipBackgroundBlur
});

57
ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts

@ -162,6 +162,63 @@ export const timeSeriesChartShapeOffsetFunctions = new Map<EChartsShape, ECharts
]
);
export enum EChartsAnimationEasing {
linear = 'linear',
quadraticIn = 'quadraticIn',
quadraticOut = 'quadraticOut',
quadraticInOut = 'quadraticInOut',
cubicIn = 'cubicIn',
cubicOut = 'cubicOut',
cubicInOut = 'cubicInOut',
quarticIn = 'quarticIn',
quarticOut = 'quarticOut',
quarticInOut = 'quarticInOut',
quinticIn = 'quinticIn',
quinticOut = 'quinticOut',
quinticInOut = 'quinticInOut',
sinusoidalIn = 'sinusoidalIn',
sinusoidalOut = 'sinusoidalOut',
sinusoidalInOut = 'sinusoidalInOut',
exponentialIn = 'exponentialIn',
exponentialOut = 'exponentialOut',
exponentialInOut = 'exponentialInOut',
circularIn = 'circularIn',
circularOut = 'circularOut',
circularInOut = 'circularInOut',
elasticIn = 'elasticIn',
elasticOut = 'elasticOut',
elasticInOut = 'elasticInOut',
backIn = 'backIn',
backOut = 'backOut',
backInOut = 'backInOut',
bounceIn = 'bounceIn',
bounceOut = 'bounceOut',
bounceInOut = 'bounceInOut'
}
export const echartsAnimationEasings = Object.keys(EChartsAnimationEasing) as EChartsAnimationEasing[];
export interface EChartsAnimationSettings {
animation: boolean;
animationThreshold: number;
animationDuration: number;
animationEasing: EChartsAnimationEasing;
animationDelay: number;
animationDurationUpdate: number;
animationEasingUpdate: EChartsAnimationEasing;
animationDelayUpdate: number;
}
export const echartsAnimationDefaultSettings: EChartsAnimationSettings = {
animation: true,
animationThreshold: 2000,
animationDuration: 500,
animationEasing: EChartsAnimationEasing.cubicOut,
animationDelay: 0,
animationDurationUpdate: 300,
animationEasingUpdate: EChartsAnimationEasing.cubicOut,
animationDelayUpdate: 0
};
export const timeAxisBandWidthCalculator: TimeAxisBandWidthCalculator = (model) => {
let interval: number;

24
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart-widget.component.html

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
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.
-->
<tb-latest-chart
#latestChart
[ctx]="ctx"
[settings]="settings"
[widgetTitlePanel]="widgetTitlePanel"
[callbacks]="callbacks"
></tb-latest-chart>

37
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.html

@ -0,0 +1,37 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
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.
-->
<div class="tb-latest-chart-panel" [style]="backgroundStyle$ | async">
<div class="tb-latest-chart-overlay" [style]="overlayStyle"></div>
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div #chartContent class="tb-latest-chart-content" [class]="legendClass">
<div #chartShape class="tb-latest-chart-shape">
</div>
<div *ngIf="showLegend" #chartLegend class="tb-latest-chart-legend">
<div class="tb-latest-chart-legend-item" *ngFor="let legendItem of legendItems" [class]="{'pointer': !legendItem.total && legendItem.hasValue}"
(mouseenter)="onLegendItemEnter(legendItem)"
(mouseleave)="onLegendItemLeave(legendItem)"
(click)="toggleLegendItem(legendItem)">
<div class="tb-latest-chart-legend-item-label">
<div class="tb-latest-chart-legend-item-label-circle" [style]="{background: (!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendItem.color : null}"></div>
<div [style]="(!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendLabelStyle : disabledLegendLabelStyle">{{ legendItem.label }}</div>
</div>
<div [style]="(!legendItem.dataKey?.hidden && legendItem.hasValue) ? legendValueStyle : disabledLegendValueStyle" class="tb-latest-chart-legend-item-value">{{ legendItem.value }}</div>
</div>
</div>
</div>
</div>

28
ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-doughnut-panel {
.tb-latest-chart-panel {
width: 100%;
height: 100%;
position: relative;
@ -21,10 +21,10 @@
flex-direction: column;
gap: 16px;
padding: 20px 24px 24px 24px;
> div:not(.tb-doughnut-overlay) {
> div:not(.tb-latest-chart-overlay) {
z-index: 1;
}
.tb-doughnut-overlay {
.tb-latest-chart-overlay {
position: absolute;
top: 12px;
left: 12px;
@ -34,7 +34,7 @@
div.tb-widget-title {
padding: 0;
}
.tb-doughnut-content {
.tb-latest-chart-content {
flex: 1;
min-width: 0;
min-height: 0;
@ -50,22 +50,22 @@
&.legend-left {
flex-direction: row-reverse;
}
.tb-doughnut-shape {
.tb-latest-chart-shape {
flex: 1;
min-width: 0;
min-height: 0;
min-width: 10px;
min-height: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.tb-doughnut-legend {
.tb-latest-chart-legend {
display: flex;
justify-content: space-around;
align-items: center;
align-self: stretch;
flex-wrap: wrap;
gap: 8px;
.tb-doughnut-legend-item {
.tb-latest-chart-legend-item {
display: flex;
flex-direction: column;
justify-content: center;
@ -74,19 +74,19 @@
&.pointer {
cursor: pointer;
}
.tb-doughnut-legend-item-label {
.tb-latest-chart-legend-item-label {
display: flex;
align-items: center;
gap: 4px;
color: #ccc;
.tb-doughnut-legend-item-label-circle {
.tb-latest-chart-legend-item-label-circle {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
}
}
.tb-doughnut-legend-item-value {
.tb-latest-chart-legend-item-value {
padding-left: 12px;
color: #ccc;
}
@ -94,11 +94,11 @@
}
&.legend-right, &.legend-left {
gap: 24px;
.tb-doughnut-legend {
.tb-latest-chart-legend {
flex-direction: column;
justify-content: center;
align-items: stretch;
.tb-doughnut-legend-item {
.tb-latest-chart-legend-item {
flex-direction: row;
justify-content: space-between;
align-items: center;

200
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts

@ -0,0 +1,200 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
LatestChartDataItem,
LatestChartLegendItem,
LatestChartSettings,
LatestChartWidgetSettings
} from '@home/components/widget/lib/chart/latest-chart.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models';
import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { TranslateService } from '@ngx-translate/core';
import { LegendPosition } from '@shared/models/widget.models';
export interface LatestChartComponentCallbacks {
createChart: (chartShape: ElementRef<HTMLElement>, renderer: Renderer2) => TbLatestChart<LatestChartSettings>;
onItemClick?: ($event: Event, item: LatestChartDataItem) => void;
}
@Component({
selector: 'tb-latest-chart',
templateUrl: './latest-chart.component.html',
styleUrls: ['./latest-chart.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild('chartContent', {static: false})
chartContent: ElementRef<HTMLElement>;
@ViewChild('chartShape', {static: false})
chartShape: ElementRef<HTMLElement>;
@ViewChild('chartLegend', {static: false})
chartLegend: ElementRef<HTMLElement>;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
@Input()
callbacks: LatestChartComponentCallbacks;
@Input()
settings: LatestChartWidgetSettings;
showLegend: boolean;
legendClass: string;
backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {};
get legendItems(): LatestChartLegendItem[] {
return this.latestChart ? this.latestChart.getLegendItems() : [];
}
legendLabelStyle: ComponentStyle;
legendValueStyle: ComponentStyle;
disabledLegendLabelStyle: ComponentStyle;
disabledLegendValueStyle: ComponentStyle;
private shapeResize$: ResizeObserver;
private legendHorizontal: boolean;
private latestChart: TbLatestChart<LatestChartSettings>;
constructor(private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
private widgetComponent: WidgetComponent,
private renderer: Renderer2,
private translate: TranslateService,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.showLegend = this.settings.showLegend;
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
if (this.showLegend) {
this.legendClass = `legend-${this.settings.legendPosition}`;
this.legendHorizontal = [LegendPosition.left, LegendPosition.right].includes(this.settings.legendPosition);
this.legendLabelStyle = textStyle(this.settings.legendLabelFont);
this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont);
this.legendLabelStyle.color = this.settings.legendLabelColor;
this.legendValueStyle = textStyle(this.settings.legendValueFont);
this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont);
this.legendValueStyle.color = this.settings.legendValueColor;
}
}
ngAfterViewInit() {
this.latestChart = this.callbacks.createChart(this.chartShape, this.renderer);
this.latestChart.onItemClick(this.callbacks.onItemClick);
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.chartContent.nativeElement);
this.onResize();
}
ngOnDestroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.latestChart) {
this.latestChart.destroy();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
if (this.latestChart) {
this.latestChart.update();
}
if (this.showLegend) {
this.cd.detectChanges();
if (this.legendHorizontal) {
setTimeout(() => {
this.onResize();
});
}
}
}
public onLegendItemEnter(item: LatestChartLegendItem) {
if (!item.total && item.hasValue) {
this.latestChart.keyEnter(item.dataKey);
}
}
public onLegendItemLeave(item: LatestChartLegendItem) {
if (!item.total && item.hasValue) {
this.latestChart.keyLeave(item.dataKey);
}
}
public toggleLegendItem(item: LatestChartLegendItem) {
if (!item.total && item.hasValue) {
this.latestChart.toggleKey(item.dataKey);
}
}
private onResize() {
if (this.legendHorizontal) {
this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', null);
this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', null);
this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', null);
}
const shapeWidth = this.chartShape.nativeElement.getBoundingClientRect().width;
const shapeHeight = this.chartShape.nativeElement.getBoundingClientRect().height;
const size = Math.min(shapeWidth, shapeHeight);
if (this.legendHorizontal) {
this.renderer.setStyle(this.chartShape.nativeElement, 'max-width', `${size}px`);
this.renderer.setStyle(this.chartShape.nativeElement, 'min-width', `${size}px`);
this.renderer.setStyle(this.chartLegend.nativeElement, 'flex', '1');
}
}
}

122
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts

@ -0,0 +1,122 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { DataKey, Datasource, LegendPosition } from '@shared/models/widget.models';
import { BackgroundSettings, Font } from '@shared/models/widget-settings.models';
import { Renderer2 } from '@angular/core';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
export interface LatestChartDataItem {
id: number;
datasource: Datasource;
dataKey: DataKey;
value: number;
hasValue: boolean;
enabled: boolean;
}
export interface LatestChartLegendItem {
dataKey?: DataKey;
color: string;
label: string;
value: string;
hasValue: boolean;
total?: boolean;
}
export enum LatestChartTooltipValueType {
absolute = 'absolute',
percentage = 'percentage'
}
export const latestChartTooltipValueTypes = Object.keys(LatestChartTooltipValueType) as LatestChartTooltipValueType[];
export const latestChartTooltipValueTypeTranslations = new Map<LatestChartTooltipValueType, string>(
[
[LatestChartTooltipValueType.absolute, 'widgets.latest-chart.tooltip-value-type-absolute'],
[LatestChartTooltipValueType.percentage, 'widgets.latest-chart.tooltip-value-type-percentage']
]
);
export interface LatestChartTooltipSettings {
showTooltip: boolean;
tooltipValueType: LatestChartTooltipValueType;
tooltipValueDecimals: number;
tooltipValueFont: Font;
tooltipValueColor: string;
tooltipBackgroundColor: string;
tooltipBackgroundBlur: number;
}
export interface LatestChartSettings extends LatestChartTooltipSettings {
autoScale?: boolean;
sortSeries: boolean;
showTotal?: boolean;
showLegend: boolean;
animation: EChartsAnimationSettings;
}
export interface LatestChartWidgetSettings extends LatestChartSettings {
legendPosition: LegendPosition;
legendLabelFont: Font;
legendLabelColor: string;
legendValueFont: Font;
legendValueColor: string;
background: BackgroundSettings;
}
export const latestChartTooltipFormatter = (renderer: Renderer2,
settings: LatestChartTooltipSettings,
params: CallbackDataParams,
units: string,
total: number): null | HTMLElement => {
if (!params.name) {
return null;
}
let value: string;
if (settings.tooltipValueType === LatestChartTooltipValueType.percentage) {
const percents = isDefinedAndNotNull(params.percent) ? params.percent : (params.value as number) / total * 100;
value = formatValue(percents, settings.tooltipValueDecimals, '%', false);
} else {
value = formatValue(params.value, settings.tooltipValueDecimals, units, false);
}
const textElement: HTMLElement = renderer.createElement('div');
renderer.setStyle(textElement, 'display', 'inline-flex');
renderer.setStyle(textElement, 'align-items', 'center');
renderer.setStyle(textElement, 'gap', '8px');
const labelElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(labelElement, renderer.createText(params.name));
renderer.setStyle(labelElement, 'font-family', 'Roboto');
renderer.setStyle(labelElement, 'font-size', '11px');
renderer.setStyle(labelElement, 'font-style', 'normal');
renderer.setStyle(labelElement, 'font-weight', '400');
renderer.setStyle(labelElement, 'line-height', '16px');
renderer.setStyle(labelElement, 'letter-spacing', '0.25px');
renderer.setStyle(labelElement, 'color', 'rgba(0, 0, 0, 0.38)');
const valueElement: HTMLElement = renderer.createElement('div');
renderer.appendChild(valueElement, renderer.createText(value));
renderer.setStyle(valueElement, 'font-family', settings.tooltipValueFont.family);
renderer.setStyle(valueElement, 'font-size', settings.tooltipValueFont.size + settings.tooltipValueFont.sizeUnit);
renderer.setStyle(valueElement, 'font-style', settings.tooltipValueFont.style);
renderer.setStyle(valueElement, 'font-weight', settings.tooltipValueFont.weight);
renderer.setStyle(valueElement, 'line-height', settings.tooltipValueFont.lineHeight);
renderer.setStyle(valueElement, 'color', settings.tooltipValueColor);
renderer.appendChild(textElement, labelElement);
renderer.appendChild(textElement, valueElement);
return textElement;
};

374
ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts

@ -0,0 +1,374 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { ResizeObserver } from '@juggle/resize-observer';
import { ECharts, echartsModule, EChartsOption } from '@home/components/widget/lib/chart/echarts-widget.models';
import {
LatestChartDataItem,
LatestChartLegendItem,
LatestChartSettings,
latestChartTooltipFormatter
} from '@home/components/widget/lib/chart/latest-chart.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common';
import { Renderer2 } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { formatValue, isDefinedAndNotNull, isNumeric, mergeDeep } from '@core/utils';
import { DataKey } from '@shared/models/widget.models';
import * as echarts from 'echarts/core';
import { CallbackDataParams } from 'echarts/types/dist/shared';
import { SVG, Svg } from '@svgdotjs/svg.js';
export abstract class TbLatestChart<S extends LatestChartSettings> {
private readonly shapeResize$: ResizeObserver;
protected readonly settings: S;
protected readonly decimals: number;
protected readonly units: string;
protected total = 0;
protected totalText = 'N/A';
protected latestChart: ECharts;
protected latestChartOption: EChartsOption;
protected svgShape: Svg;
protected dataItems: LatestChartDataItem[] = [];
private legendItems: LatestChartLegendItem[] = [];
private itemClick: ($event: Event, item: LatestChartDataItem) => void;
protected constructor(private ctx: WidgetContext,
private readonly inputSettings: DeepPartial<S>,
protected chartElement: HTMLElement,
private renderer: Renderer2,
protected translate: TranslateService,
private autoResize = true) {
this.settings = mergeDeep({} as S,
this.defaultSettings(),
this.inputSettings as S);
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
this.initSettings();
this.setupData();
this.onResize();
if (this.autoResize) {
this.shapeResize$ = new ResizeObserver(() => {
this.onResize();
});
this.shapeResize$.observe(this.chartElement);
}
}
private setupData(): void {
let counter = 0;
if (this.ctx.datasources.length) {
for (const datasource of this.ctx.datasources) {
const dataKeys = datasource.dataKeys;
for (const dataKey of dataKeys) {
const id = counter++;
this.dataItems.push({
id,
datasource,
dataKey,
value: 0,
hasValue: false,
enabled: true
});
if (this.settings.showLegend) {
this.legendItems.push(
{
dataKey,
value: '--',
label: dataKey.label,
color: dataKey.color,
hasValue: false
}
);
}
}
}
}
if (this.settings.sortSeries) {
this.dataItems.sort((a, b) => a.dataKey.label.localeCompare(b.dataKey.label));
if (this.settings.showLegend) {
this.legendItems.sort((a, b) => a.label.localeCompare(b.label));
}
}
if (this.settings.showLegend && !this.settings.showTotal) {
this.legendItems.push(
{
value: '--',
label: this.translate.instant('widgets.latest-chart.total'),
color: 'rgba(0, 0, 0, 0.06)',
hasValue: false,
total: true
}
);
}
}
public getLegendItems(): LatestChartLegendItem[] {
return this.legendItems;
}
public update(): void {
for (const dsData of this.ctx.data) {
let value = 0;
const tsValue = dsData.data[0];
const dataItem = this.dataItems.find(item => item.dataKey === dsData.dataKey);
if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) {
value = tsValue[1];
dataItem.hasValue = true;
dataItem.value = Number(value);
} else {
dataItem.hasValue = false;
dataItem.value = 0;
}
}
this.updateSeriesData();
}
public keyEnter(dataKey: DataKey): void {
const item = this.dataItems.find(d => d.dataKey === dataKey);
if (item) {
const dataIndex = this.latestChartOption.series[0].data.findIndex(
(d: any) => d.id === item.id);
if (dataIndex > -1) {
this.latestChart.dispatchAction({
type: 'highlight',
dataIndex
});
}
}
}
public keyLeave(dataKey: DataKey): void {
const item = this.dataItems.find(d => d.dataKey === dataKey);
if (item) {
const dataIndex = this.latestChartOption.series[0].data.findIndex(
(d: any) => d.id === item.id);
if (dataIndex > -1) {
this.latestChart.dispatchAction({
type: 'downplay',
dataIndex
});
}
}
}
public toggleKey(dataKey: DataKey): void {
const enable = dataKey.hidden;
const dataItem = this.dataItems.find(d => d.dataKey === dataKey);
if (dataItem) {
let dataIndex = this.latestChartOption.series[0].data.findIndex(
(d: any) => d.id === dataItem.id);
dataItem.enabled = enable;
if (!enable && dataIndex > -1) {
this.latestChart.dispatchAction({
type: 'downplay',
dataIndex
});
}
this.updateSeriesData();
dataKey.hidden = !enable;
if (enable) {
dataIndex = this.latestChartOption.series[0].data.findIndex(
(d: any) => d.id === dataItem.id);
if (dataIndex > -1) {
this.latestChart.dispatchAction({
type: 'highlight',
dataIndex
});
}
}
}
}
public destroy(): void {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
if (this.latestChart) {
this.latestChart.dispose();
}
}
public resize(): void {
this.onResize();
}
public onItemClick(itemClick: ($event: Event, item: LatestChartDataItem) => void) {
this.itemClick = itemClick;
}
private updateSeriesData(initial = false) {
this.total = 0;
this.totalText = 'N/A';
let hasValue = false;
for (const dataItem of this.dataItems) {
if (dataItem.enabled && dataItem.hasValue) {
hasValue = true;
this.total += dataItem.value;
}
if (this.settings.showLegend) {
const legendItem = this.legendItems.find(item => item.dataKey === dataItem.dataKey);
if (dataItem.hasValue) {
legendItem.hasValue = true;
legendItem.value = formatValue(dataItem.value, this.decimals, this.units, false);
} else {
legendItem.hasValue = false;
legendItem.value = '--';
}
}
}
if (this.settings.showTotal || this.settings.showLegend) {
if (hasValue) {
this.totalText = formatValue(this.total, this.decimals, this.units, false);
if (this.settings.showLegend && !this.settings.showTotal) {
this.legendItems[this.legendItems.length - 1].hasValue = true;
this.legendItems[this.legendItems.length - 1].value = this.totalText;
}
} else if (this.settings.showLegend && !this.settings.showTotal) {
this.legendItems[this.legendItems.length - 1].hasValue = false;
this.legendItems[this.legendItems.length - 1].value = '--';
}
}
this.doUpdateSeriesData();
this.latestChart.setOption(this.latestChartOption);
this.afterUpdateSeriesData(initial);
}
private drawChart() {
echartsModule.init();
this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal');
this.latestChart = echarts.init(this.chartElement, null, {
renderer: 'svg',
width: this.settings.autoScale ? this.initialShapeWidth() : undefined,
height: this.settings.autoScale ? this.initialShapeHeight() : undefined,
});
this.latestChartOption = {
tooltip: {
trigger: this.settings.showTooltip ? 'item' : 'none',
confine: false,
appendTo: 'body',
formatter: (params: CallbackDataParams) =>
this.settings.showTooltip
? latestChartTooltipFormatter(this.renderer, this.settings, params, this.units, this.total)
: undefined,
padding: [4, 8],
backgroundColor: this.settings.tooltipBackgroundColor,
extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);`,
position: (pos) => [pos[0] + 10, pos[1] + 10]
},
animation: this.settings.animation.animation,
animationThreshold: this.settings.animation.animationThreshold,
animationDuration: this.settings.animation.animationDuration,
animationEasing: this.settings.animation.animationEasing,
animationDelay: this.settings.animation.animationDelay,
animationDurationUpdate: this.settings.animation.animationDurationUpdate,
animationEasingUpdate: this.settings.animation.animationEasingUpdate,
animationDelayUpdate: this.settings.animation.animationDelayUpdate
};
this.prepareLatestChartOption();
this.updateSeriesData(true);
this.renderer.setStyle(this.latestChart.getDom().firstChild, 'overflow', 'visible');
if (this.settings.autoScale) {
this.renderer.setStyle(this.latestChart.getDom().firstChild, 'position', 'absolute');
}
this.renderer.setStyle(this.latestChart.getDom().firstChild.firstChild, 'overflow', 'visible');
this.svgShape = SVG(this.latestChart.getDom().firstChild.firstChild).toRoot();
this.afterDrawChart();
this.latestChart.on('click', (params) => {
this.onChartClick(params);
});
}
protected onChartClick(params: echarts.ECElementEvent) {
if (params.componentType === 'series') {
if (params.data) {
const data = params.data as any;
if (isDefinedAndNotNull(data?.id)) {
const item = this.dataItems.find(d => d.id === data.id);
if (item && this.itemClick) {
this.itemClick(params.event.event, item);
}
}
}
}
};
private onResize() {
const shapeWidth = this.chartElement.offsetWidth;
const shapeHeight = this.chartElement.offsetHeight;
if (shapeWidth && shapeHeight) {
if (!this.latestChart) {
this.drawChart();
} else {
const width = this.latestChart.getWidth();
const height = this.latestChart.getHeight();
if (width !== shapeWidth || height !== shapeHeight) {
this.beforeResize(shapeWidth, shapeHeight);
if (!this.settings.autoScale) {
this.latestChart.resize();
} else {
let scale: number;
if (shapeWidth < shapeHeight) {
scale = shapeWidth / this.initialShapeWidth();
} else {
scale = shapeHeight / this.initialShapeHeight();
}
this.renderer.setStyle(this.latestChart.getDom().firstChild, 'transform', `scale(${scale})`);
}
this.afterResize(shapeWidth, shapeHeight);
}
}
}
}
protected initSettings() {
}
protected initialShapeWidth(): number {
return 100;
}
protected initialShapeHeight(): number {
return 100;
}
protected beforeResize(_shapeWidth: number, _shapeHeight: number) {};
protected afterResize(_shapeWidth: number, _shapeHeight: number) {};
protected afterDrawChart() {};
protected afterUpdateSeriesData(_initial: boolean) {};
protected abstract defaultSettings(): S;
protected abstract prepareLatestChartOption(): void;
protected abstract doUpdateSeriesData(): void;
}

86
ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.component.ts

@ -0,0 +1,86 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { Component, Input, OnInit, TemplateRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { TranslateService } from '@ngx-translate/core';
import { TbPieChart } from '@home/components/widget/lib/chart/pie-chart';
import {
LatestChartComponent,
LatestChartComponentCallbacks
} from '@home/components/widget/lib/chart/latest-chart.component';
import {
pieChartWidgetDefaultSettings,
pieChartWidgetPieChartSettings,
PieChartWidgetSettings
} from '@home/components/widget/lib/chart/pie-chart-widget.models';
@Component({
selector: 'tb-pie-chart-widget',
templateUrl: './latest-chart-widget.component.html',
styleUrls: [],
encapsulation: ViewEncapsulation.None
})
export class PieChartWidgetComponent implements OnInit {
@ViewChild('latestChart')
latestChart: LatestChartComponent;
@Input()
ctx: WidgetContext;
@Input()
widgetTitlePanel: TemplateRef<any>;
settings: PieChartWidgetSettings;
callbacks: LatestChartComponentCallbacks;
constructor(private widgetComponent: WidgetComponent,
private translate: TranslateService) {
}
ngOnInit(): void {
this.ctx.$scope.pieChartWidget = this;
this.settings = {...pieChartWidgetDefaultSettings, ...this.ctx.settings};
this.callbacks = {
createChart: (chartShape, renderer) => {
const settings = pieChartWidgetPieChartSettings(this.settings);
return new TbPieChart(this.ctx, settings, chartShape.nativeElement, renderer, this.translate, true);
},
onItemClick: ($event: Event, item) => {
const descriptors = this.ctx.actionsApi.getActionDescriptors('sliceClick');
if ($event && descriptors.length) {
$event.stopPropagation();
const datasource = item.datasource;
const entityId = datasource ? datasource.entity?.id : null;
const entityName = datasource ? datasource.entityName : null;
const entityLabel = datasource ? datasource.entityLabel : null;
this.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, item, entityLabel);
}
}
};
}
public onInit() {
this.latestChart?.onInit();
}
public onDataUpdated() {
this.latestChart?.onDataUpdated();
}
}

131
ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts

@ -0,0 +1,131 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import {
LatestChartTooltipValueType,
LatestChartWidgetSettings
} from '@home/components/widget/lib/chart/latest-chart.models';
import { BackgroundType, Font } from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
import { DeepPartial } from '@shared/models/common';
import {
pieChartAnimationDefaultSettings,
PieChartLabelPosition,
PieChartSettings
} from '@home/components/widget/lib/chart/pie-chart.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { EChartsAnimationSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
export interface PieChartWidgetSettings extends LatestChartWidgetSettings {
showLabel: boolean;
labelPosition: PieChartLabelPosition;
labelFont: Font;
labelColor: string;
borderWidth: number;
borderColor: string;
radius: number;
clockwise: boolean;
}
export const pieChartWidgetDefaultSettings: PieChartWidgetSettings = {
showLabel: true,
labelPosition: PieChartLabelPosition.outside,
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: 'normal',
lineHeight: '1.2'
},
labelColor: '#000',
borderWidth: 0,
borderColor: '#000',
radius: 80,
clockwise: false,
sortSeries: false,
animation: mergeDeep({} as EChartsAnimationSettings,
pieChartAnimationDefaultSettings),
showLegend: true,
legendPosition: LegendPosition.bottom,
legendLabelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '400',
lineHeight: '16px'
},
legendLabelColor: 'rgba(0, 0, 0, 0.38)',
legendValueFont: {
family: 'Roboto',
size: 14,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '20px'
},
legendValueColor: 'rgba(0, 0, 0, 0.87)',
showTooltip: true,
tooltipValueType: LatestChartTooltipValueType.percentage,
tooltipValueDecimals: 0,
tooltipValueFont: {
family: 'Roboto',
size: 13,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
tooltipBackgroundBlur: 4,
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
};
export const pieChartWidgetPieChartSettings = (settings: PieChartWidgetSettings): DeepPartial<PieChartSettings> => ({
autoScale: false,
doughnut: false,
clockwise: settings.clockwise,
sortSeries: settings.sortSeries,
showTotal: false,
animation: settings.animation,
showLegend: settings.showLegend,
showLabel: settings.showLabel,
labelPosition: settings.labelPosition,
labelFont: settings.labelFont,
labelColor: settings.labelColor,
borderWidth: settings.borderWidth,
borderColor: settings.borderColor,
radius: isDefinedAndNotNull(settings.radius) ? settings.radius + '%' : undefined,
emphasisBorderWidth: settings.borderWidth,
emphasisBorderColor: settings.borderColor,
showTooltip: settings.showTooltip,
tooltipValueType: settings.tooltipValueType,
tooltipValueDecimals: settings.tooltipValueDecimals,
tooltipValueFont: settings.tooltipValueFont,
tooltipValueColor: settings.tooltipValueColor,
tooltipBackgroundColor: settings.tooltipBackgroundColor,
tooltipBackgroundBlur: settings.tooltipBackgroundBlur
});

126
ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.models.ts

@ -0,0 +1,126 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { ColorSettings, constantColor, Font } from '@shared/models/widget-settings.models';
import {
LatestChartSettings,
LatestChartTooltipValueType
} from '@home/components/widget/lib/chart/latest-chart.models';
import { mergeDeep } from '@core/utils';
import {
EChartsAnimationEasing,
EChartsAnimationSettings
} from '@home/components/widget/lib/chart/echarts-widget.models';
export enum PieChartLabelPosition {
outside = 'outside',
inside = 'inside'
}
export const pieChartLabelPositions = Object.keys(PieChartLabelPosition) as PieChartLabelPosition[];
export const pieChartLabelPositionTranslations = new Map<PieChartLabelPosition, string>(
[
[PieChartLabelPosition.outside, 'widgets.pie-chart.label-position-outside'],
[PieChartLabelPosition.inside, 'widgets.pie-chart.label-position-inside']
]
);
export interface PieChartSettings extends LatestChartSettings {
doughnut: boolean;
radius: string;
clockwise: boolean;
totalValueFont: Font;
totalValueColor: ColorSettings;
showLabel: boolean;
labelPosition: PieChartLabelPosition;
labelFont: Font;
labelColor: string;
borderWidth: number;
borderColor: string;
borderRadius: string;
emphasisScale: boolean;
emphasisBorderWidth: number;
emphasisBorderColor: string;
emphasisShadowBlur: number;
emphasisShadowColor: string;
}
export const pieChartAnimationDefaultSettings: EChartsAnimationSettings = {
animation: true,
animationThreshold: 2000,
animationDuration: 1000,
animationEasing: EChartsAnimationEasing.cubicOut,
animationDelay: 0,
animationDurationUpdate: 500,
animationEasingUpdate: EChartsAnimationEasing.cubicOut,
animationDelayUpdate: 0
};
export const pieChartDefaultSettings: PieChartSettings = {
autoScale: false,
doughnut: false,
radius: '80%',
clockwise: false,
sortSeries: false,
showTotal: false,
animation: mergeDeep({} as EChartsAnimationSettings,
pieChartAnimationDefaultSettings),
showLegend: true,
totalValueFont: {
family: 'Roboto',
size: 24,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '1'
},
totalValueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showLabel: false,
labelPosition: PieChartLabelPosition.outside,
labelFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: 'normal',
lineHeight: '1'
},
labelColor: '#000',
borderWidth: 0,
borderColor: '#000',
borderRadius: '0%',
emphasisScale: true,
emphasisBorderWidth: 0,
emphasisBorderColor: '#000',
emphasisShadowBlur: 10,
emphasisShadowColor: 'rgba(0, 0, 0, 0.5)',
showTooltip: true,
tooltipValueType: LatestChartTooltipValueType.percentage,
tooltipValueDecimals: 0,
tooltipValueFont: {
family: 'Roboto',
size: 13,
sizeUnit: 'px',
style: 'normal',
weight: '500',
lineHeight: '16px'
},
tooltipValueColor: 'rgba(0, 0, 0, 0.76)',
tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)',
tooltipBackgroundBlur: 4
};

194
ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts

@ -0,0 +1,194 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { pieChartDefaultSettings, PieChartSettings } from '@home/components/widget/lib/chart/pie-chart.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { DeepPartial } from '@shared/models/common';
import { Renderer2 } from '@angular/core';
import { ColorProcessor, textStyle } from '@shared/models/widget-settings.models';
import { PieDataItemOption } from 'echarts/types/src/chart/pie/PieSeries';
import { Text } from '@svgdotjs/svg.js';
import { TranslateService } from '@ngx-translate/core';
import { TbLatestChart } from '@home/components/widget/lib/chart/latest-chart';
import { formatValue } from '@core/utils';
const shapeSize = 134;
const shapeSegmentWidth = 13.4;
export class TbPieChart extends TbLatestChart<PieChartSettings> {
private totalValueColor: ColorProcessor;
private totalTextNode: Text;
constructor(ctx: WidgetContext,
inputSettings: DeepPartial<PieChartSettings>,
chartElement: HTMLElement,
renderer: Renderer2,
translate: TranslateService,
autoResize = true) {
super(ctx, inputSettings, chartElement, renderer, translate, autoResize);
}
protected defaultSettings(): PieChartSettings {
return pieChartDefaultSettings;
}
protected initSettings() {
if (this.settings.showTotal) {
this.totalValueColor = ColorProcessor.fromSettings(this.settings.totalValueColor);
}
}
protected prepareLatestChartOption() {
const shapeWidth = this.chartElement.offsetWidth;
const shapeHeight = this.chartElement.offsetHeight;
const size = this.settings.autoScale ? shapeSize : Math.min(shapeWidth, shapeHeight);
const innerRadius = size / 2 - shapeSegmentWidth;
const outerRadius = size / 2;
const labelStyle = textStyle(this.settings.labelFont);
labelStyle.fontSize = this.settings.labelFont.size;
labelStyle.lineHeight = labelStyle.fontSize * 1.2;
labelStyle.color = this.settings.labelColor;
this.latestChartOption.series = [
{
type: 'pie',
clockwise: this.settings.clockwise,
radius: this.settings.doughnut ? [innerRadius, outerRadius] : this.settings.radius,
avoidLabelOverlap: false,
itemStyle: {
borderRadius: this.settings.borderRadius,
borderWidth: this.settings.borderWidth,
borderColor: this.settings.borderColor
},
label: {
show: this.settings.showLabel,
position: this.settings.labelPosition,
formatter: (params) => {
const percents = params.percent;
const value = formatValue(percents, 0, '%', false);
return `{label|${params.name}\n${value}}`;
},
rich: {
label: labelStyle
}
},
emphasis: {
scale: this.settings.emphasisScale,
itemStyle: {
borderColor: this.settings.emphasisBorderColor,
borderWidth: this.settings.emphasisBorderWidth,
shadowColor: this.settings.emphasisShadowColor,
shadowBlur: this.settings.emphasisShadowBlur
},
label: {
show: this.settings.showLabel
}
},
animation: this.settings.animation.animation,
animationThreshold: this.settings.animation.animationThreshold,
animationDuration: this.settings.animation.animationDuration,
animationEasing: this.settings.animation.animationEasing,
animationDelay: this.settings.animation.animationDelay,
animationDurationUpdate: this.settings.animation.animationDurationUpdate,
animationEasingUpdate: this.settings.animation.animationEasingUpdate,
animationDelayUpdate: this.settings.animation.animationDelayUpdate
}
];
}
protected afterDrawChart() {
if (this.settings.showTotal) {
this.totalTextNode = this.svgShape.text('').font({
family: 'Roboto',
leading: 1
}).attr({'text-anchor': 'middle'});
this.renderTotal();
}
};
protected doUpdateSeriesData() {
const seriesData: PieDataItemOption[] = [];
const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue);
for (const dataItem of this.dataItems) {
if (dataItem.enabled && dataItem.hasValue) {
seriesData.push(
{id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}}
);
if (this.settings.doughnut && enabledDataItems.length > 1) {
seriesData.push({
value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true}
});
}
}
}
if (this.settings.doughnut) {
for (let i = 1; i < seriesData.length; i += 2) {
seriesData[i].value = this.total / 100;
}
}
this.latestChartOption.series[0].data = seriesData;
}
protected afterUpdateSeriesData(initial: boolean) {
if (this.settings.showTotal) {
this.totalValueColor.update(this.total);
if (!initial) {
this.renderTotal();
}
}
};
protected initialShapeWidth(): number {
return shapeSize;
}
protected initialShapeHeight(): number {
return shapeSize;
}
protected beforeResize(shapeWidth: number, shapeHeight: number) {
if (!this.settings.autoScale) {
if (this.settings.doughnut) {
const size = Math.min(shapeWidth, shapeHeight);
const innerRadius = size / 2 - shapeSegmentWidth;
const outerRadius = size / 2;
this.latestChartOption.series[0].radius = [innerRadius, outerRadius];
this.latestChart.setOption(this.latestChartOption);
}
}
};
protected afterResize(shapeWidth: number, shapeHeight: number) {
if (this.settings.showTotal) {
this.totalTextNode.center((this.settings.autoScale ? shapeSize : shapeWidth) / 2,
(this.settings.autoScale ? shapeSize : shapeHeight) / 2);
}
};
private renderTotal() {
this.totalTextNode.text(add => {
add.tspan(this.translate.instant('widgets.latest-chart.total')).font({size: '12px', weight: 400}).fill('rgba(0, 0, 0, 0.38)');
add.tspan('').newLine().font({size: '4px'});
add.tspan(this.totalText).newLine().font(
{family: this.settings.totalValueFont.family,
size: this.settings.totalValueFont.size + this.settings.totalValueFont.sizeUnit,
weight: this.settings.totalValueFont.weight,
style: this.settings.totalValueFont.style}
).fill(this.totalValueColor.color);
}).center(this.svgShape.bbox().width / 2, this.svgShape.bbox().height / 2);
}
}

16
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -24,15 +24,19 @@ import {
sortedColorRange
} from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
import { EChartsShape, EChartsTooltipWidgetSettings } from '@home/components/widget/lib/chart/echarts-widget.models';
import {
echartsAnimationDefaultSettings,
EChartsAnimationSettings,
EChartsShape,
EChartsTooltipWidgetSettings
} from '@home/components/widget/lib/chart/echarts-widget.models';
import {
createTimeSeriesChartVisualMapPiece,
defaultTimeSeriesChartXAxisSettings,
defaultTimeSeriesChartYAxisSettings,
LineSeriesStepType,
SeriesFillType,
SeriesLabelPosition, ThresholdLabelPosition, timeSeriesChartAnimationDefaultSettings,
TimeSeriesChartAnimationSettings,
SeriesLabelPosition, ThresholdLabelPosition,
timeSeriesChartColorScheme, timeSeriesChartGridDefaultSettings, TimeSeriesChartGridSettings,
TimeSeriesChartKeySettings,
TimeSeriesChartLineType,
@ -84,7 +88,7 @@ export interface RangeChartWidgetSettings extends EChartsTooltipWidgetSettings {
grid: TimeSeriesChartGridSettings;
yAxis: TimeSeriesChartYAxisSettings;
xAxis: TimeSeriesChartXAxisSettings;
animation: TimeSeriesChartAnimationSettings;
animation: EChartsAnimationSettings;
thresholds: TimeSeriesChartThreshold[];
showLegend: boolean;
legendPosition: LegendPosition;
@ -150,8 +154,8 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = {
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings,
{showSplitLines: false} as TimeSeriesChartXAxisSettings),
animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
timeSeriesChartAnimationDefaultSettings),
animation: mergeDeep({} as EChartsAnimationSettings,
echartsAnimationDefaultSettings),
thresholds: [],
showLegend: true,
legendPosition: LegendPosition.top,

66
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts

@ -16,6 +16,8 @@
import {
ECharts,
echartsAnimationDefaultSettings,
EChartsAnimationSettings,
EChartsOption,
EChartsSeriesItem,
EChartsShape,
@ -571,64 +573,6 @@ export interface TimeSeriesChartBarWidthSettings {
intervalGap: number;
}
export enum TimeSeriesChartAnimationEasing {
linear = 'linear',
quadraticIn = 'quadraticIn',
quadraticOut = 'quadraticOut',
quadraticInOut = 'quadraticInOut',
cubicIn = 'cubicIn',
cubicOut = 'cubicOut',
cubicInOut = 'cubicInOut',
quarticIn = 'quarticIn',
quarticOut = 'quarticOut',
quarticInOut = 'quarticInOut',
quinticIn = 'quinticIn',
quinticOut = 'quinticOut',
quinticInOut = 'quinticInOut',
sinusoidalIn = 'sinusoidalIn',
sinusoidalOut = 'sinusoidalOut',
sinusoidalInOut = 'sinusoidalInOut',
exponentialIn = 'exponentialIn',
exponentialOut = 'exponentialOut',
exponentialInOut = 'exponentialInOut',
circularIn = 'circularIn',
circularOut = 'circularOut',
circularInOut = 'circularInOut',
elasticIn = 'elasticIn',
elasticOut = 'elasticOut',
elasticInOut = 'elasticInOut',
backIn = 'backIn',
backOut = 'backOut',
backInOut = 'backInOut',
bounceIn = 'bounceIn',
bounceOut = 'bounceOut',
bounceInOut = 'bounceInOut'
}
export const timeSeriesChartAnimationEasings = Object.keys(TimeSeriesChartAnimationEasing) as TimeSeriesChartAnimationEasing[];
export interface TimeSeriesChartAnimationSettings {
animation: boolean;
animationThreshold: number;
animationDuration: number;
animationEasing: TimeSeriesChartAnimationEasing;
animationDelay: number;
animationDurationUpdate: number;
animationEasingUpdate: TimeSeriesChartAnimationEasing;
animationDelayUpdate: number;
}
export const timeSeriesChartAnimationDefaultSettings: TimeSeriesChartAnimationSettings = {
animation: true,
animationThreshold: 2000,
animationDuration: 500,
animationEasing: TimeSeriesChartAnimationEasing.cubicOut,
animationDelay: 0,
animationDurationUpdate: 300,
animationEasingUpdate: TimeSeriesChartAnimationEasing.cubicOut,
animationDelayUpdate: 0
};
export interface TimeSeriesChartVisualMapPiece {
lt?: number;
gt?: number;
@ -721,7 +665,7 @@ export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings, T
grid: TimeSeriesChartGridSettings;
yAxes: TimeSeriesChartYAxes;
xAxis: TimeSeriesChartXAxisSettings;
animation: TimeSeriesChartAnimationSettings;
animation: EChartsAnimationSettings;
barWidthSettings: TimeSeriesChartBarWidthSettings;
noAggregationBarWidthSettings: TimeSeriesChartNoAggregationBarWidthSettings;
visualMapSettings?: TimeSeriesChartVisualMapSettings;
@ -742,8 +686,8 @@ export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = {
},
xAxis: mergeDeep({} as TimeSeriesChartXAxisSettings,
defaultTimeSeriesChartXAxisSettings),
animation: mergeDeep({} as TimeSeriesChartAnimationSettings,
timeSeriesChartAnimationDefaultSettings),
animation: mergeDeep({} as EChartsAnimationSettings,
echartsAnimationDefaultSettings),
barWidthSettings: {
barGap: 0.3,
intervalGap: 0.6

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.html

@ -227,9 +227,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.html

@ -32,17 +32,17 @@
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.doughnut.auto-scale' | translate }}
{{ 'widgets.latest-chart.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="clockwise">
{{ 'widgets.doughnut.clockwise-layout' | translate }}
{{ 'widgets.latest-chart.clockwise-layout' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sortSeries">
{{ 'widgets.doughnut.sort-series' | translate }}
{{ 'widgets.latest-chart.sort-series' | translate }}
</mat-slide-toggle>
</div>
<div [fxShow]="totalEnabled" class="tb-form-row space-between">
@ -119,8 +119,8 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tooltipValueType">
<mat-option *ngFor="let type of doughnutTooltipValueTypes" [value]="type">
{{ doughnutTooltipValueTypeTranslationMap.get(type) | translate }}
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type">
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@ -154,6 +154,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-echarts-animation-settings
formControlName="animation">
</tb-echarts-animation-settings>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts

@ -24,19 +24,22 @@ import {
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { formatValue, isDefinedAndNotNull, mergeDeep } from '@core/utils';
import {
doughnutDefaultSettings,
DoughnutLayout,
doughnutLayoutImages,
doughnutLayouts,
doughnutLayoutTranslations,
DoughnutTooltipValueType,
doughnutTooltipValueTypes,
doughnutTooltipValueTypeTranslations,
DoughnutWidgetSettings,
horizontalDoughnutLayoutImages
} from '@home/components/widget/lib/chart/doughnut-widget.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
LatestChartTooltipValueType,
latestChartTooltipValueTypes,
latestChartTooltipValueTypeTranslations
} from '@home/components/widget/lib/chart/latest-chart.models';
@Component({
selector: 'tb-doughnut-widget-settings',
@ -62,9 +65,9 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent {
legendPositionTranslationMap = legendPositionTranslationMap;
doughnutTooltipValueTypes = doughnutTooltipValueTypes;
latestChartTooltipValueTypes = latestChartTooltipValueTypes;
doughnutTooltipValueTypeTranslationMap = doughnutTooltipValueTypeTranslations;
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations;
doughnutWidgetSettingsForm: UntypedFormGroup;
@ -88,7 +91,7 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent {
}
protected defaultSettings(): WidgetSettings {
return doughnutDefaultSettings(this.horizontal);
return mergeDeep<DoughnutWidgetSettings>({} as DoughnutWidgetSettings, doughnutDefaultSettings(this.horizontal));
}
protected onSettingsSet(settings: WidgetSettings) {
@ -101,6 +104,8 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent {
totalValueFont: [settings.totalValueFont, []],
totalValueColor: [settings.totalValueColor, []],
animation: [settings.animation, []],
showLegend: [settings.showLegend, []],
legendPosition: [settings.legendPosition, []],
legendLabelFont: [settings.legendLabelFont, []],
@ -175,9 +180,9 @@ export class DoughnutWidgetSettingsComponent extends WidgetSettingsComponent {
}
private _tooltipValuePreviewFn(): string {
const tooltipValueType: DoughnutTooltipValueType = this.doughnutWidgetSettingsForm.get('tooltipValueType').value;
const tooltipValueType: LatestChartTooltipValueType = this.doughnutWidgetSettingsForm.get('tooltipValueType').value;
const decimals: number = this.doughnutWidgetSettingsForm.get('tooltipValueDecimals').value;
if (tooltipValueType === DoughnutTooltipValueType.percentage) {
if (tooltipValueType === LatestChartTooltipValueType.percentage) {
return formatValue(35, decimals, '%', false);
} else {
const units: string = this.widgetConfig.config.units;

184
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.html

@ -0,0 +1,184 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
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.
-->
<ng-container [formGroup]="pieChartWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.pie-chart.pie-chart-card-style</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="showLabel">
{{ 'widgets.pie-chart.label' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="labelPosition">
<mat-option *ngFor="let position of pieChartLabelPositions" [value]="position">
{{ pieChartLabelPositionTranslationMap.get(position) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-font-settings formControlName="labelFont"
disabledLineHeight
forceSizeUnit="px"
[previewText]="'Wind<br/>55%'">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="labelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.pie-chart.border' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="borderWidth" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
<tb-color-input asBoxInput
colorClearButton
formControlName="borderColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.pie-chart.radius' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="radius" type="number" min="0" max="100" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>%</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="clockwise">
{{ 'widgets.latest-chart.clockwise-layout' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sortSeries">
{{ 'widgets.latest-chart.sort-series' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetSettingsForm.get('showLegend').value" [disabled]="!pieChartWidgetSettingsForm.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div>{{ 'legend.position' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="legendPosition">
<mat-option *ngFor="let pos of legendPositions" [value]="pos">
{{ legendPositionTranslationMap.get(pos) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div>{{ 'legend.label' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendLabelFont"
previewText="Wind power">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendLabelColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'legend.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="legendValueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="legendValueColor">
</tb-color-input>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="pieChartWidgetSettingsForm.get('showTooltip').value" [disabled]="!pieChartWidgetSettingsForm.get('showTooltip').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="showTooltip" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widget-config.tooltip' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between column-xs">
<div>{{ 'tooltip.value' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tooltipValueType">
<mat-option *ngFor="let type of latestChartTooltipValueTypes" [value]="type">
{{ latestChartTooltipValueTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipValueDecimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix fxHide.lt-md translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="tooltipValueFont"
[previewText]="tooltipValuePreviewFn">
</tb-font-settings>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipValueColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-color' | translate }}</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="tooltipBackgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<div>{{ 'tooltip.background-blur' | translate }}</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="tooltipBackgroundBlur" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>px</div>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<tb-echarts-animation-settings
formControlName="animation">
</tb-echarts-animation-settings>
<div class="tb-form-row space-between">
<div>{{ 'widgets.background.background' | translate }}</div>
<tb-background-settings formControlName="background">
</tb-background-settings>
</div>
</div>
</ng-container>

182
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/pie-chart-widget-settings.component.ts

@ -0,0 +1,182 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// 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.
///
import { Component } from '@angular/core';
import {
legendPositions,
legendPositionTranslationMap,
WidgetSettings,
WidgetSettingsComponent
} from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { formatValue, mergeDeep } from '@core/utils';
import {
LatestChartTooltipValueType,
latestChartTooltipValueTypes,
latestChartTooltipValueTypeTranslations
} from '@home/components/widget/lib/chart/latest-chart.models';
import {
pieChartLabelPositions,
pieChartLabelPositionTranslations
} from '@home/components/widget/lib/chart/pie-chart.models';
import {
pieChartWidgetDefaultSettings,
PieChartWidgetSettings
} from '@home/components/widget/lib/chart/pie-chart-widget.models';
@Component({
selector: 'tb-pie-chart-widget-settings',
templateUrl: './pie-chart-widget-settings.component.html',
styleUrls: []
})
export class PieChartWidgetSettingsComponent extends WidgetSettingsComponent {
pieChartLabelPositions = pieChartLabelPositions;
pieChartLabelPositionTranslationMap = pieChartLabelPositionTranslations;
legendPositions = legendPositions;
legendPositionTranslationMap = legendPositionTranslationMap;
latestChartTooltipValueTypes = latestChartTooltipValueTypes;
latestChartTooltipValueTypeTranslationMap = latestChartTooltipValueTypeTranslations;
pieChartWidgetSettingsForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
tooltipValuePreviewFn = this._tooltipValuePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.pieChartWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return mergeDeep<PieChartWidgetSettings>({} as PieChartWidgetSettings, pieChartWidgetDefaultSettings);
}
protected onSettingsSet(settings: WidgetSettings) {
this.pieChartWidgetSettingsForm = this.fb.group({
showLabel: [settings.showLabel, []],
labelPosition: [settings.labelPosition, []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
borderWidth: [settings.borderWidth, [Validators.min(0)]],
borderColor: [settings.borderColor, []],
radius: [settings.radius, [Validators.min(0), Validators.max(100)]],
clockwise: [settings.clockwise, []],
sortSeries: [settings.sortSeries, []],
animation: [settings.animation, []],
showLegend: [settings.showLegend, []],
legendPosition: [settings.legendPosition, []],
legendLabelFont: [settings.legendLabelFont, []],
legendLabelColor: [settings.legendLabelColor, []],
legendValueFont: [settings.legendValueFont, []],
legendValueColor: [settings.legendValueColor, []],
showTooltip: [settings.showTooltip, []],
tooltipValueType: [settings.tooltipValueType, []],
tooltipValueDecimals: [settings.tooltipValueDecimals, []],
tooltipValueFont: [settings.tooltipValueFont, []],
tooltipValueColor: [settings.tooltipValueColor, []],
tooltipBackgroundColor: [settings.tooltipBackgroundColor, []],
tooltipBackgroundBlur: [settings.tooltipBackgroundBlur, []],
background: [settings.background, []]
});
}
protected validatorTriggers(): string[] {
return ['showLabel', 'showLegend', 'showTooltip'];
}
protected updateValidators(emitEvent: boolean) {
const showLabel: boolean = this.pieChartWidgetSettingsForm.get('showLabel').value;
const showLegend: boolean = this.pieChartWidgetSettingsForm.get('showLegend').value;
const showTooltip: boolean = this.pieChartWidgetSettingsForm.get('showTooltip').value;
if (showLabel) {
this.pieChartWidgetSettingsForm.get('labelPosition').enable();
this.pieChartWidgetSettingsForm.get('labelFont').enable();
this.pieChartWidgetSettingsForm.get('labelColor').enable();
} else {
this.pieChartWidgetSettingsForm.get('labelPosition').disable();
this.pieChartWidgetSettingsForm.get('labelFont').disable();
this.pieChartWidgetSettingsForm.get('labelColor').disable();
}
if (showLegend) {
this.pieChartWidgetSettingsForm.get('legendPosition').enable();
this.pieChartWidgetSettingsForm.get('legendLabelFont').enable();
this.pieChartWidgetSettingsForm.get('legendLabelColor').enable();
this.pieChartWidgetSettingsForm.get('legendValueFont').enable();
this.pieChartWidgetSettingsForm.get('legendValueColor').enable();
} else {
this.pieChartWidgetSettingsForm.get('legendPosition').disable();
this.pieChartWidgetSettingsForm.get('legendLabelFont').disable();
this.pieChartWidgetSettingsForm.get('legendLabelColor').disable();
this.pieChartWidgetSettingsForm.get('legendValueFont').disable();
this.pieChartWidgetSettingsForm.get('legendValueColor').disable();
}
if (showTooltip) {
this.pieChartWidgetSettingsForm.get('tooltipValueType').enable();
this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').enable();
this.pieChartWidgetSettingsForm.get('tooltipValueFont').enable();
this.pieChartWidgetSettingsForm.get('tooltipValueColor').enable();
this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').enable();
this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable();
} else {
this.pieChartWidgetSettingsForm.get('tooltipValueType').disable();
this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').disable();
this.pieChartWidgetSettingsForm.get('tooltipValueFont').disable();
this.pieChartWidgetSettingsForm.get('tooltipValueColor').disable();
this.pieChartWidgetSettingsForm.get('tooltipBackgroundColor').disable();
this.pieChartWidgetSettingsForm.get('tooltipBackgroundBlur').disable();
}
}
private _valuePreviewFn(): string {
const units: string = this.widgetConfig.config.units;
const decimals: number = this.widgetConfig.config.decimals;
return formatValue(110, decimals, units, false);
}
private _tooltipValuePreviewFn(): string {
const tooltipValueType: LatestChartTooltipValueType = this.pieChartWidgetSettingsForm.get('tooltipValueType').value;
const decimals: number = this.pieChartWidgetSettingsForm.get('tooltipValueDecimals').value;
if (tooltipValueType === LatestChartTooltipValueType.percentage) {
return formatValue(35, decimals, '%', false);
} else {
const units: string = this.widgetConfig.config.units;
return formatValue(110, decimals, units, false);
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.html

@ -303,9 +303,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html

@ -254,9 +254,9 @@
</ng-template>
</mat-expansion-panel>
</div>
<tb-time-series-chart-animation-settings
<tb-echarts-animation-settings
formControlName="animation">
</tb-time-series-chart-animation-settings>
</tb-echarts-animation-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

20
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.html

@ -23,61 +23,61 @@
<mat-panel-title>
<mat-slide-toggle class="mat-slide" formControlName="animation" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.time-series-chart.animation.animation' | translate }}
{{ 'widgets.echarts.animation.animation' | translate }}
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-threshold' | translate }}</div>
<div>{{ 'widgets.echarts.animation.animation-threshold' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationThreshold" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-duration' | translate }}</div>
<div>{{ 'widgets.echarts.animation.animation-duration' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDuration" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.animation.animation-easing</div>
<div translate>widgets.echarts.animation.animation-easing</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="animationEasing">
<mat-option *ngFor="let easing of timeSeriesChartAnimationEasings" [value]="easing">
<mat-option *ngFor="let easing of echartsAnimationEasings" [value]="easing">
{{ easing }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.animation-delay' | translate }}</div>
<div>{{ 'widgets.echarts.animation.animation-delay' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDelay" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.update-animation-duration' | translate }}</div>
<div>{{ 'widgets.echarts.animation.update-animation-duration' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDurationUpdate" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div translate>widgets.time-series-chart.animation.update-animation-easing</div>
<div translate>widgets.echarts.animation.update-animation-easing</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="animationEasingUpdate">
<mat-option *ngFor="let easing of timeSeriesChartAnimationEasings" [value]="easing">
<mat-option *ngFor="let easing of echartsAnimationEasings" [value]="easing">
{{ easing }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div>{{ 'widgets.time-series-chart.animation.update-animation-delay' | translate }}</div>
<div>{{ 'widgets.echarts.animation.update-animation-delay' | translate }}</div>
<mat-form-field appearance="outline" class="medium-width number" subscriptSizing="dynamic">
<input matInput formControlName="animationDelayUpdate" type="number" min="0" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix>ms</div>

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/echarts-animation-settings.component.ts

@ -22,34 +22,34 @@ import {
UntypedFormGroup,
Validators
} from '@angular/forms';
import {
timeSeriesChartAnimationEasings,
TimeSeriesChartAnimationSettings
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetService } from '@core/http/widget.service';
import {
echartsAnimationEasings,
EChartsAnimationSettings
} from '@home/components/widget/lib/chart/echarts-widget.models';
@Component({
selector: 'tb-time-series-chart-animation-settings',
templateUrl: './time-series-chart-animation-settings.component.html',
selector: 'tb-echarts-animation-settings',
templateUrl: './echarts-animation-settings.component.html',
styleUrls: ['./../../widget-settings.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimeSeriesChartAnimationSettingsComponent),
useExisting: forwardRef(() => EchartsAnimationSettingsComponent),
multi: true
}
]
})
export class TimeSeriesChartAnimationSettingsComponent implements OnInit, ControlValueAccessor {
export class EchartsAnimationSettingsComponent implements OnInit, ControlValueAccessor {
settingsExpanded = false;
timeSeriesChartAnimationEasings = timeSeriesChartAnimationEasings;
echartsAnimationEasings = echartsAnimationEasings;
@Input()
disabled: boolean;
private modelValue: TimeSeriesChartAnimationSettings;
private modelValue: EChartsAnimationSettings;
private propagateChange = null;
@ -96,7 +96,7 @@ export class TimeSeriesChartAnimationSettingsComponent implements OnInit, Contro
}
}
writeValue(value: TimeSeriesChartAnimationSettings): void {
writeValue(value: EChartsAnimationSettings): void {
this.modelValue = value;
this.animationSettingsFormGroup.patchValue(
value, {emitEvent: false}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html

@ -83,7 +83,7 @@
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview">
<div class="fixed-title-width" translate>widgets.widget-font.preview</div>
<div class="preview-text" fxFlex [style]="previewStyle">{{ previewText }}</div>
<div class="preview-text" fxFlex [style]="previewStyle" [innerHTML]="previewText | safe:'html'"></div>
</div>
<div class="tb-font-settings-panel-buttons">
<button *ngIf="clearButton"

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -119,8 +119,8 @@ import {
TimeSeriesChartAxisSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings-panel.component';
import {
TimeSeriesChartAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-animation-settings.component';
EchartsAnimationSettingsComponent
} from '@home/components/widget/lib/settings/common/chart/echarts-animation-settings.component';
import {
AutoDateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/auto-date-format-settings-panel.component';
@ -195,7 +195,7 @@ import {
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent,
EchartsAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent,
TimeSeriesChartStatesPanelComponent,
@ -255,7 +255,7 @@ import {
TimeSeriesChartYAxisRowComponent,
TimeSeriesChartAxisSettingsPanelComponent,
TimeSeriesChartAxisSettingsButtonComponent,
TimeSeriesChartAnimationSettingsComponent,
EchartsAnimationSettingsComponent,
TimeSeriesChartFillSettingsComponent,
TimeSeriesChartThresholdSettingsComponent,
TimeSeriesChartStatesPanelComponent,

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -345,6 +345,9 @@ import {
import {
StatusWidgetSettingsComponent
} from '@home/components/widget/lib/settings/indicator/status-widget-settings.component';
import {
PieChartWidgetSettingsComponent
} from '@home/components/widget/lib/settings/chart/pie-chart-widget-settings.component';
@NgModule({
declarations: [
@ -468,7 +471,8 @@ import {
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
StatusWidgetSettingsComponent,
PieChartWidgetSettingsComponent
],
imports: [
CommonModule,
@ -597,7 +601,8 @@ import {
TimeSeriesChartLineSettingsComponent,
TimeSeriesChartBarSettingsComponent,
TimeSeriesChartWidgetSettingsComponent,
StatusWidgetSettingsComponent
StatusWidgetSettingsComponent,
PieChartWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
@ -691,5 +696,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-toggle-button-widget-settings': ToggleButtonWidgetSettingsComponent,
'tb-time-series-chart-key-settings': TimeSeriesChartKeySettingsComponent,
'tb-time-series-chart-widget-settings': TimeSeriesChartWidgetSettingsComponent,
'tb-status-widget-settings': StatusWidgetSettingsComponent
'tb-status-widget-settings': StatusWidgetSettingsComponent,
'tb-pie-chart-widget-settings': PieChartWidgetSettingsComponent
};

9
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -87,6 +87,8 @@ import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-wi
import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component';
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@NgModule({
declarations:
@ -140,7 +142,9 @@ import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/sta
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
StatusWidgetComponent,
LatestChartComponent,
PieChartWidgetComponent
],
imports: [
CommonModule,
@ -198,7 +202,8 @@ import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/sta
SliderWidgetComponent,
ToggleButtonWidgetComponent,
TimeSeriesChartWidgetComponent,
StatusWidgetComponent
StatusWidgetComponent,
PieChartWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

4
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss

@ -17,10 +17,10 @@
z-index: 10;
background: #F3F6FA;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
.tb-preview-dashboard {
position: relative;
margin: auto;
}
.tb-preview-panel {
position: absolute;

12
ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md

@ -39,8 +39,16 @@ An optional key/value object holding additional entity parameters depending on w
containing <code>entity</code> field holding basic entity properties <br> (ex. <code>id</code>, <code>name</code>, <code>label</code>) and <code>data</code> field holding other entity attributes/timeseries declared in widget datasource configuration.
</li>
</ul>
</li>
<li>Pie - Flot widget (<i>On slice click</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code>:
</li>
<li>Pie (<i>On slice click</i>) - <b>additionalParams:</b> <code>LatestChartDataItem</code>:
<ul>
<li><b>additionalParams:</b> <code>LatestChartDataItem</code> - A
data object of clicked pie slice
containing <code>dataKey</code>, <code>datasource</code> and <code>value</code> fields.
</li>
</ul>
</li>
<li>Pie - Flot widget (deprecated) (<i>On slice click</i>) - <b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code>:
<ul>
<li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a></code> - A
<a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts#L62" target="_blank">TbFlotPlotItem</a> object

47
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -805,7 +805,6 @@
"email-messages": "Email messages",
"email-messages-daily-activity": "Email messages daily activity",
"email-messages-monthly-activity": "Email messages monthly activity",
"exceptions": "Exceptions",
"executions": "Executions",
"scripts": "Scripts",
"scripts-hourly-activity": "Scripts hourly activity",
@ -823,9 +822,7 @@
"permanent-failures": "${entityName} Permanent Failures",
"permanent-timeouts": "${entityName} Permanent Timeouts",
"processing-failures": "${entityName} Processing Failures",
"processing-failures-and-timeouts": "Processing Failures and Timeouts",
"processing-timeouts": "${entityName} Processing Timeouts",
"queue-stats": "Queue Stats",
"rule-chain": "Rule Chain",
"rule-engine": "Rule Engine",
"rule-engine-daily-activity": "Rule Engine daily activity",
@ -5712,16 +5709,10 @@
}
},
"doughnut": {
"total": "Total",
"layout": "Layout",
"layout-default": "Default",
"layout-with-total": "With total",
"auto-scale": "Auto scale",
"clockwise-layout": "Clockwise layout",
"sort-series": "Sort series by label",
"central-total-value": "Central total value",
"tooltip-value-type-absolute": "Absolute",
"tooltip-value-type-percentage": "Percentage",
"doughnut-card-style": "Doughnut card style"
},
"entities-hierarchy": {
@ -6738,6 +6729,34 @@
"table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
},
"echarts": {
"animation": {
"animation": "Animation",
"animation-threshold": "Animation threshold",
"animation-duration": "Animation duration",
"animation-easing": "Animation easing",
"animation-delay": "Animation delay",
"update-animation-duration": "Update animation duration",
"update-animation-easing": "Update animation easing",
"update-animation-delay": "Update animation delay"
}
},
"latest-chart": {
"total": "Total",
"auto-scale": "Auto scale",
"clockwise-layout": "Clockwise layout",
"sort-series": "Sort series by label",
"tooltip-value-type-absolute": "Absolute",
"tooltip-value-type-percentage": "Percentage"
},
"pie-chart": {
"label": "Label",
"label-position-outside": "Outside",
"label-position-inside": "Inside",
"border": "Border",
"radius": "Radius",
"pie-chart-card-style": "Pie chart card style"
},
"time-series-chart": {
"chart": "Chart",
"chart-style": "Chart style",
@ -6867,16 +6886,6 @@
"show": "Show",
"add-y-axis": "Add Y axis"
},
"animation": {
"animation": "Animation",
"animation-threshold": "Animation threshold",
"animation-duration": "Animation duration",
"animation-easing": "Animation easing",
"animation-delay": "Animation delay",
"update-animation-duration": "Update animation duration",
"update-animation-easing": "Update animation easing",
"update-animation-delay": "Update animation delay"
},
"series": {
"legend-settings": "Legend settings",
"show-in-legend": "Show in legend",

Loading…
Cancel
Save