Skip to content

Commit

Permalink
feat(bi): First iteration of BI tooling (#18995)
Browse files Browse the repository at this point in the history
* First pass of BI and DataVisualizationNode

* First pass of a BI interface

* PR comments

* Fix types

* Added data viz node to metadata
  • Loading branch information
Gilbert09 authored Dec 1, 2023
1 parent b5b8db6 commit ffeddd9
Show file tree
Hide file tree
Showing 24 changed files with 683 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ export const commandPaletteLogic = kea<commandPaletteLogicType>([
synonyms: ['hogql', 'sql'],
executor: () => {
// TODO: Don't reset insight on change
push(insightTypeURL[InsightType.SQL])
push(insightTypeURL(Boolean(values.featureFlags[FEATURE_FLAGS.BI_VIZ]))[InsightType.SQL])
},
},
{
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const FEATURE_FLAGS = {
HOGQL_INSIGHTS_LIFECYCLE: 'hogql-insights-lifecycle', // owner: @mariusandra
HOGQL_INSIGHTS_TRENDS: 'hogql-insights-trends', // owner: @Gilbert09
HOGQL_INSIGHT_LIVE_COMPARE: 'hogql-insight-live-compare', // owner: @mariusandra
BI_VIZ: 'bi_viz', // owner: @Gilbert09
WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline
SURVEYS_RESULTS_VISUALIZATIONS: 'surveys-results-visualizations', // owner: @jurajmajerik
SURVEYS_PAYGATES: 'surveys-paygates',
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/queries/Query/Query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import { QueryEditor } from '~/queries/QueryEditor/QueryEditor'
import { AnyResponseType, Node, QuerySchema } from '~/queries/schema'
import { QueryContext } from '~/queries/types'

import { DataTableVisualization } from '../nodes/DataVisualization/DataVisualization'
import { SavedInsight } from '../nodes/SavedInsight/SavedInsight'
import { TimeToSeeData } from '../nodes/TimeToSeeData/TimeToSeeData'
import {
isDataNode,
isDataTableNode,
isDataVisualizationNode,
isInsightVizNode,
isSavedInsightNode,
isTimeToSeeDataSessionsNode,
Expand Down Expand Up @@ -76,6 +78,16 @@ export function Query(props: QueryProps): JSX.Element | null {
uniqueKey={props.uniqueKey}
/>
)
} else if (isDataVisualizationNode(query)) {
component = (
<DataTableVisualization
query={query}
setQuery={setQuery}
cachedResults={props.cachedResults}
uniqueKey={props.uniqueKey}
context={queryContext}
/>
)
} else if (isDataNode(query)) {
component = <DataNode query={query} cachedResults={props.cachedResults} />
} else if (isSavedInsightNode(query)) {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/queries/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { defaultDataTableColumns } from '~/queries/nodes/DataTable/utils'
import {
ActionsNode,
DataTableNode,
DataVisualizationNode,
EventsNode,
EventsQuery,
FunnelsQuery,
Expand Down Expand Up @@ -322,6 +323,11 @@ const HogQLTable: DataTableNode = {
source: HogQLRaw,
}

const DataVisualization: DataVisualizationNode = {
kind: NodeKind.DataVisualizationNode,
source: HogQLRaw,
}

/* a subset of examples including only those we can show all users and that don't use HogQL */
export const queryExamples: Record<string, Node> = {
Events,
Expand Down Expand Up @@ -353,6 +359,7 @@ export const examples: Record<string, Node> = {
TimeToSeeDataJSON,
HogQLRaw,
HogQLTable,
DataVisualization,
}

export const stringifiedExamples: Record<string, string> = Object.fromEntries(
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/queries/nodes/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const personGroupTypes = [TaxonomicFilterGroupType.HogQLExpression, TaxonomicFil
let uniqueNode = 0

export function DataTable({ uniqueKey, query, setQuery, context, cachedResults }: DataTableProps): JSX.Element {
const uniqueNodeKey = useState(() => uniqueNode++)
const [uniqueNodeKey] = useState(() => uniqueNode++)
const [dataKey] = useState(() => `DataNode.${uniqueKey || uniqueNodeKey}`)
const [vizKey] = useState(() => `DataTable.${uniqueNodeKey}`)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DataVisualization {
--viz-min-height: calc(80vh - 6rem);
}
24 changes: 24 additions & 0 deletions frontend/src/queries/nodes/DataVisualization/Components/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import './Chart.scss'

import { useValues } from 'kea'

import { dataVisualizationLogic } from '../dataVisualizationLogic'
import { LineGraph } from './Charts/LineGraph'
import { ChartSelection } from './ChartSelection'

export const Chart = (): JSX.Element => {
const { showEditingUI } = useValues(dataVisualizationLogic)

return (
<div className="flex flex-row gap-4">
{showEditingUI && (
<div className="h-full">
<ChartSelection />
</div>
)}
<div className="w-full">
<LineGraph />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '../../../../styles/mixins';

.DataVisualization {
.ChartSelectionWrapper {
width: 20vw;
height: var(--viz-min-height);
border-radius: var(--radius);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import './ChartSelection.scss'

import { LemonLabel, LemonSelect } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'

import { dataNodeLogic } from '../../DataNode/dataNodeLogic'
import { dataVisualizationLogic } from '../dataVisualizationLogic'

export const ChartSelection = (): JSX.Element => {
const { columns, selectedXIndex, selectedYIndex } = useValues(dataVisualizationLogic)
const { responseLoading } = useValues(dataNodeLogic)
const { setXAxis, setYAxis } = useActions(dataVisualizationLogic)

const options = columns.map(({ name, type }) => ({
value: name,
label: `${name} - ${type}`,
}))

return (
<div className="ChartSelectionWrapper bg-bg-light border p-4">
<div className="flex flex-col">
<LemonLabel>X-axis</LemonLabel>
<LemonSelect
value={selectedXIndex !== null ? options[selectedXIndex]?.label : 'None'}
options={options}
disabledReason={responseLoading ? 'Query loading...' : undefined}
onChange={(value) => {
const index = options.findIndex((n) => n.value === value)
setXAxis(index)
}}
/>
<LemonLabel className="mt-4">Y-axis</LemonLabel>
<LemonSelect
value={selectedYIndex !== null ? options[selectedYIndex]?.label : 'None'}
options={options}
disabledReason={responseLoading ? 'Query loading...' : undefined}
onChange={(value) => {
const index = options.findIndex((n) => n.value === value)
setYAxis(index)
}}
/>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DataVisualization__LineGraph {
min-height: var(--viz-min-height);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import 'chartjs-adapter-dayjs-3'
import './LineGraph.scss'

import { ChartData, Color, GridLineOptions, TickOptions } from 'chart.js'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import { useMountedLogic, useValues } from 'kea'
import { Chart, ChartItem, ChartOptions } from 'lib/Chart'
import { getGraphColors } from 'lib/colors'
import { useEffect, useRef } from 'react'

import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { GraphType } from '~/types'

import { dataVisualizationLogic } from '../../dataVisualizationLogic'

export const LineGraph = (): JSX.Element => {
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const { isDarkModeOn } = useValues(themeLogic)
const colors = getGraphColors(isDarkModeOn)

const vizLogic = useMountedLogic(dataVisualizationLogic)
const { xData, yData } = useValues(vizLogic)

useEffect(() => {
if (!xData || !yData) {
return
}

const data: ChartData = {
labels: xData,
datasets: [
{
label: 'Dataset 1',
data: yData,
borderColor: 'red',
},
],
}

const tickOptions: Partial<TickOptions> = {
color: colors.axisLabel as Color,
font: {
family: '-apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
size: 12,
weight: '500',
},
}

const gridOptions: Partial<GridLineOptions> = {
color: colors.axisLine as Color,
borderColor: colors.axisLine as Color,
tickColor: colors.axisLine as Color,
borderDash: [4, 2],
}

const options: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
elements: {
line: {
tension: 0,
},
},
plugins: {
datalabels: {
color: 'white',
anchor: (context) => {
const datum = context.dataset.data[context.dataIndex]
return typeof datum !== 'number' ? 'end' : datum > 0 ? 'end' : 'start'
},
backgroundColor: (context) => {
return (context.dataset.borderColor as string) || 'black'
},
display: () => {
return true
},
borderWidth: 2,
borderRadius: 4,
borderColor: 'white',
},
legend: {
display: false,
},
// @ts-expect-error Types of library are out of date
crosshair: {
snap: {
enabled: true, // Snap crosshair to data points
},
sync: {
enabled: false, // Sync crosshairs across multiple Chartjs instances
},
zoom: {
enabled: false, // Allow drag to zoom
},
line: {
color: colors.crosshair ?? undefined,
width: 1,
},
},
},
hover: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
scales: {
x: {
display: true,
beginAtZero: true,
ticks: tickOptions,
grid: {
...gridOptions,
drawOnChartArea: false,
tickLength: 12,
},
},
y: {
display: true,
beginAtZero: true,
stacked: false,
ticks: {
display: true,
...tickOptions,
precision: 1,
},
grid: gridOptions,
},
},
}

const newChart = new Chart(canvasRef.current?.getContext('2d') as ChartItem, {
type: GraphType.Line,
data,
options,
plugins: [ChartDataLabels],
})
return () => newChart.destroy()
}, [xData, yData])

return (
<div className="DataVisualization__LineGraph rounded bg-bg-light relative flex flex-col p-2">
<div className="flex w-full h-full overflow-hidden">
<canvas ref={canvasRef} />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { LemonSelect, LemonSelectOptions } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { IconShowChart, IconTableChart } from 'lib/lemon-ui/icons'

import { ChartDisplayType } from '~/types'

import { dataVisualizationLogic } from '../dataVisualizationLogic'

export const TableDisplay = (): JSX.Element => {
const { setVisualizationType } = useActions(dataVisualizationLogic)
const { visualizationType } = useValues(dataVisualizationLogic)

const options: LemonSelectOptions<ChartDisplayType> = [
{
options: [
{
value: ChartDisplayType.ActionsTable,
icon: <IconTableChart />,
label: 'Table',
},
{
value: ChartDisplayType.ActionsLineGraph,
icon: <IconShowChart />,
label: 'Line chart',
},
],
},
]

return (
<LemonSelect
value={visualizationType}
onChange={(value) => {
setVisualizationType(value)
}}
dropdownPlacement="bottom-end"
optionTooltipPlacement="left"
dropdownMatchSelectWidth={false}
data-attr="chart-filter"
options={options}
size="small"
/>
)
}
Loading

0 comments on commit ffeddd9

Please sign in to comment.