diff --git a/frontend/__snapshots__/components-activitylog--team-activity--dark.png b/frontend/__snapshots__/components-activitylog--team-activity--dark.png index 362dfc6c870a1..00382659acb05 100644 Binary files a/frontend/__snapshots__/components-activitylog--team-activity--dark.png and b/frontend/__snapshots__/components-activitylog--team-activity--dark.png differ diff --git a/frontend/__snapshots__/components-activitylog--team-activity--light.png b/frontend/__snapshots__/components-activitylog--team-activity--light.png index 51cf91b20f34e..3d3a41f84e29c 100644 Binary files a/frontend/__snapshots__/components-activitylog--team-activity--light.png and b/frontend/__snapshots__/components-activitylog--team-activity--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--dark.png b/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--dark.png index 85141c3acd31f..eaa16a3d84a0c 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--dark.png and b/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--light.png b/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--light.png index 14f2427aae416..da0cdd1dca947 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--light.png and b/frontend/__snapshots__/exporter-exporter--trends-pie-insight-detailed--light.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--dark.png b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--dark.png index b785192fbb0b9..7dc6460889867 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--dark.png and b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--dark.png differ diff --git a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--light.png b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--light.png index 429addfbd2d91..287eba4ee2c89 100644 Binary files a/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--light.png and b/frontend/__snapshots__/exporter-exporter--trends-table-breakdown-insight--light.png differ diff --git a/frontend/__snapshots__/lemon-ui-colors--data-colors--dark.png b/frontend/__snapshots__/lemon-ui-colors--data-colors--dark.png new file mode 100644 index 0000000000000..e5ad4abf9efdf Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-colors--data-colors--dark.png differ diff --git a/frontend/__snapshots__/lemon-ui-colors--data-colors--light.png b/frontend/__snapshots__/lemon-ui-colors--data-colors--light.png new file mode 100644 index 0000000000000..4ea608850368c Binary files /dev/null and b/frontend/__snapshots__/lemon-ui-colors--data-colors--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark--webkit.png index 9938a4afd5874..baee2dd4ae867 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark.png index 30f004e4623ed..c529c149c3376 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light--webkit.png index c3d1ee8e0ad57..a1bca2a3e0c97 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light.png index 1732b30bb1d9f..4229a414d142f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png index 09f199af011b5..338ec98b6fdfe 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--dark--webkit.png index 141e36f0af8ec..36a29e662f5c7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light--webkit.png index 6da267526f835..76a01fe605536 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light.png index 4e4b6398651eb..bfdf49ff0f4de 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png index 5b0c6d5548a97..38079a84ab141 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png index 247c8ccdce428..a25536f92dc47 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png index 9e78fbe1f727f..b68f58dc70b9f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png index b408a7319e1ef..5df19ba32bc94 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png index 3e1d1c8125ef1..f3cf7dd1c6362 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png index 97482f07d4847..d774e7fae38aa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png index d5db46dade2d2..1954186a26b34 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png index d8f63b5908506..1df5d100e0b6a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-area-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--light.png index 16b2cb99c94fb..273fb7182e412 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png index 7e0d3cf1de216..bb1469cf068e5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png index 7977f47baba7f..d5791cfeb02dd 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png index 5769e1d4a655d..efaa89e5bd8ed 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png index 7aef465d2f20e..e971424788093 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png index 06f5050b99a93..cca61adf98edb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png index 77427f0fe4d16..7c484ba551581 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png index b9426649372fa..96362ebd92930 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png index 08aca02dbb3c0..e8f2b2c3bbc0c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-bar-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--light.png index fa6299893325a..54c918a0a8e04 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png index b2f067a3af7f5..4e94942bf7b8a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png index 76ccee1ca9fae..05c4952d31ac5 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png index 3122aca379040..5e09b1c9a9af1 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png index dd7c387f1cd33..4d7a76498cf11 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--light.png index 6b12f72e1b8f3..0b08453aed4bf 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-breakdown-labels--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png index 8193f6628caff..0fda9eae01ccb 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png index 0cd89979f05a3..b089edc6ef5af 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png index efec75e215c14..1024a4957f738 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png index aea9a0d2f6710..1ced3f7f771f2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png index 9a07c83b5593f..57b0cd71e62df 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png index f8a7e8cf7494b..447554cd2abd0 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png index ca667e26b78d5..96db71aaca5a9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png index 15317ce31345c..8c30252d0613c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-line-multi-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png index 2170c9b34c499..b9318676d3144 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png index 6472eaf4b9e13..a3eef3b8b5d9e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png index 31ac8b1aae483..efc30857681ce 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png index 0d6738590a3be..9ca8b4ad81552 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-number-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--light.png index 7eb5fa2182c1b..7293eaa7bc9d9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png index 47abab0310e7e..9832d8dcaa483 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png index b7ec82f7ebc85..eff2210c2f0f6 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png index c6828a749cdc2..4cccf04f6919f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png index 448a7c8c654a0..1257ed925947a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--light.png index 156110f23ffc6..ac05109c8546b 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-breakdown-labels--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png index 556181bc47f7a..5cf6a9427c5c7 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png index 55eceaaf2d771..4342983127520 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png index 3602c5079161e..55b1af792d563 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png index 12d4b9a1f3698..4b38354943353 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-pie-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png index 128ea48921a6b..7284653babac9 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png index 3cf953528bc10..f72be764b975a 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png index 4d0bb102313ec..51ab468f41854 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png index c365861a7bbd0..a871abe1ac52e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-breakdown-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png index 5497a01b1b67a..1b387c32fb142 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png index f83b5f82b7470..ad23d22196b4d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png index cc2a286c9b3b4..dae2390d0c5ce 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png index 20ba355a9de20..c34f3dc0fe377 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-table-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png index 4eef1e282f398..7ffb78de60c83 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png index ba56d7d783fca..1d3486d6108a2 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png index 1d6b2ff7fd709..cb59ce921686d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png index b85303f635744..98cfcc0a97614 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--trends-world-map-edit--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths--light--webkit.png index d4a68dba3c11f..416a7288841b8 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--user-paths--light--webkit.png and b/frontend/__snapshots__/scenes-app-insights--user-paths--light--webkit.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png index de098d4ce843e..05e473e182395 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png index de9f65b23f02c..735c420a3f20a 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-all-options--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-password-only--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-github--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-google--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-enforced-saml--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png index 21592267c1f1c..e11ac5731e92d 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png index 6b749977be35f..d6d3e0c1b0320 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-session-timeout-sso-only--light.png differ diff --git a/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx b/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx index 0a287abee317f..02211219727c9 100644 --- a/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx +++ b/frontend/src/exporter/ExportedInsight/ExportedInsight.tsx @@ -1,7 +1,7 @@ import './ExportedInsight.scss' import clsx from 'clsx' -import { BindLogic } from 'kea' +import { BindLogic, useMountedLogic } from 'kea' import { TopHeading } from 'lib/components/Cards/InsightCard/TopHeading' import { InsightLegend } from 'lib/components/InsightLegend/InsightLegend' import { @@ -9,6 +9,7 @@ import { DISPLAY_TYPES_WITHOUT_LEGEND, } from 'lib/components/InsightLegend/utils' import { SINGLE_SERIES_DISPLAY_TYPES } from 'lib/constants' +import { dataThemeLogic } from 'scenes/dataThemeLogic' import { insightLogic } from 'scenes/insights/insightLogic' import { InsightsTable } from 'scenes/insights/views/InsightsTable/InsightsTable' @@ -17,15 +18,19 @@ import { getQueryBasedInsightModel } from '~/queries/nodes/InsightViz/utils' import { Query } from '~/queries/Query/Query' import { isDataTableNode, isInsightVizNode, isTrendsQuery } from '~/queries/utils' import { Logo } from '~/toolbar/assets/Logo' -import { ChartDisplayType, InsightLogicProps, InsightModel } from '~/types' +import { ChartDisplayType, DataColorThemeModel, InsightLogicProps, InsightModel } from '~/types' export function ExportedInsight({ insight: legacyInsight, + themes, exportOptions: { whitelabel, noHeader, legend, detailed: detailedResults }, }: { insight: InsightModel + themes: DataColorThemeModel[] exportOptions: ExportOptions }): JSX.Element { + useMountedLogic(dataThemeLogic({ themes })) + const insight = getQueryBasedInsightModel(legacyInsight) if ( diff --git a/frontend/src/exporter/Exporter.tsx b/frontend/src/exporter/Exporter.tsx index 0b152399ec916..20810f1e8f190 100644 --- a/frontend/src/exporter/Exporter.tsx +++ b/frontend/src/exporter/Exporter.tsx @@ -23,7 +23,7 @@ import { exporterViewLogic } from './exporterViewLogic' export function Exporter(props: ExportedData): JSX.Element { // NOTE: Mounting the logic is important as it is used by sub-logics const { exportedData } = useValues(exporterViewLogic(props)) - const { type, dashboard, insight, recording, accessToken, ...exportOptions } = exportedData + const { type, dashboard, insight, recording, themes, accessToken, ...exportOptions } = exportedData const { whitelabel, showInspector = false } = exportOptions const { currentTeam } = useValues(teamLogic) @@ -75,12 +75,13 @@ export function Exporter(props: ExportedData): JSX.Element { ) : null ) : null} {insight ? ( - + ) : dashboard ? ( ) : recording ? ( { return await api.delete(this.assembleFullUrl()) } + + // Data color themes + public dataColorThemes(teamId?: TeamType['id']): ApiRequest { + return this.environmentsDetail(teamId).addPathComponent('data_color_themes') + } + + public dataColorTheme(id: DataColorThemeModel['id'], teamId?: TeamType['id']): ApiRequest { + return this.environmentsDetail(teamId).addPathComponent('data_color_themes').addPathComponent(id) + } } const normalizeUrl = (url: string): string => { @@ -2520,6 +2530,18 @@ const api = { }, }, + dataColorThemes: { + async list(): Promise { + return await new ApiRequest().dataColorThemes().get() + }, + async create(data: Partial): Promise { + return await new ApiRequest().dataColorThemes().create({ data }) + }, + async update(id: DataColorThemeModel['id'], data: Partial): Promise { + return await new ApiRequest().dataColorTheme(id).update({ data }) + }, + }, + queryURL: (): string => { return new ApiRequest().query().assembleFullUrl(true) }, diff --git a/frontend/src/lib/colors.ts b/frontend/src/lib/colors.ts index 9646b6dbc953d..6965ba1342cda 100644 --- a/frontend/src/lib/colors.ts +++ b/frontend/src/lib/colors.ts @@ -4,39 +4,49 @@ import { LifecycleToggle } from '~/types' import { LemonTagType } from './lemon-ui/LemonTag' -/** --brand-blue in HSL for saturation mixing */ -export const BRAND_BLUE_HSL: [number, number, number] = [228, 100, 56] -export const PURPLE: [number, number, number] = [260, 88, 71] +/* + * Data colors. + */ -/* Insight series colors. */ +/** CSS variable names for the default posthog theme data colors. */ const dataColorVars = [ - 'color-1', - 'color-2', - 'color-3', - 'color-4', - 'color-5', - 'color-6', - 'color-7', - 'color-8', - 'color-9', - 'color-10', - 'color-11', - 'color-12', - 'color-13', - 'color-14', - 'color-15', -] + 'data-color-1', + 'data-color-2', + 'data-color-3', + 'data-color-4', + 'data-color-5', + 'data-color-6', + 'data-color-7', + 'data-color-8', + 'data-color-9', + 'data-color-10', + 'data-color-11', + 'data-color-12', + 'data-color-13', + 'data-color-14', + 'data-color-15', +] as const -export const tagColors: LemonTagType[] = [ - 'primary', - 'highlight', - 'warning', - 'danger', - 'success', - 'completion', - 'caution', - 'option', -] +export type DataColorToken = + | 'preset-1' + | 'preset-2' + | 'preset-3' + | 'preset-4' + | 'preset-5' + | 'preset-6' + | 'preset-7' + | 'preset-8' + | 'preset-9' + | 'preset-10' + | 'preset-11' + | 'preset-12' + | 'preset-13' + | 'preset-14' + | 'preset-15' + +export type DataColorTheme = Partial> & { + [key: `preset-${number}`]: string +} export function getColorVar(variable: string): string { const colorValue = getComputedStyle(document.body).getPropertyValue('--' + variable) @@ -48,6 +58,10 @@ export function getColorVar(variable: string): string { return colorValue.trim() } +export function getDataThemeColor(theme: DataColorTheme, color: DataColorToken): string { + return theme[color] as string +} + /** Returns the color for the given series index. * * The returned colors are in hex format for compatibility with Chart.js. They repeat @@ -57,12 +71,12 @@ export function getColorVar(variable: string): string { */ export function getSeriesColor(index: number = 0): string { const adjustedIndex = index % dataColorVars.length - return getColorVar(`data-${dataColorVars[adjustedIndex]}`) + return getColorVar(dataColorVars[adjustedIndex]) } /** Returns all color options for series */ export function getSeriesColorPalette(): string[] { - return dataColorVars.map((colorVar) => getColorVar(`data-${colorVar}`)) + return dataColorVars.map((colorVar) => getColorVar(colorVar)) } /** Return the background color for the given series index. */ @@ -104,18 +118,17 @@ export function getGraphColors(isDarkModeOn: boolean): Record
{indexedResults && - indexedResults.map((item, index) => ( - - ))} + indexedResults.map((item, index) => )}
) : null diff --git a/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx b/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx index 692125aef9267..09ba6a58507e5 100644 --- a/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx +++ b/frontend/src/lib/components/InsightLegend/InsightLegendRow.tsx @@ -1,5 +1,5 @@ import { useActions, useValues } from 'kea' -import { getSeriesBackgroundColor, getTrendLikeSeriesColor } from 'lib/colors' +import { getSeriesBackgroundColor } from 'lib/colors' import { InsightLabel } from 'lib/components/InsightLabel' import { LemonCheckbox } from 'lib/lemon-ui/LemonCheckbox' import { useEffect, useRef } from 'react' @@ -19,15 +19,14 @@ import { shouldHighlightThisRow } from './utils' type InsightLegendRowProps = { rowIndex: number item: IndexedTrendResult - totalItems: number } -export function InsightLegendRow({ rowIndex, item, totalItems }: InsightLegendRowProps): JSX.Element { +export function InsightLegendRow({ rowIndex, item }: InsightLegendRowProps): JSX.Element { const { cohorts } = useValues(cohortsModel) const { formatPropertyValueForDisplay } = useValues(propertyDefinitionsModel) const { insightProps, highlightedSeries } = useValues(insightLogic) - const { display, trendsFilter, breakdownFilter, isSingleSeries, hiddenLegendIndexes } = useValues( + const { display, trendsFilter, breakdownFilter, isSingleSeries, hiddenLegendIndexes, getTrendsColor } = useValues( trendsDataLogic(insightProps) ) const { toggleHiddenLegendIndex } = useActions(trendsDataLogic(insightProps)) @@ -54,21 +53,23 @@ export function InsightLegendRow({ rowIndex, item, totalItems }: InsightLegendRo ) const isPrevious = !!item.compare && item.compare_label === 'previous' - const adjustedIndex = isPrevious ? item.seriesIndex - totalItems / 2 : item.seriesIndex + + const themeColor = getTrendsColor(item) + const mainColor = isPrevious ? `${themeColor}80` : themeColor return (
toggleHiddenLegendIndex(rowIndex)} fullWidth label={ ('#000000') + + useEffect(() => { + // allow only 6-digit hex colors + // other color formats are not supported everywhere e.g. insight visualizations + if (color != null && /^#[0-9A-Fa-f]{6}$/.test(color)) { + setLastValidColor(color) + } + }, [color]) + + return ( + + ) +} + interface SeriesLetterProps { className?: string hasBreakdown: boolean diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index a521039c23313..d7422e52447e5 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -236,6 +236,7 @@ export const FEATURE_FLAGS = { WEB_ANALYTICS_CONVERSION_GOAL_FILTERS: 'web-analytics-conversion-goal-filters', // owner: @rafaeelaudibert #team-web-analytics CDP_ACTIVITY_LOG_NOTIFICATIONS: 'cdp-activity-log-notifications', // owner: #team-cdp COOKIELESS_SERVER_HASH_MODE_SETTING: 'cookieless-server-hash-mode-setting', // owner: @robbie-c #team-web-analytics + INSIGHT_COLORS: 'insight-colors', // owner @thmsobrmlr #team-product-analytics } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx index 4f9256e1569c9..9cfa959fc8e9b 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx @@ -41,7 +41,7 @@ export interface LemonTableProps> { /** Class to append to each row. */ rowClassName?: string | ((record: T, rowIndex: number) => string | null) /** Color to mark each row with. */ - rowRibbonColor?: string | ((record: T, rowIndex: number) => string | null) + rowRibbonColor?: string | ((record: T, rowIndex: number) => string | null | undefined) /** Status of each row. Defaults no status. */ rowStatus?: 'highlighted' | ((record: T, rowIndex: number) => 'highlighted' | null) /** Function that for each row determines what props should its `tr` element have based on the row's record. */ diff --git a/frontend/src/lib/lemon-ui/colors.stories.tsx b/frontend/src/lib/lemon-ui/colors.stories.tsx index f2e87c73528af..1a578ea7c0029 100644 --- a/frontend/src/lib/lemon-ui/colors.stories.tsx +++ b/frontend/src/lib/lemon-ui/colors.stories.tsx @@ -182,6 +182,24 @@ const threeThousand = [ 'primary-alt', ] +const dataColors = [ + 'data-color-1', + 'data-color-2', + 'data-color-3', + 'data-color-4', + 'data-color-5', + 'data-color-6', + 'data-color-7', + 'data-color-8', + 'data-color-9', + 'data-color-10', + 'data-color-11', + 'data-color-12', + 'data-color-13', + 'data-color-14', + 'data-color-15', +] + export function ColorPalette(): JSX.Element { const [hover, setHover] = useState() return ( @@ -278,3 +296,53 @@ export function AllThreeThousandColorOptions(): JSX.Element { /> ) } + +export function DataColors(): JSX.Element { + return ( + ({ name: color, color }))} + columns={[ + { + title: 'Class name', + key: 'name', + dataIndex: 'name', + render: function RenderName(name) { + return name + }, + }, + { + title: 'Light mode', + key: 'light', + dataIndex: 'color', + render: function RenderColor(color) { + return ( +
+
+
+ ) + }, + }, + { + title: 'Dark mode', + key: 'dark', + dataIndex: 'color', + render: function RenderColor(color) { + return ( +
+
+
+ ) + }, + }, + ]} + /> + ) +} diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 15b90eeb5473a..d8a66b558ac15 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1452,11 +1452,20 @@ export function resolveWebhookService(webhookUrl: string): string { return 'your webhook service' } -function hexToRGB(hex: string): { r: number; g: number; b: number } { +export function hexToRGB(hex: string): { r: number; g: number; b: number } { const originalString = hex.trim() const hasPoundSign = originalString[0] === '#' - const originalColor = hasPoundSign ? originalString.slice(1) : originalString + let originalColor = hasPoundSign ? originalString.slice(1) : originalString + // convert 3-digit hex colors to 6-digit + if (originalColor.length === 3) { + originalColor = originalColor + .split('') + .map((c) => c + c) + .join('') + } + + // make sure we have a 6-digit color if (originalColor.length !== 6) { console.warn(`Incorrectly formatted color string: ${hex}.`) return { r: 0, g: 0, b: 0 } @@ -1513,6 +1522,26 @@ export function lightenDarkenColor(hex: string, pct: number): string { return `rgb(${[r, g, b].join(',')})` } +/* Colors in hsl for gradation. */ +export const BRAND_BLUE_HSL: [number, number, number] = [228, 100, 56] +export const PURPLE: [number, number, number] = [260, 88, 71] + +/** + * Gradate color saturation based on its intended strength. + * This is for visualizations where a data point's color depends on its value. + * @param hsl The HSL color to gradate. + * @param strength The strength of the data point. + * @param floor The minimum saturation. This preserves proportionality of strength, so doesn't just cut it off. + */ +export function gradateColor( + hsl: [number, number, number], + strength: number, + floor: number = 0 +): `hsla(${number}, ${number}%, ${number}%, ${string})` { + const saturation = floor + (1 - floor) * strength + return `hsla(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%, ${saturation.toPrecision(3)})` +} + export function toString(input?: any): string { return input?.toString() || '' } diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index de9e072e7f69b..b9963ac3fa5ad 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -1,4 +1,5 @@ import { + MOCK_DATA_COLOR_THEMES, MOCK_DEFAULT_COHORT, MOCK_DEFAULT_ORGANIZATION, MOCK_DEFAULT_ORGANIZATION_INVITE, @@ -153,6 +154,7 @@ export const defaultMocks: Mocks = { '/api/projects/:team_id/hog_function_templates': _hogFunctionTemplates, '/api/projects/:team_id/hog_function_templates/:id': hogFunctionTemplateRetrieveMock, '/api/projects/:team_id/hog_functions': EMPTY_PAGINATED_RESPONSE, + '/api/environments/:team_id/data_color_themes': MOCK_DATA_COLOR_THEMES, }, post: { 'https://us.i.posthog.com/e/': (req, res, ctx): MockSignature => posthogCORSResponse(req, res, ctx), diff --git a/frontend/src/queries/nodes/DataVisualization/Components/ColorPickerButton.tsx b/frontend/src/queries/nodes/DataVisualization/Components/ColorPickerButton.tsx index a9fdac3aedf91..107f9d426d83e 100644 --- a/frontend/src/queries/nodes/DataVisualization/Components/ColorPickerButton.tsx +++ b/frontend/src/queries/nodes/DataVisualization/Components/ColorPickerButton.tsx @@ -1,7 +1,7 @@ import { LemonButton, Popover } from '@posthog/lemon-ui' import { useValues } from 'kea' -import { SeriesGlyph } from 'lib/components/SeriesGlyph' -import { hexToRGBA, lightenDarkenColor, RGBToHex, RGBToRGBA } from 'lib/utils' +import { ColorGlyph } from 'lib/components/SeriesGlyph' +import { lightenDarkenColor, RGBToHex } from 'lib/utils' import { useState } from 'react' import { ColorResult, TwitterPicker } from 'react-color' @@ -57,17 +57,7 @@ export const ColorPickerButton = ({ sideIcon={<>} className="ConditionalFormattingTab__ColorPicker" > - - <> - + ) diff --git a/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/ConditionalFormattingTab.tsx b/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/ConditionalFormattingTab.tsx index 30cd92628f817..5f94978e5afb1 100644 --- a/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/ConditionalFormattingTab.tsx +++ b/frontend/src/queries/nodes/DataVisualization/Components/ConditionalFormatting/ConditionalFormattingTab.tsx @@ -3,10 +3,8 @@ import './ConditionalFormattingTab.scss' import { IconPlusSmall, IconTrash } from '@posthog/icons' import { LemonButton, LemonCollapse, LemonInput, LemonSelect, LemonTag } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { SeriesGlyph } from 'lib/components/SeriesGlyph' -import { hexToRGBA, lightenDarkenColor, RGBToRGBA } from 'lib/utils' +import { ColorGlyph } from 'lib/components/SeriesGlyph' -import { themeLogic } from '~/layout/navigation-3000/themeLogic' import { ColorPickerButton } from '~/queries/nodes/DataVisualization/Components/ColorPickerButton' import { ConditionalFormattingRule } from '~/queries/schema' @@ -33,7 +31,6 @@ const getRuleHeader = (rule: ConditionalFormattingRule): string => { } export const ConditionalFormattingTab = (): JSX.Element => { - const { isDarkModeOn } = useValues(themeLogic) const { conditionalFormattingRules, conditionalFormattingRulesPanelActiveKeys } = useValues(dataVisualizationLogic) const { addConditionalFormattingRule, setConditionalFormattingRulesPanelActiveKeys } = useActions(dataVisualizationLogic) @@ -53,17 +50,7 @@ export const ConditionalFormattingTab = (): JSX.Element => { key: rule.id, header: ( <> - - <> - + {getRuleHeader(rule)} ), diff --git a/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx b/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx index d500bdf9e2f00..4c1db2454dd1f 100644 --- a/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx +++ b/frontend/src/queries/nodes/DataVisualization/Components/SeriesTab.tsx @@ -13,11 +13,8 @@ import { import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' import { getSeriesColor, getSeriesColorPalette } from 'lib/colors' -import { SeriesGlyph } from 'lib/components/SeriesGlyph' +import { ColorGlyph } from 'lib/components/SeriesGlyph' import { LemonField } from 'lib/lemon-ui/LemonField' -import { hexToRGBA, lightenDarkenColor, RGBToRGBA } from 'lib/utils' - -import { themeLogic } from '~/layout/navigation-3000/themeLogic' import { AxisSeries, dataVisualizationLogic } from '../dataVisualizationLogic' import { ColorPickerButton } from './ColorPickerButton' @@ -126,7 +123,6 @@ const YSeries = ({ series, index }: { series: AxisSeries; index: number const { isSettingsOpen, canOpenSettings, activeSettingsTab } = useValues(seriesLogic) const { setSettingsOpen, submitFormatting, submitDisplay, setSettingsTab } = useActions(seriesLogic) - const { isDarkModeOn } = useValues(themeLogic) const seriesColor = series.settings?.display?.color ?? getSeriesColor(index) const showSeriesColor = !showTableSettings && !selectedSeriesBreakdownColumn @@ -135,20 +131,7 @@ const YSeries = ({ series, index }: { series: AxisSeries; index: number value: name, label: (
- {showSeriesColor && ( - - <> - - )} + {showSeriesColor && } {series.settings?.display?.label && series.column.name === name ? series.settings.display.label : name} {type.name} @@ -399,24 +382,12 @@ export const SeriesBreakdownSelector = (): JSX.Element => { } const BreakdownSeries = ({ series, index }: { series: AxisBreakdownSeries; index: number }): JSX.Element => { - const { isDarkModeOn } = useValues(themeLogic) const seriesColor = series.settings?.display?.color ?? getSeriesColor(index) return (
- - <> - + {series.name ? series.name : '[No value]'}
{/* For now let's keep things simple and not allow too much configuration */} diff --git a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx index ab8b30abc4f5e..67e7f9e7d8dc4 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightDisplayConfig.tsx @@ -1,4 +1,5 @@ -import { LemonButton, LemonInput } from '@posthog/lemon-ui' +import { IconInfo } from '@posthog/icons' +import { LemonButton, LemonInput, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { ChartFilter } from 'lib/components/ChartFilter' import { CompareFilter } from 'lib/components/CompareFilter/CompareFilter' @@ -13,6 +14,7 @@ import { ReactNode } from 'react' import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' import { axisLabel } from 'scenes/insights/aggregationAxisFormat' import { PercentStackViewFilter } from 'scenes/insights/EditorFilters/PercentStackViewFilter' +import { ResultCustomizationByPicker } from 'scenes/insights/EditorFilters/ResultCustomizationByPicker' import { ScalePicker } from 'scenes/insights/EditorFilters/ScalePicker' import { ShowAlertThresholdLinesFilter } from 'scenes/insights/EditorFilters/ShowAlertThresholdLinesFilter' import { ShowLegendFilter } from 'scenes/insights/EditorFilters/ShowLegendFilter' @@ -30,6 +32,7 @@ import { PathStepPicker } from 'scenes/insights/views/Paths/PathStepPicker' import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' import { useDebouncedCallback } from 'use-debounce' +import { resultCustomizationsModalLogic } from '~/queries/nodes/InsightViz/resultCustomizationsModalLogic' import { isValidBreakdown } from '~/queries/utils' import { ChartDisplayType } from '~/types' @@ -52,6 +55,7 @@ export function InsightDisplayConfig(): JSX.Element { supportsValueOnSeries, showPercentStackView, supportsPercentStackView, + supportsResultCustomizationBy, yAxisScaleType, isNonTimeSeriesDisplay, compareFilter, @@ -60,6 +64,7 @@ export function InsightDisplayConfig(): JSX.Element { const { isTrendsFunnel, isStepsFunnel, isTimeToConvertFunnel, isEmptyFunnel } = useValues( funnelDataLogic(insightProps) ) + const { hasInsightColors } = useValues(resultCustomizationsModalLogic(insightProps)) const { updateCompareFilter } = useActions(insightVizDataLogic(insightProps)) @@ -74,7 +79,7 @@ export function InsightDisplayConfig(): JSX.Element { const { showValuesOnSeries, mightContainFractionalNumbers } = useValues(trendsDataLogic(insightProps)) const advancedOptions: LemonMenuItems = [ - ...(supportsValueOnSeries || supportsPercentStackView || hasLegend + ...(supportsValueOnSeries || supportsPercentStackView || hasLegend || supportsResultCustomizationBy ? [ { title: 'Display', @@ -87,6 +92,23 @@ export function InsightDisplayConfig(): JSX.Element { }, ] : []), + ...(supportsResultCustomizationBy && hasInsightColors + ? [ + { + title: ( + <> +
+ Color customization by{' '} + + + +
+ + ), + items: [{ label: () => }], + }, + ] + : []), ...(!showPercentStackView && isTrends ? [ { diff --git a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx index 69a30e9e727f8..6f6531a3669ac 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx +++ b/frontend/src/queries/nodes/InsightViz/InsightVizDisplay.tsx @@ -32,6 +32,7 @@ import { ExporterFormat, FunnelVizType, InsightType, ItemMode } from '~/types' import { InsightDisplayConfig } from './InsightDisplayConfig' import { InsightResultMetadata } from './InsightResultMetadata' +import { ResultCustomizationsModal } from './ResultCustomizationsModal' export function InsightVizDisplay({ disableHeader, @@ -55,13 +56,13 @@ export function InsightVizDisplay({ context?: QueryContext embedded: boolean inSharedMode?: boolean -}): JSX.Element { +}): JSX.Element | null { const { insightProps, canEditInsight } = useValues(insightLogic) const { activeView } = useValues(insightNavLogic(insightProps)) const { hasFunnelResults } = useValues(funnelDataLogic(insightProps)) - const { isFunnelWithEnoughSteps, validationError } = useValues(insightVizDataLogic(insightProps)) + const { isFunnelWithEnoughSteps, validationError, theme } = useValues(insightVizDataLogic(insightProps)) const { isFunnels, isPaths, @@ -218,6 +219,10 @@ export function InsightVizDisplay({ const showComputationMetadata = !disableLastComputation || !!samplingFactor + if (!theme) { + return null + } + return ( <> {/* These are filters that are reused between insight features. They each have generic logic that updates the url */} @@ -265,12 +270,13 @@ export function InsightVizDisplay({
) : ( - renderActiveView() + <>{renderActiveView()} )}
)}
+ {renderTable()} {!disableCorrelationTable && activeView === InsightType.FUNNELS && } diff --git a/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.scss b/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.scss new file mode 100644 index 0000000000000..5169495b7ffb4 --- /dev/null +++ b/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.scss @@ -0,0 +1,5 @@ +.ResultCustomizationsModal__ColorGlyphButton .LemonButton__chrome { + gap: 4px; + padding-right: 6px !important; + padding-left: 6px !important; +} diff --git a/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.tsx b/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.tsx new file mode 100644 index 0000000000000..9d1bed9f6faaf --- /dev/null +++ b/frontend/src/queries/nodes/InsightViz/ResultCustomizationsModal.tsx @@ -0,0 +1,361 @@ +import './ResultCustomizationsModal.scss' + +import { LemonButton, LemonButtonProps, LemonModal } from '@posthog/lemon-ui' +import assert from 'assert' +import { useActions, useValues } from 'kea' +import { DataColorToken } from 'lib/colors' +import { EntityFilterInfo } from 'lib/components/EntityFilterInfo' +import { ColorGlyph } from 'lib/components/SeriesGlyph' +import { hexToRGB } from 'lib/utils' +import { dataThemeLogic } from 'scenes/dataThemeLogic' +import { insightLogic } from 'scenes/insights/insightLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { formatBreakdownLabel } from 'scenes/insights/utils' +import { IndexedTrendResult } from 'scenes/trends/types' + +import { cohortsModel } from '~/models/cohortsModel' +import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' +import { ResultCustomizationBy } from '~/queries/schema' +import { FlattenedFunnelStepByBreakdown } from '~/types' + +import { resultCustomizationsModalLogic } from './resultCustomizationsModalLogic' + +export function ResultCustomizationsModal(): JSX.Element | null { + const { insightProps } = useValues(insightLogic) + + const { modalVisible, dataset, colorToken, resultCustomizationBy } = useValues( + resultCustomizationsModalLogic(insightProps) + ) + const { closeModal, setColorToken, save } = useActions(resultCustomizationsModalLogic(insightProps)) + + const { isTrends, isFunnels, querySource } = useValues(insightVizDataLogic) + + const { getTheme } = useValues(dataThemeLogic) + const theme = getTheme(querySource?.dataColorTheme) + + if (dataset == null || theme == null) { + return null + } + + return ( + + + Cancel + + + Save customizations + + + } + onClose={closeModal} + > +

+ Query results can be customized to provide a more{' '} + meaningful appearance for you and your team members. The customizations are also shown + on dashboards. +

+ {isTrends && ( + + )} + {isFunnels && } + +

Color

+
+ {Object.keys(theme).map((key) => ( + { + e.preventDefault() + e.stopPropagation() + + setColorToken(key as DataColorToken) + }} + /> + ))} +
+
+ ) +} + +type TrendsInfoProps = { + dataset: IndexedTrendResult + resultCustomizationBy: ResultCustomizationBy +} + +function TrendsInfo({ dataset, resultCustomizationBy }: TrendsInfoProps): JSX.Element { + const { cohorts } = useValues(cohortsModel) + const { formatPropertyValueForDisplay } = useValues(propertyDefinitionsModel) + const { breakdownFilter } = useValues(insightVizDataLogic) + + return ( + <> + {dataset.breakdown_value ? ( +

+ You are customizing the appearance of series{' '} + + + {' '} + for the breakdown{' '} + + {formatBreakdownLabel( + dataset.breakdown_value, + breakdownFilter, + cohorts, + formatPropertyValueForDisplay + )} + + . +

+ ) : ( +

+ You are customizing the appearance of series{' '} + + + + . +

+ )} + +

+ Results are assigned by{' '} + {resultCustomizationBy === ResultCustomizationBy.Position ? ( + <> + their rank in the dataset + + ) : ( + <> + their name in the dataset + + )} + . You can change this in insight settings. +

+ + ) +} + +type FunnelsInfoProps = { + dataset: FlattenedFunnelStepByBreakdown +} + +function FunnelsInfo({ dataset }: FunnelsInfoProps): JSX.Element { + return ( + <> + You are customizing the appearance of the{' '} + {dataset.breakdown_value?.[0] === 'Baseline' ? ( + Baseline + ) : ( + <> + {dataset.breakdown_value?.[0]} breakdown + + )} + . + + ) +} + +type ColorGlyphButtonProps = { + colorToken: DataColorToken + selected: boolean + onClick: LemonButtonProps['onClick'] +} + +function ColorGlyphButton({ colorToken, selected, onClick }: ColorGlyphButtonProps): JSX.Element { + const { getTheme } = useValues(dataThemeLogic) + + const { querySource } = useValues(insightVizDataLogic) + + const theme = getTheme(querySource?.dataColorTheme) + const color = theme?.[colorToken] as string + + return ( + + + + ) +} + +type ReferenceColor = { name: string; group: string } + +/** HTML5 colors */ +const referenceColors: Record = { + '#FFC0CB': { name: 'Pink', group: 'Pink' }, + '#FFB6C1': { name: 'LightPink', group: 'Pink' }, + '#FF69B4': { name: 'HotPink', group: 'Pink' }, + '#FF1493': { name: 'DeepPink', group: 'Pink' }, + '#DB7093': { name: 'PaleVioletRed', group: 'Pink' }, + '#C71585': { name: 'MediumVioletRed', group: 'Pink' }, + '#E6E6FA': { name: 'Lavender', group: 'Purple' }, + '#D8BFD8': { name: 'Thistle', group: 'Purple' }, + '#DDA0DD': { name: 'Plum', group: 'Purple' }, + '#DA70D6': { name: 'Orchid', group: 'Purple' }, + '#EE82EE': { name: 'Violet', group: 'Purple' }, + '#FF00FF': { name: 'Magenta', group: 'Purple' }, + '#BA55D3': { name: 'MediumOrchid', group: 'Purple' }, + '#9932CC': { name: 'DarkOrchid', group: 'Purple' }, + '#9400D3': { name: 'DarkViolet', group: 'Purple' }, + '#8A2BE2': { name: 'BlueViolet', group: 'Purple' }, + '#8B008B': { name: 'DarkMagenta', group: 'Purple' }, + '#800080': { name: 'Purple', group: 'Purple' }, + '#9370DB': { name: 'MediumPurple', group: 'Purple' }, + '#7B68EE': { name: 'MediumSlateBlue', group: 'Purple' }, + '#6A5ACD': { name: 'SlateBlue', group: 'Purple' }, + '#483D8B': { name: 'DarkSlateBlue', group: 'Purple' }, + '#663399': { name: 'RebeccaPurple', group: 'Purple' }, + '#4B0082': { name: 'Indigo', group: 'Purple' }, + '#FFA07A': { name: 'LightSalmon', group: 'Red' }, + '#FA8072': { name: 'Salmon', group: 'Red' }, + '#E9967A': { name: 'DarkSalmon', group: 'Red' }, + '#F08080': { name: 'LightCoral', group: 'Red' }, + '#CD5C5C': { name: 'IndianRed', group: 'Red' }, + '#DC143C': { name: 'Crimson', group: 'Red' }, + '#FF0000': { name: 'Red', group: 'Red' }, + '#B22222': { name: 'FireBrick', group: 'Red' }, + '#8B0000': { name: 'DarkRed', group: 'Red' }, + '#FFA500': { name: 'Orange', group: 'Orange' }, + '#FF8C00': { name: 'DarkOrange', group: 'Orange' }, + '#FF7F50': { name: 'Coral', group: 'Orange' }, + '#FF6347': { name: 'Tomato', group: 'Orange' }, + '#FF4500': { name: 'OrangeRed', group: 'Orange' }, + '#FFD700': { name: 'Gold', group: 'Yellow' }, + '#FFFF00': { name: 'Yellow', group: 'Yellow' }, + '#FFFFE0': { name: 'LightYellow', group: 'Yellow' }, + '#FFFACD': { name: 'LemonChiffon', group: 'Yellow' }, + '#FAFAD2': { name: 'LightGoldenRodYellow', group: 'Yellow' }, + '#FFEFD5': { name: 'PapayaWhip', group: 'Yellow' }, + '#FFE4B5': { name: 'Moccasin', group: 'Yellow' }, + '#FFDAB9': { name: 'PeachPuff', group: 'Yellow' }, + '#EEE8AA': { name: 'PaleGoldenRod', group: 'Yellow' }, + '#F0E68C': { name: 'Khaki', group: 'Yellow' }, + '#BDB76B': { name: 'DarkKhaki', group: 'Yellow' }, + '#ADFF2F': { name: 'GreenYellow', group: 'Green' }, + '#7FFF00': { name: 'Chartreuse', group: 'Green' }, + '#7CFC00': { name: 'LawnGreen', group: 'Green' }, + '#00FF00': { name: 'Lime', group: 'Green' }, + '#32CD32': { name: 'LimeGreen', group: 'Green' }, + '#98FB98': { name: 'PaleGreen', group: 'Green' }, + '#90EE90': { name: 'LightGreen', group: 'Green' }, + '#00FA9A': { name: 'MediumSpringGreen', group: 'Green' }, + '#00FF7F': { name: 'SpringGreen', group: 'Green' }, + '#3CB371': { name: 'MediumSeaGreen', group: 'Green' }, + '#2E8B57': { name: 'SeaGreen', group: 'Green' }, + '#228B22': { name: 'ForestGreen', group: 'Green' }, + '#008000': { name: 'Green', group: 'Green' }, + '#006400': { name: 'DarkGreen', group: 'Green' }, + '#9ACD32': { name: 'YellowGreen', group: 'Green' }, + '#6B8E23': { name: 'OliveDrab', group: 'Green' }, + '#556B2F': { name: 'DarkOliveGreen', group: 'Green' }, + '#66CDAA': { name: 'MediumAquaMarine', group: 'Green' }, + '#8FBC8F': { name: 'DarkSeaGreen', group: 'Green' }, + '#20B2AA': { name: 'LightSeaGreen', group: 'Green' }, + '#008B8B': { name: 'DarkCyan', group: 'Green' }, + '#008080': { name: 'Teal', group: 'Green' }, + '#00FFFF': { name: 'Cyan', group: 'Cyan' }, + '#E0FFFF': { name: 'LightCyan', group: 'Cyan' }, + '#AFEEEE': { name: 'PaleTurquoise', group: 'Cyan' }, + '#7FFFD4': { name: 'Aquamarine', group: 'Cyan' }, + '#40E0D0': { name: 'Turquoise', group: 'Cyan' }, + '#48D1CC': { name: 'MediumTurquoise', group: 'Cyan' }, + '#00CED1': { name: 'DarkTurquoise', group: 'Cyan' }, + '#5F9EA0': { name: 'CadetBlue', group: 'Blue' }, + '#4682B4': { name: 'SteelBlue', group: 'Blue' }, + '#B0C4DE': { name: 'LightSteelBlue', group: 'Blue' }, + '#ADD8E6': { name: 'LightBlue', group: 'Blue' }, + '#B0E0E6': { name: 'PowderBlue', group: 'Blue' }, + '#87CEFA': { name: 'LightSkyBlue', group: 'Blue' }, + '#87CEEB': { name: 'SkyBlue', group: 'Blue' }, + '#6495ED': { name: 'CornflowerBlue', group: 'Blue' }, + '#00BFFF': { name: 'DeepSkyBlue', group: 'Blue' }, + '#1E90FF': { name: 'DodgerBlue', group: 'Blue' }, + '#4169E1': { name: 'RoyalBlue', group: 'Blue' }, + '#0000FF': { name: 'Blue', group: 'Blue' }, + '#0000CD': { name: 'MediumBlue', group: 'Blue' }, + '#00008B': { name: 'DarkBlue', group: 'Blue' }, + '#000080': { name: 'Navy', group: 'Blue' }, + '#191970': { name: 'MidnightBlue', group: 'Blue' }, + '#FFF8DC': { name: 'Cornsilk', group: 'Brown' }, + '#FFEBCD': { name: 'BlanchedAlmond', group: 'Brown' }, + '#FFE4C4': { name: 'Bisque', group: 'Brown' }, + '#FFDEAD': { name: 'NavajoWhite', group: 'Brown' }, + '#F5DEB3': { name: 'Wheat', group: 'Brown' }, + '#DEB887': { name: 'BurlyWood', group: 'Brown' }, + '#D2B48C': { name: 'Tan', group: 'Brown' }, + '#BC8F8F': { name: 'RosyBrown', group: 'Brown' }, + '#F4A460': { name: 'SandyBrown', group: 'Brown' }, + '#DAA520': { name: 'GoldenRod', group: 'Brown' }, + '#B8860B': { name: 'DarkGoldenRod', group: 'Brown' }, + '#CD853F': { name: 'Peru', group: 'Brown' }, + '#D2691E': { name: 'Chocolate', group: 'Brown' }, + '#808000': { name: 'Olive', group: 'Brown' }, + '#8B4513': { name: 'SaddleBrown', group: 'Brown' }, + '#A0522D': { name: 'Sienna', group: 'Brown' }, + '#A52A2A': { name: 'Brown', group: 'Brown' }, + '#800000': { name: 'Maroon', group: 'Brown' }, + '#FFFFFF': { name: 'White', group: 'White' }, + '#FFFAFA': { name: 'Snow', group: 'White' }, + '#F0FFF0': { name: 'HoneyDew', group: 'White' }, + '#F5FFFA': { name: 'MintCream', group: 'White' }, + '#F0FFFF': { name: 'Azure', group: 'White' }, + '#F0F8FF': { name: 'AliceBlue', group: 'White' }, + '#F8F8FF': { name: 'GhostWhite', group: 'White' }, + '#F5F5F5': { name: 'WhiteSmoke', group: 'White' }, + '#FFF5EE': { name: 'SeaShell', group: 'White' }, + '#F5F5DC': { name: 'Beige', group: 'White' }, + '#FDF5E6': { name: 'OldLace', group: 'White' }, + '#FFFAF0': { name: 'FloralWhite', group: 'White' }, + '#FFFFF0': { name: 'Ivory', group: 'White' }, + '#FAEBD7': { name: 'AntiqueWhite', group: 'White' }, + '#FAF0E6': { name: 'Linen', group: 'White' }, + '#FFF0F5': { name: 'LavenderBlush', group: 'White' }, + '#FFE4E1': { name: 'MistyRose', group: 'White' }, + '#DCDCDC': { name: 'Gainsboro', group: 'Gray' }, + '#D3D3D3': { name: 'LightGray', group: 'Gray' }, + '#C0C0C0': { name: 'Silver', group: 'Gray' }, + '#A9A9A9': { name: 'DarkGray', group: 'Gray' }, + '#696969': { name: 'DimGray', group: 'Gray' }, + '#808080': { name: 'Gray', group: 'Gray' }, + '#778899': { name: 'LightSlateGray', group: 'Gray' }, + '#708090': { name: 'SlateGray', group: 'Gray' }, + '#2F4F4F': { name: 'DarkSlateGray', group: 'Gray' }, + '#000000': { name: 'Black', group: 'Gray' }, +} + +function nearestColor(color: string): ReferenceColor { + const { r: r1, g: g1, b: b1 } = hexToRGB(color) + + let minDistance = null + let minColor = null + + for (const referenceColor in referenceColors) { + const { r: r2, g: g2, b: b2 } = hexToRGB(referenceColor) + const distance = Math.sqrt((r2 - r1) ** 2 + (g2 - g1) ** 2 + (b2 - b1) ** 2) + + if (minDistance === null || distance < minDistance) { + minDistance = distance + minColor = referenceColor + } + } + + assert(minColor) + + return referenceColors[minColor] +} + +function colorDescription(color: string): string { + const { name, group } = nearestColor(color) + const colorName = name.split(/(?=[A-Z])/).join(' ') + + return colorName.includes(group) ? colorName : `${colorName} (${group})` +} diff --git a/frontend/src/queries/nodes/InsightViz/resultCustomizationsModalLogic.ts b/frontend/src/queries/nodes/InsightViz/resultCustomizationsModalLogic.ts new file mode 100644 index 0000000000000..c297bfada948f --- /dev/null +++ b/frontend/src/queries/nodes/InsightViz/resultCustomizationsModalLogic.ts @@ -0,0 +1,150 @@ +import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { DataColorToken } from 'lib/colors' +import { FEATURE_FLAGS } from 'lib/constants' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { funnelDataLogic } from 'scenes/funnels/funnelDataLogic' +import { RESULT_CUSTOMIZATION_DEFAULT } from 'scenes/insights/EditorFilters/ResultCustomizationByPicker' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { getFunnelDatasetKey, getTrendResultCustomizationKey } from 'scenes/insights/utils' +import { trendsDataLogic } from 'scenes/trends/trendsDataLogic' +import { IndexedTrendResult } from 'scenes/trends/types' + +import { ResultCustomizationBy, TrendsFilter } from '~/queries/schema' +import { FlattenedFunnelStepByBreakdown, InsightLogicProps } from '~/types' + +import type { resultCustomizationsModalLogicType } from './resultCustomizationsModalLogicType' + +export const resultCustomizationsModalLogic = kea([ + props({} as InsightLogicProps), + key(keyForInsightLogicProps('new')), + path((key) => ['scenes', 'insights', 'views', 'InsightsTable', 'resultCustomizationsModalLogic', key]), + + connect((props: InsightLogicProps) => ({ + values: [ + insightVizDataLogic, + ['isTrends', 'isFunnels', 'insightFilter'], + trendsDataLogic(props), + [ + 'resultCustomizationBy as resultCustomizationByRaw', + 'resultCustomizations as trendsResultCustomizations', + 'getTrendsColorToken', + ], + funnelDataLogic(props), + ['resultCustomizations as funnelsResultCustomizations', 'getFunnelsColorToken'], + featureFlagLogic, + ['featureFlags'], + ], + actions: [insightVizDataLogic, ['updateInsightFilter']], + })), + + actions({ + openModal: (dataset: IndexedTrendResult | FlattenedFunnelStepByBreakdown) => ({ dataset }), + closeModal: true, + + setColorToken: (token: DataColorToken) => ({ token }), + + save: true, + }), + + reducers({ + dataset: [ + null as IndexedTrendResult | FlattenedFunnelStepByBreakdown | null, + { + openModal: (_, { dataset }) => dataset, + closeModal: () => null, + }, + ], + localColorToken: [ + null as DataColorToken | null, + { + setColorToken: (_, { token }) => token, + closeModal: () => null, + }, + ], + }), + + selectors({ + hasInsightColors: [ + (s) => [s.featureFlags], + (featureFlags): boolean => !!featureFlags[FEATURE_FLAGS.INSIGHT_COLORS], + ], + modalVisible: [(s) => [s.dataset], (dataset): boolean => dataset !== null], + colorToken: [ + (s) => [s.localColorToken, s.colorTokenFromQuery], + (localColorToken, colorTokenFromQuery): DataColorToken | null => localColorToken || colorTokenFromQuery, + ], + colorTokenFromQuery: [ + (s) => [s.isTrends, s.isFunnels, s.getTrendsColorToken, s.getFunnelsColorToken, s.dataset], + (isTrends, isFunnels, getTrendsColorToken, getFunnelsColorToken, dataset): DataColorToken | null => { + if (!dataset) { + return null + } + + if (isTrends) { + return getTrendsColorToken(dataset as IndexedTrendResult) + } else if (isFunnels) { + return getFunnelsColorToken(dataset as FlattenedFunnelStepByBreakdown) + } + + return null + }, + ], + resultCustomizationBy: [ + (s) => [s.resultCustomizationByRaw], + (resultCustomizationByRaw) => resultCustomizationByRaw || RESULT_CUSTOMIZATION_DEFAULT, + ], + resultCustomizations: [ + (s) => [s.isTrends, s.isFunnels, s.trendsResultCustomizations, s.funnelsResultCustomizations], + (isTrends, isFunnels, trendsResultCustomizations, funnelsResultCustomizations) => { + if (isTrends) { + return trendsResultCustomizations + } else if (isFunnels) { + return funnelsResultCustomizations + } + + return null + }, + ], + }), + + listeners(({ actions, values }) => ({ + save: () => { + if (values.localColorToken == null || values.dataset == null) { + actions.closeModal() + return + } + + if (values.isTrends) { + const resultCustomizationKey = getTrendResultCustomizationKey( + values.resultCustomizationBy, + values.dataset as IndexedTrendResult + ) + actions.updateInsightFilter({ + resultCustomizations: { + ...values.trendsResultCustomizations, + [resultCustomizationKey]: { + assignmentBy: values.resultCustomizationBy, + color: values.localColorToken, + }, + }, + } as Partial) + } + + if (values.isFunnels) { + const resultCustomizationKey = getFunnelDatasetKey(values.dataset as FlattenedFunnelStepByBreakdown) + actions.updateInsightFilter({ + resultCustomizations: { + ...values.funnelsResultCustomizations, + [resultCustomizationKey]: { + assignmentBy: ResultCustomizationBy.Value, + color: values.localColorToken, + }, + }, + }) + } + + actions.closeModal() + }, + })), +]) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index dc52be4fe170c..adf0ef5f77413 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -3929,6 +3929,26 @@ }, "type": "object" }, + "DataColorToken": { + "enum": [ + "preset-1", + "preset-2", + "preset-3", + "preset-4", + "preset-5", + "preset-6", + "preset-7", + "preset-8", + "preset-9", + "preset-10", + "preset-11", + "preset-12", + "preset-13", + "preset-14", + "preset-15" + ], + "type": "string" + }, "DataTableNode": { "additionalProperties": false, "properties": { @@ -6782,6 +6802,13 @@ "$ref": "#/definitions/FunnelLayout", "default": "vertical" }, + "resultCustomizations": { + "additionalProperties": { + "$ref": "#/definitions/ResultCustomizationByValue" + }, + "description": "Customizations for the appearance of result datasets.", + "type": "object" + }, "useUdf": { "type": "boolean" } @@ -6868,6 +6895,10 @@ "$ref": "#/definitions/BreakdownFilter", "description": "Breakdown of the events and actions" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -7950,6 +7981,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8007,6 +8042,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8064,6 +8103,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8121,6 +8164,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8178,6 +8225,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8288,6 +8339,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -8697,6 +8752,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -11362,6 +11421,58 @@ } ] }, + "ResultCustomization": { + "anyOf": [ + { + "$ref": "#/definitions/ResultCustomizationByValue" + }, + { + "$ref": "#/definitions/ResultCustomizationByPosition" + } + ] + }, + "ResultCustomizationBase": { + "additionalProperties": false, + "properties": { + "color": { + "$ref": "#/definitions/DataColorToken" + } + }, + "required": ["color"], + "type": "object" + }, + "ResultCustomizationBy": { + "enum": ["value", "position"], + "type": "string" + }, + "ResultCustomizationByPosition": { + "additionalProperties": false, + "properties": { + "assignmentBy": { + "const": "position", + "type": "string" + }, + "color": { + "$ref": "#/definitions/DataColorToken" + } + }, + "required": ["assignmentBy", "color"], + "type": "object" + }, + "ResultCustomizationByValue": { + "additionalProperties": false, + "properties": { + "assignmentBy": { + "const": "value", + "type": "string" + }, + "color": { + "$ref": "#/definitions/DataColorToken" + } + }, + "required": ["assignmentBy", "color"], + "type": "object" + }, "RetentionEntity": { "additionalProperties": false, "properties": { @@ -11476,6 +11587,10 @@ ], "description": "Groups aggregation" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -12146,6 +12261,10 @@ "$ref": "#/definitions/CompareFilter", "description": "Compare to date range" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -12568,6 +12687,31 @@ }, "type": "array" }, + "resultCustomizationBy": { + "$ref": "#/definitions/ResultCustomizationBy", + "default": "value", + "description": "Wether result datasets are associated by their values or by their order." + }, + "resultCustomizations": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/definitions/ResultCustomizationByValue" + }, + "type": "object" + }, + { + "additionalProperties": { + "$ref": "#/definitions/ResultCustomizationByPosition" + }, + "propertyNames": { + "type": "string" + }, + "type": "object" + } + ], + "description": "Customizations for the appearance of result datasets." + }, "showAlertThresholdLines": { "default": false, "type": "boolean" @@ -12700,6 +12844,10 @@ ], "description": "Whether we should be comparing against a specific conversion goal" }, + "dataColorTheme": { + "description": "Colors used in the insight's visualization", + "type": ["number", "null"] + }, "dateRange": { "$ref": "#/definitions/DateRange", "description": "Date range for the query" @@ -13418,6 +13566,9 @@ } }, "type": "object" + }, + "numerical_key": { + "type": "string" } } } diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index daaae3e403c08..26b995a1c6d1d 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1,3 +1,4 @@ +import { DataColorToken } from 'lib/colors' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { @@ -43,6 +44,10 @@ export { ChartDisplayCategory } /** @asType integer */ type integer = number +// Type alias for a numerical key. Needs to be reflected as string in json-schema, as JSON only supports string keys. +/** @asType string */ +export type numerical_key = number + /** * PostHog Query Schema definition. * @@ -840,6 +845,8 @@ export interface InsightsQueryBase> ex aggregation_group_type_index?: integer | null /** Sampling rate */ samplingFactor?: number | null + /** Colors used in the insight's visualization */ + dataColorTheme?: number | null /** Modifiers used when performing the query */ modifiers?: HogQLQueryModifiers } @@ -847,6 +854,11 @@ export interface InsightsQueryBase> ex /** `TrendsFilterType` minus everything inherited from `FilterType` and `shown_as` */ export type TrendsFilterLegacy = Omit +export enum ResultCustomizationBy { + Value = 'value', + Position = 'position', +} + export type TrendsFilter = { /** @default 1 */ smoothingIntervals?: integer @@ -870,6 +882,15 @@ export type TrendsFilter = { showPercentStackView?: TrendsFilterLegacy['show_percent_stack_view'] yAxisScaleType?: TrendsFilterLegacy['y_axis_scale_type'] hiddenLegendIndexes?: integer[] + /** + * Wether result datasets are associated by their values or by their order. + * @default value + **/ + resultCustomizationBy?: ResultCustomizationBy + /** Customizations for the appearance of result datasets. */ + resultCustomizations?: + | Record + | Record } export const TRENDS_FILTER_PROPERTIES = new Set([ @@ -896,6 +917,20 @@ export interface TrendsQueryResponse extends AnalyticsQueryResponseBase +export type ResultCustomizationBase = { + color: DataColorToken +} + +export interface ResultCustomizationByPosition extends ResultCustomizationBase { + assignmentBy: ResultCustomizationBy.Position +} + +export interface ResultCustomizationByValue extends ResultCustomizationBase { + assignmentBy: ResultCustomizationBy.Value +} + +export type ResultCustomization = ResultCustomizationByValue | ResultCustomizationByPosition + export interface TrendsQuery extends InsightsQueryBase { kind: NodeKind.TrendsQuery /** @@ -1369,6 +1404,8 @@ export type FunnelsFilter = { /** @default total */ funnelStepReference?: FunnelsFilterLegacy['funnel_step_reference'] useUdf?: boolean + /** Customizations for the appearance of result datasets. */ + resultCustomizations?: Record } export interface FunnelsQuery extends InsightsQueryBase { diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index dc98b2ec2d3d1..b3a4e305811f0 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -33,6 +33,7 @@ import { PersonsNode, QuerySchema, QueryStatusResponse, + ResultCustomizationBy, RetentionQuery, SavedInsightNode, SessionAttributionExplorerQuery, @@ -333,6 +334,13 @@ export const getYAxisScaleType = (query: InsightQueryNode): string | undefined = return undefined } +export const getResultCustomizationBy = (query: InsightQueryNode): ResultCustomizationBy | undefined => { + if (isTrendsQuery(query)) { + return query.trendsFilter?.resultCustomizationBy + } + return undefined +} + export const supportsPercentStackView = (q: InsightQueryNode | null | undefined): boolean => isTrendsQuery(q) && PERCENT_STACK_VIEW_DISPLAY_TYPE.includes(getDisplay(q) || ChartDisplayType.ActionsLineGraph) diff --git a/frontend/src/scenes/dashboard/Dashboard.tsx b/frontend/src/scenes/dashboard/Dashboard.tsx index 5f9e59ee897de..0906d2c948ebb 100644 --- a/frontend/src/scenes/dashboard/Dashboard.tsx +++ b/frontend/src/scenes/dashboard/Dashboard.tsx @@ -1,5 +1,5 @@ import { LemonButton } from '@posthog/lemon-ui' -import { BindLogic, useActions, useValues } from 'kea' +import { BindLogic, useActions, useMountedLogic, useValues } from 'kea' import { NotFound } from 'lib/components/NotFound' import { useKeyboardHotkeys } from 'lib/hooks/useKeyboardHotkeys' import { DashboardEventSource } from 'lib/utils/eventUsageLogic' @@ -8,12 +8,13 @@ import { DashboardEditBar } from 'scenes/dashboard/DashboardEditBar' import { DashboardItems } from 'scenes/dashboard/DashboardItems' import { dashboardLogic, DashboardLogicProps } from 'scenes/dashboard/dashboardLogic' import { DashboardReloadAction, LastRefreshText } from 'scenes/dashboard/DashboardReloadAction' +import { dataThemeLogic } from 'scenes/dataThemeLogic' import { InsightErrorState } from 'scenes/insights/EmptyStates' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' import { VariablesForDashboard } from '~/queries/nodes/DataVisualization/Components/Variables/Variables' -import { DashboardMode, DashboardPlacement, DashboardType, QueryBasedInsightModel } from '~/types' +import { DashboardMode, DashboardPlacement, DashboardType, DataColorThemeModel, QueryBasedInsightModel } from '~/types' import { DashboardHeader } from './DashboardHeader' import { EmptyDashboardComponent } from './EmptyDashboardComponent' @@ -22,6 +23,7 @@ interface DashboardProps { id?: string dashboard?: DashboardType placement?: DashboardPlacement + themes?: DataColorThemeModel[] } export const scene: SceneExport = { @@ -33,7 +35,9 @@ export const scene: SceneExport = { }), } -export function Dashboard({ id, dashboard, placement }: DashboardProps = {}): JSX.Element { +export function Dashboard({ id, dashboard, placement, themes }: DashboardProps = {}): JSX.Element { + useMountedLogic(dataThemeLogic({ themes })) + return ( diff --git a/frontend/src/scenes/dataThemeLogic.tsx b/frontend/src/scenes/dataThemeLogic.tsx new file mode 100644 index 0000000000000..af0bc658ff3a8 --- /dev/null +++ b/frontend/src/scenes/dataThemeLogic.tsx @@ -0,0 +1,94 @@ +import { actions, afterMount, connect, kea, path, props, reducers, selectors, useValues } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { DataColorTheme } from 'lib/colors' + +import { DataColorThemeModel } from '~/types' + +import type { dataThemeLogicType } from './dataThemeLogicType' +import { teamLogic } from './teamLogic' + +export const ThemeName = ({ id }: { id: number }): JSX.Element => { + const { themes } = useValues(dataThemeLogic) + const theme = themes?.find((theme) => theme.id === id) + + return theme ? {theme.name} : No theme found for id: {id} +} + +export type DataThemeLogicProps = { + themes?: DataColorThemeModel[] +} + +export const dataThemeLogic = kea([ + props({} as DataThemeLogicProps), + path(['scenes', 'dataThemeLogic']), + connect({ values: [teamLogic, ['currentTeam']] }), + actions({ setThemes: (themes) => ({ themes }) }), + loaders(({ props }) => ({ + themes: [ + props.themes || null, + { + loadThemes: async () => await api.dataColorThemes.list(), + }, + ], + })), + reducers({ + themes: { + setThemes: (_, { themes }) => themes, + }, + }), + selectors({ + posthogTheme: [ + (s) => [s.themes], + (themes) => { + if (!themes) { + return null + } + + return themes.sort((theme) => theme.id).find((theme) => theme.is_global) + }, + ], + defaultTheme: [ + (s) => [s.currentTeam, s.themes, s.posthogTheme], + (currentTeam, themes, posthogTheme) => { + if (!currentTeam || !themes) { + return null + } + + // use the posthog theme unless someone set a specfic theme for the team + const environmentTheme = themes.find((theme) => theme.id === currentTeam.default_data_theme) + return environmentTheme || posthogTheme + }, + ], + getTheme: [ + (s) => [s.themes, s.defaultTheme], + (themes, defaultTheme) => + (themeId: string | number | null | undefined): DataColorTheme | null => { + let customTheme + + if (Number.isInteger(themeId) && themes != null) { + customTheme = themes.find((theme) => theme.id === themeId) + } + + if (customTheme) { + return customTheme.colors.reduce((theme, color, index) => { + theme[`preset-${index + 1}`] = color + return theme + }, {}) + } + + if (defaultTheme) { + return defaultTheme.colors.reduce((theme, color, index) => { + theme[`preset-${index + 1}`] = color + return theme + }, {}) + } + + return null + }, + ], + }), + afterMount(({ actions }) => { + actions.loadThemes() + }), +]) diff --git a/frontend/src/scenes/funnels/FunnelBarHorizontal/Bar.tsx b/frontend/src/scenes/funnels/FunnelBarHorizontal/Bar.tsx index 2a723efa32d81..e31a03e42b771 100644 --- a/frontend/src/scenes/funnels/FunnelBarHorizontal/Bar.tsx +++ b/frontend/src/scenes/funnels/FunnelBarHorizontal/Bar.tsx @@ -1,12 +1,14 @@ import { LemonDropdown } from '@posthog/lemon-ui' -import { getSeriesColor } from 'lib/colors' +import { useValues } from 'kea' import { capitalizeFirstLetter, percentage } from 'lib/utils' import { useEffect, useRef, useState } from 'react' +import { insightLogic } from 'scenes/insights/insightLogic' import { Noun } from '~/models/groupsModel' import { BreakdownFilter } from '~/queries/schema' import { FunnelStepWithConversionMetrics } from '~/types' +import { funnelDataLogic } from '../funnelDataLogic' import { FunnelTooltip } from '../FunnelTooltip' import { getSeriesPositionName } from '../funnelUtils' @@ -43,6 +45,9 @@ export function Bar({ aggregationTargetLabel, wrapperWidth, }: BarProps): JSX.Element | null { + const { insightProps } = useValues(insightLogic) + const { getFunnelsColor } = useValues(funnelDataLogic(insightProps)) + const barRef = useRef(null) const labelRef = useRef(null) const [labelPosition, setLabelPosition] = useState('inside') @@ -111,7 +116,7 @@ export function Bar({ style={{ flex: `${conversionPercentage} 1 0`, cursor: cursorType, - backgroundColor: getSeriesColor(breakdownIndex ?? 0), + backgroundColor: getFunnelsColor(step), }} onClick={() => { if (!disabled && onBarClick) { diff --git a/frontend/src/scenes/funnels/FunnelBarVertical/StepBar.tsx b/frontend/src/scenes/funnels/FunnelBarVertical/StepBar.tsx index 8c4f301a87c9f..f5224fcf54318 100644 --- a/frontend/src/scenes/funnels/FunnelBarVertical/StepBar.tsx +++ b/frontend/src/scenes/funnels/FunnelBarVertical/StepBar.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' -import { getSeriesColor } from 'lib/colors' import { percentage } from 'lib/utils' import { useRef } from 'react' import { insightLogic } from 'scenes/insights/insightLogic' @@ -23,21 +22,19 @@ interface StepBarCSSProperties extends React.CSSProperties { } export function StepBar({ step, stepIndex, series, showPersonsModal }: StepBarProps): JSX.Element { const { insightProps } = useValues(insightLogic) - const { disableFunnelBreakdownBaseline } = useValues(funnelDataLogic(insightProps)) + const { getFunnelsColor } = useValues(funnelDataLogic(insightProps)) const { showTooltip, hideTooltip } = useActions(funnelTooltipLogic(insightProps)) const { openPersonsModalForSeries } = useActions(funnelPersonsModalLogic(insightProps)) const ref = useRef(null) - const seriesOrderForColor = disableFunnelBreakdownBaseline ? (series.order ?? 0) + 1 : series.order ?? 0 - return (
([ 'interval', 'insightData', 'insightDataError', + 'theme', ], groupsModel, ['aggregationLabel'], @@ -84,7 +86,7 @@ export const funnelDataLogic = kea([ ], }), - selectors(() => ({ + selectors(({ props }) => ({ querySource: [ (s) => [s.vizQuerySource], (vizQuerySource) => (isFunnelsQuery(vizQuerySource) ? vizQuerySource : null), @@ -209,6 +211,7 @@ export const funnelDataLogic = kea([ }, ], hiddenLegendBreakdowns: [(s) => [s.funnelsFilter], (funnelsFilter) => funnelsFilter?.hiddenLegendBreakdowns], + resultCustomizations: [(s) => [s.funnelsFilter], (funnelsFilter) => funnelsFilter?.resultCustomizations], visibleStepsWithConversionMetrics: [ (s) => [s.stepsWithConversionMetrics, s.funnelsFilter, s.flattenedBreakdowns], (steps, funnelsFilter, flattenedBreakdowns): FunnelStepWithConversionMetrics[] => { @@ -408,6 +411,33 @@ export const funnelDataLogic = kea([ (steps) => Array.isArray(steps) ? steps.map((step, index) => ({ ...step, seriesIndex: index, id: index })) : [], ], + getFunnelsColorToken: [ + (s) => [s.resultCustomizations, s.theme], + (resultCustomizations, theme) => { + return (dataset) => { + if (theme == null) { + return null + } + return getFunnelResultCustomizationColorToken( + resultCustomizations, + theme, + dataset, + props?.cachedInsight?.disable_baseline + ) + } + }, + ], + getFunnelsColor: [ + (s) => [s.theme, s.getFunnelsColorToken], + (theme, getFunnelsColorToken) => { + return (dataset) => { + if (theme == null) { + return '#000000' // fallback while loading + } + return theme[getFunnelsColorToken(dataset)!] + } + }, + ], })), listeners(({ actions, values }) => ({ diff --git a/frontend/src/scenes/insights/EditorFilters/ResultCustomizationByPicker.tsx b/frontend/src/scenes/insights/EditorFilters/ResultCustomizationByPicker.tsx new file mode 100644 index 0000000000000..4e81afb843ec1 --- /dev/null +++ b/frontend/src/scenes/insights/EditorFilters/ResultCustomizationByPicker.tsx @@ -0,0 +1,29 @@ +import { LemonSegmentedButton } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { insightLogic } from 'scenes/insights/insightLogic' + +import { ResultCustomizationBy } from '~/queries/schema' + +import { insightVizDataLogic } from '../insightVizDataLogic' + +export const RESULT_CUSTOMIZATION_DEFAULT = ResultCustomizationBy.Value + +export function ResultCustomizationByPicker(): JSX.Element | null { + const { insightProps } = useValues(insightLogic) + const { resultCustomizationBy } = useValues(insightVizDataLogic(insightProps)) + const { updateInsightFilter } = useActions(insightVizDataLogic(insightProps)) + + return ( + updateInsightFilter({ resultCustomizationBy: value as ResultCustomizationBy })} + value={resultCustomizationBy || RESULT_CUSTOMIZATION_DEFAULT} + options={[ + { value: ResultCustomizationBy.Value, label: 'By name' }, + { value: ResultCustomizationBy.Position, label: 'By rank' }, + ]} + size="small" + fullWidth + /> + ) +} diff --git a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx index a0a2fb58e7b1f..a3aefd3bd7b39 100644 --- a/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx +++ b/frontend/src/scenes/insights/InsightNav/insightNavLogic.tsx @@ -57,9 +57,11 @@ export interface Tab { dataAttr: string } +type OmitConflictingProperties = Omit + export interface CommonInsightFilter - extends Partial, - Partial, + extends Partial>, + Partial>, Partial, Partial, Partial, @@ -276,9 +278,11 @@ const cachePropertiesFromQuery = (query: InsightQueryNode, cache: QueryPropertyC newCache.series = cache?.series } - // store the insight specific filter in commonFilter + /** store the insight specific filter in commonFilter */ const filterKey = filterKeyForQuery(query) - newCache.commonFilter = { ...cache?.commonFilter, ...query[filterKey] } + // exclude properties that shouldn't be shared + const { resultCustomizations, ...commonProperties } = query[filterKey] || {} + newCache.commonFilter = { ...cache?.commonFilter, ...commonProperties } return newCache } diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index 14b0b4cbd393d..dfaa7f8135694 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -11,6 +11,7 @@ import { dayjs } from 'lib/dayjs' import { dateMapping, is12HoursOrLess, isLessThan2Days } from 'lib/utils' import posthog from 'posthog-js' import { databaseTableListLogic } from 'scenes/data-management/database/databaseTableListLogic' +import { dataThemeLogic } from 'scenes/dataThemeLogic' import { insightDataLogic } from 'scenes/insights/insightDataLogic' import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' import { sceneLogic } from 'scenes/sceneLogic' @@ -43,6 +44,7 @@ import { getDisplay, getFormula, getInterval, + getResultCustomizationBy, getSeries, getShowAlertThresholdLines, getShowLabelsOnSeries, @@ -86,6 +88,8 @@ export const insightVizDataLogic = kea([ ['filterTestAccountsDefault'], databaseTableListLogic, ['dataWarehouseTablesMap'], + dataThemeLogic, + ['getTheme'], ], actions: [insightDataLogic, ['setQuery', 'setInsightData', 'loadData', 'loadDataSuccess', 'loadDataFailure']], })), @@ -162,6 +166,11 @@ export const insightVizDataLogic = kea([ return false }, ], + supportsResultCustomizationBy: [ + (s) => [s.isTrends, s.display], + (isTrends, display) => + isTrends && [ChartDisplayType.ActionsLineGraph].includes(display || ChartDisplayType.ActionsLineGraph), + ], dateRange: [(s) => [s.querySource], (q) => (q ? q.dateRange : null)], breakdownFilter: [(s) => [s.querySource], (q) => (q ? getBreakdown(q) : null)], @@ -178,6 +187,7 @@ export const insightVizDataLogic = kea([ showLabelOnSeries: [(s) => [s.querySource], (q) => (q ? getShowLabelsOnSeries(q) : null)], showPercentStackView: [(s) => [s.querySource], (q) => (q ? getShowPercentStackView(q) : null)], yAxisScaleType: [(s) => [s.querySource], (q) => (q ? getYAxisScaleType(q) : null)], + resultCustomizationBy: [(s) => [s.querySource], (q) => (q ? getResultCustomizationBy(q) : null)], vizSpecificOptions: [(s) => [s.query], (q: Node) => (isInsightVizNode(q) ? q.vizSpecificOptions : null)], insightFilter: [(s) => [s.querySource], (q) => (q ? filterForQuery(q) : null)], trendsFilter: [(s) => [s.querySource], (q) => (isTrendsQuery(q) ? q.trendsFilter : null)], @@ -387,6 +397,8 @@ export const insightVizDataLogic = kea([ (s) => [s.querySource, actionsModel.selectors.actions], (querySource, actions) => (querySource ? getAllEventNames(querySource, actions) : []), ], + + theme: [(s) => [s.getTheme, s.querySource], (getTheme, querySource) => getTheme(querySource?.dataColorTheme)], }), listeners(({ actions, values, props }) => ({ diff --git a/frontend/src/scenes/insights/utils.test.ts b/frontend/src/scenes/insights/utils.test.ts index d23a499e5fb0a..87ae768a32226 100644 --- a/frontend/src/scenes/insights/utils.test.ts +++ b/frontend/src/scenes/insights/utils.test.ts @@ -6,11 +6,13 @@ import { formatBreakdownType, getDisplayNameFromEntityFilter, getDisplayNameFromEntityNode, + getTrendDatasetKey, } from 'scenes/insights/utils' +import { IndexedTrendResult } from 'scenes/trends/types' import { ActionsNode, BreakdownFilter, EventsNode, NodeKind } from '~/queries/schema' import { isEventsNode } from '~/queries/utils' -import { Entity, EntityFilter, FilterType, InsightType } from '~/types' +import { CompareLabelType, Entity, EntityFilter, FilterType, InsightType } from '~/types' const createFilter = (id?: Entity['id'], name?: string, custom_name?: string): EntityFilter => { return { @@ -471,3 +473,58 @@ describe('formatBreakdownType()', () => { expect(formatBreakdownType(breakdownFilter)).toEqual('Cohort') }) }) + +describe('getTrendDatasetKey()', () => { + it('handles a simple insight', () => { + const dataset: Partial = { + label: '$pageview', + action: { + id: '$pageview', + type: 'events', + order: 0, + }, + } + + expect(getTrendDatasetKey(dataset as IndexedTrendResult)).toEqual('{"series":0}') + }) + + it('handles insights with breakdowns', () => { + const dataset: Partial = { + label: 'Opera::US', + action: { + id: '$pageview', + type: 'events', + order: 0, + }, + breakdown_value: ['Opera', 'US'], + } + + expect(getTrendDatasetKey(dataset as IndexedTrendResult)).toEqual( + '{"series":0,"breakdown_value":["Opera","US"]}' + ) + }) + + it('handles insights with compare against previous', () => { + const dataset: Partial = { + label: '$pageview', + action: { + id: '$pageview', + type: 'events', + order: 0, + }, + compare: true, + compare_label: CompareLabelType.Current, + } + + expect(getTrendDatasetKey(dataset as IndexedTrendResult)).toEqual('{"series":0,"compare_label":"current"}') + }) + + it('handles insights with formulas', () => { + const dataset: Partial = { + label: 'Formula (A+B)', + action: undefined, + } + + expect(getTrendDatasetKey(dataset as IndexedTrendResult)).toEqual('{"series":"formula"}') + }) +}) diff --git a/frontend/src/scenes/insights/utils.tsx b/frontend/src/scenes/insights/utils.tsx index 04e55519ab05b..c8e95fa6c6fa5 100644 --- a/frontend/src/scenes/insights/utils.tsx +++ b/frontend/src/scenes/insights/utils.tsx @@ -1,9 +1,11 @@ import api from 'lib/api' +import { DataColorTheme, DataColorToken } from 'lib/colors' import { dayjs } from 'lib/dayjs' import { CORE_FILTER_DEFINITIONS_BY_GROUP } from 'lib/taxonomy' import { ensureStringIsNotBlank, humanFriendlyNumber, objectsEqual } from 'lib/utils' import { getCurrentTeamId } from 'lib/utils/getAppContext' import { ReactNode } from 'react' +import { IndexedTrendResult } from 'scenes/trends/types' import { urls } from 'scenes/urls' import { propertyFilterTypeToPropertyDefinitionType } from '~/lib/components/PropertyFilters/utils' @@ -18,6 +20,10 @@ import { Node, NodeKind, PathsFilter, + ResultCustomization, + ResultCustomizationBy, + ResultCustomizationByPosition, + ResultCustomizationByValue, } from '~/queries/schema' import { isDataWarehouseNode, isEventsNode } from '~/queries/utils' import { @@ -29,6 +35,8 @@ import { EntityFilter, EntityTypes, EventType, + FlattenedFunnelStepByBreakdown, + FunnelStepWithConversionMetrics, GroupTypeIndex, InsightShortId, InsightType, @@ -37,6 +45,7 @@ import { PropertyOperator, } from '~/types' +import { RESULT_CUSTOMIZATION_DEFAULT } from './EditorFilters/ResultCustomizationByPicker' import { insightLogic } from './insightLogic' export const isAllEventsEntityFilter = (filter: EntityFilter | ActionFilter | null): boolean => { @@ -435,6 +444,122 @@ export function insightUrlForEvent(event: Pick