techniq / layerchart

Composable Svelte chart components to build a wide range of visualizations
https://www.layerchart.com
MIT License
668 stars 12 forks source link

Simplified Chart components #223

Closed techniq closed 2 months ago

techniq commented 4 months ago

This idea has been in my head for some time, but I figure it's time to formally write it down, as it would simplify the "basic" use cases (especially for exploration or quick visualizations) and enable additional use cases similar to the newly released shadcn/ui Charts. While I was initially adverse to the idea, I think creating BarChart, AreaChart, LineChart, etc will provide a lot of value, including:

The general idea is

<script>
  import { BarChart, ... } from 'layerchart';
</script>

<BarChart {data} x="date" y="value">
  ...
</BarChart>

is a streamlined instance of

<script>
  import { Chart, ... } from 'layerchart';
  import { scaleBand, scaleLinear } from 'd3-scale';
</script>

<Chart
  {data}
  x="date"
  xScale={scaleBand()}
  y="value"
  yScale={scaleLinear()}
  yDomain={[0, null]}
  padding={{ left: 16, bottom: 24 }}
  tooltip={{ mode: 'band' }}
>
  ...
</Chart>

Possibly also provide a default slot impl, so

<script>
  import { BarChart } from 'layerchart';
</script>

<BarChart {data} x="date" y="value" />

is a streamlined instance of

<script>
  import { Chart, Svg, Axis, Bars, Highlight, Tooltip, TooltipItem } from 'layerchart';
  import { scaleBand, scaleLinear } from 'd3-scale';
</script>

<Chart
  {data}
  x="date"
  xScale={scaleBand()}
  y="value"
  yScale={scaleLinear()}
  yDomain={[0, null]}
  padding={{ left: 16, bottom: 24 }}
  tooltip={{ mode: 'band' }}
>
  <Svg>
    <Axis placement="left" grid rule />
    <Axis placement="bottom" rule />
    <Bars radius={4} strokeWidth={1} class="fill-primary" />
    <Highlight area />
  </Svg>
  <Tooltip header={(data) => format(data.date, "eee, MMMM do")} let:data>
    <TooltipItem label="value" value={data.value} />
  </Tooltip>
</Chart>

Items to consider

Initial charts to support

Related: #68

mhkeller commented 3 months ago

Really neat to see this idea coming together. It's been on my mind, too, over the years. One idea I was kicking around was a way to have both a simplified API and allow for ad hoc customization as discussed here https://github.com/mhkeller/layercake/discussions/174. I haven't done a proof of concept with the snippets API but it could be a neat way to swap out the default components with your own once your charts gets more customized.

techniq commented 3 months ago

Really neat to see this idea coming together. It's been on my mind, too, over the years. One idea I was kicking around was a way to have both a simplified API and allow for ad hoc customization as discussed here mhkeller/layercake#174. I haven't done a proof of concept with the snippets API but it could be a neat way to swap out the default components with your own once your charts gets more customized.

@mhkeller That's the goal with these simplified charts 😁 - Provide a great default/initial experience for common use cases, but also provide various extension points as you need to customize:

Basically get you setup quickly, but get out of your way.

look-into-my-eyes-thor-gif

These simplified charts also provide better integration between components (ex. series prop which adds the additional marks, tooltip items with colors, etc)

Some of the extension/overrides available thus far:

Chart props

Pass any prop to the simplified chart instance that passes down to the underlying <Chart> instance

<AreaChart padding={{ left: 24, bottom: 16 }} ... />

Inner component props (example)

Provide additional props (or overrides) on the inner components without requiring a full component overrides.

<BarChart
  props={{
    axisLeft: { rule: false },
    axisBottom: { format: (value) => format(Math.abs(value), "metric") },
}}>

Named slots

More granular overrides via named slots (such as marks, axis, tooltip, etc)

<AreaChart data={dateSeriesData} x="date" y="value">
  <svelte:fragment slot="marks">
    <LinearGradient class="from-primary/50 to-primary/0" vertical let:url>
      <Area line={{ class: "stroke-2 stroke-primary" }} fill={url} />
    </LinearGradient>
  </svelte:fragment>
</AreaChart>
<ScatterChart {data} x="x" y="y">
  <svelte:fragment slot="tooltip" let:x let:y let:padding let:height>
    <Tooltip.Root
      x={padding.left}
      y="data"
      anchor="right"
      contained={false}
      class="text-[10px] font-semibold text-primary bg-surface-100 mr-[2px] px-1 py-[2px] border border-primary rounded whitespace-nowrap"
      let:data
    >
      {format(y(data), "integer")}
    </Tooltip.Root>

    <Tooltip.Root
      x="data"
      y={height}
      anchor="top"
      class="text-[10px] font-semibold text-primary bg-surface-100 mt-[1px] px-2 py-[1px] border border-primary rounded whitespace-nowrap"
      contained={false}
      let:data
    >
      {format(x(data), "integer")}
    </Tooltip.Root>
  </svelte:fragment>
</ScatterChart>

Opt into some features (labels, etc) (example)

<BarChart data={dateSeriesData} x="date" y="value" labels />

Full custom chart

Customize all components used, but still include all the benefits of default chart type instance (domains, tooltip mode, series support, etc) and simplified imports (ex. no d3-scale, for example)

<AreaChart data={dateSeriesData} x="date" y="value" let:x let:y>
  <Svg>
    <Axis placement="left" grid rule />
    <Axis
      placement="bottom"
      format={(d) => format(d, PeriodType.Day, { variant: "short" })}
      rule
    />
    <Area
      line={{ class: "stroke-2 stroke-primary" }}
      class="fill-primary/30"
    />
    <Highlight points lines />
  </Svg>

  <Tooltip.Root let:data>
    <Tooltip.Header>{format(x(data), PeriodType.DayTime)}</Tooltip.Header>
    <Tooltip.List>
      <Tooltip.Item label="value" value={y(data)} />
    </Tooltip.List>
  </Tooltip.Root>
</AreaChart>

I'm still feeling this out, but take a look at AreaChart, BarChart, LineChart, PieChart, and ScatterChart for examples of many of these concepts.

I'm currently working on overhauling how stacks (BarChart, AreaChart) and groups (BarChart) are implemented (eyeing derived scales PR for groups 😁). Stacks along with a few additional use cases are likely what will be included in the first release, and will consider adding more chart types (hierarchy, force, geo, etc) over time.