Skip to content
Snippets Groups Projects
BaseTimelineDetailedChart.vue 4.9 KiB
Newer Older
<script setup lang="ts">
import * as d3 from "d3"
import { ref, watch, computed, onMounted } from "vue"
import type { TimelineChartDataPoint } from "@/types"
import { createTooltip, setEventListeners } from "@/helpers/d3/d3-tooltip"
import colors from 'tailwindcss/colors'


interface Props {
  data: TimelineChartDataPoint[],
  maxY?: number,
  width?: number,
  startDate: Date,
  endDate: Date,
  height?: number,
  tooltipContent: (d: TimelineChartDataPoint) => string,
  yAxisTitle?: string
}

const props = defineProps<Props>()

const height = props.height || 60
const marginTop = 10
const marginRight = 10
const marginBottom = 30
const marginLeft = 40
const _width = computed(() => props.width ?? 300)
const _maxY = computed(() => props.maxY ?? 2)

const container = ref<HTMLDivElement>()

function isUp(data: TimelineChartDataPoint[], higherIsUp = true) {

  if (data.length === 0) return 0

  const last = data[data.length - 1].value
  const secondLast = data.length > 1 ? data[data.length - 2].value : last

  const diff = higherIsUp ? last - secondLast : secondLast - last

  if (diff === 0) return 0
  else if (diff > 0) return 1
  else return -1
}

function render([data, startDate, endDate]) {
  if (!data || !startDate || !endDate) return

  if (data.length === 0) return

  // Declare the x (horizontal position) scale.
  const x = d3.scaleTime()
      .domain([startDate, endDate])
      .range([marginLeft, _width.value - marginRight])

// Declare the y (vertical position) scale.
  const y = d3.scaleLinear()
      .domain([0, _maxY.value])
      .range([height - marginBottom, marginTop])

// Create the SVG container.
  const svg = d3.create("svg")
      .attr("width", _width.value - marginRight)
      .attr("height", height)
      .classed('!overflow-visible', true)

// Add the x-axis.
  svg.append("g")
      .classed('x-axis-group', true)
      .attr("transform", `translate(0,${height - marginBottom})`)
      .call(d3.axisBottom(x).ticks(6).tickSize(4).tickFormat(d3.utcFormat("%d.%m.%Y")))

  svg.select('.x-axis-group .domain').attr('stroke', colors.gray['400'])
  svg.selectAll('.x-axis-group .tick text').attr('fill', colors.gray['400'])

// Add the y-axis.
  svg.append("g")
      .classed('y-axis-group', true)
      .attr("transform", `translate(${marginLeft},0)`)
      .call(d3.axisLeft(y).ticks(6).tickSize(0).tickPadding(5))

  svg.append("text")
      .attr("text-anchor", "end")
      .attr("transform", "rotate(-90)")
      .attr("y", marginLeft - 30)
      .attr("x", marginTop - 50)
      .text(props.yAxisTitle ?? '')
      .attr('fill', colors.gray['400'])


  svg.select('.y-axis-group .domain').attr('stroke', colors.gray['400'])
  svg.selectAll('.y-axis-group .tick text').attr('fill', colors.gray['400'])


  const trend = isUp(data, false)

  const pathGroup = svg
      .append('g')
      .classed('path-group', true)
      .classed('up', trend === 1)
      .classed('down', trend === -1)

  pathGroup.append("path")
      .datum(data)
      .attr("stroke-width", 1.5)
      .attr("d", d3.line()
          .x(function(d) { return x(d.date) })
          .y(function(d) { return y(d.value) })
      )

  const pointGroups = pathGroup.selectAll("myCircles")
      .data(data)
      .enter()
      .append("g")

  pointGroups
      .append('circle')
      .attr("r", 10)
      .style('fill', 'transparent')
      .style('cursor', 'pointer')
      .attr("cx", function(d) { return x(d.date) })
      .attr("cy", function(d) { return y(d.value) })
      .classed('chart-point', true)

  pointGroups
      .append("circle")
      .attr("r", 2)
      .classed('pointer-events-none', true)
      .attr("cx", function(d) { return x(d.date) })
      .attr("cy", function(d) { return y(d.value) })

  const oldTooltip = document.querySelector('.d3-tooltip')
  const tooltip = oldTooltip ? d3.select(oldTooltip) : createTooltip(d3.select('body'), {
    classes: 'border border-gray-300 bg-white p-2 rounded-md'
  })

  setEventListeners(svg.selectAll('.chart-point'), tooltip, { useData: props.tooltipContent })

  // Append the SVG element.
  if (!container.value) return
  container.value.replaceChildren()
  container.value.append(svg.node())
}

onMounted(() => {
  render([props.data, props.startDate, props.endDate])
})

watch([() => props.data, () => props.startDate, () => props.endDate], render)


</script>

<template>
  <div ref="container"></div>
</template>

<style lang="scss">


.path-group {
  path {
    fill: none;
    stroke: var(--color--medium-text);
  }

  circle {
    fill: var(--color--medium-text);
  }

  &.up {
    path {
      stroke: var(--color--positive-text);
    }

    circle {
      fill: var(--color--positive-text);
    }
  }

  &.down {
    path {
      stroke: var(--color--negative-text);
    }

    circle {
      fill: var(--color--negative-text);
    }
  }
}

.y-axis-group {
  .tick {
    &:first-of-type {
      text {
        transform: translateY(-3px);
      }
    }
    text {
      @apply text-[9px];
    }
  }
}

</style>