Source

114 chart lines · 3 files · 3.2 kBBenchmark harness excluded · shared/mount.ts

Likert survey responses500 records · CSV · 8.2 kB
@charts-poc/demo-data/survey

Selection: Complete published snapshot

Question
string
ID
number
Response
string

Eitan Leesobservablehq/plot@356f579b1d94 · revision 356f579b1d94 · test/data/survey.csv · Observable Plot repository MIT; upstream source credited · SHA-256 de6879d71248Pinned snapshot

cases/26-diverging-likert/tanstack.ts47 lines · entry
cases/26-diverging-likert/tanstack.ts
import { barX, colorLegend, defineChart, ruleX } from '@tanstack/charts'
import { survey } from '@charts-poc/demo-data/survey'
import { scaleBand, scaleLinear } from 'd3-scale'
import { likertResponses, selectLikertSurvey } from './selection'
import { likertSegments } from './transform'
import { tanstackMount } from '../../shared/mount'

const colors = ['#991b1b', '#ef4444', '#cbd5e1', '#60a5fa', '#1d4ed8']

const likertSurvey = selectLikertSurvey(survey)
const segments = likertSegments(likertSurvey)

const definition = () =>
  defineChart({
    marks: [
      barX(segments, {
        x1: 'x1',
        x2: 'x2',
        y: 'Question',
        color: 'Response',
        key: (row) => `${row.Question}:${row.Response}`,
        inset: 0.75,
      }),
      ruleX([0], { stroke: '#64748b' }),
    ],
    x: {
      scale: scaleLinear,
      grid: true,
      axis: {
        ticks: { format: (value) => `${Math.abs(value)}` },
        label: '← more disagree · Number of responses · more agree →',
      },
    },
    y: {
      scale: () => scaleBand<string>().paddingInner(0.14).paddingOuter(0.08),
    },
    color: {
      domain: likertResponses,
      range: colors,
      legend: colorLegend({ label: 'Response' }),
    },
  })

export const mount = tanstackMount(
  definition,
  'Diverging Likert survey responses',
)
cases/26-diverging-likert/selection.ts30 lines · support
cases/26-diverging-likert/selection.ts
import type { SurveyRow } from '@charts-poc/demo-data/survey'

export type LikertResponse =
  'Strongly Disagree' | 'Disagree' | 'Neutral' | 'Agree' | 'Strongly Agree'

export interface LikertSurveyRow extends SurveyRow {
  readonly Response: LikertResponse
}

export const likertResponses: readonly LikertResponse[] = [
  'Strongly Disagree',
  'Disagree',
  'Neutral',
  'Agree',
  'Strongly Agree',
]

const responseSet = new Set<string>(likertResponses)

export function selectLikertSurvey(
  rows: readonly SurveyRow[],
): readonly LikertSurveyRow[] {
  return rows.filter((row): row is LikertSurveyRow =>
    responseSet.has(row.Response),
  )
}

export function likertQuestions(rows: readonly LikertSurveyRow[]) {
  return [...new Set(rows.map((row) => row.Question))]
}
cases/26-diverging-likert/transform.ts37 lines · support
cases/26-diverging-likert/transform.ts
import { group } from 'd3-array'
import { likertResponses } from './selection'
import type { LikertResponse, LikertSurveyRow } from './selection'

export interface LikertSegment {
  readonly Question: string
  readonly Response: LikertResponse
  readonly count: number
  readonly x1: number
  readonly x2: number
}

export function likertSegments(
  rows: readonly LikertSurveyRow[],
): readonly LikertSegment[] {
  return [...group(rows, (row) => row.Question)].flatMap(
    ([Question, questionRows]) => {
      const counts = new Map<LikertResponse, number>(
        likertResponses.map((Response) => [
          Response,
          questionRows.filter((row) => row.Response === Response).length,
        ]),
      )
      let cursor =
        -(counts.get('Strongly Disagree') ?? 0) -
        (counts.get('Disagree') ?? 0) -
        (counts.get('Neutral') ?? 0) / 2

      return likertResponses.map((Response) => {
        const count = counts.get(Response) ?? 0
        const x1 = cursor
        cursor += count
        return { Question, Response, count, x1, x2: cursor }
      })
    },
  )
}