chartjs / chartjs-plugin-annotation

Annotation plugin for Chart.js
MIT License
606 stars 328 forks source link

Cannot use plugin inline #931

Closed riderx closed 5 months ago

riderx commented 5 months ago

I had issue with chartJs-vue3 where I have 4 chart display on my app all inited with register in each component. The chart number 4 didn't have any annotation plugin install but was showing error in annotation when mouse over. I realized register do register globally. So I tried to inline, and it was not possible to do with this plugin. My usage of the plugin is super simple, I draw 3 horizontal lines with a label to display "limits" CleanShot 2024-06-06 at 15 12 27@2x I realized I could recreate the plugin just for this purpose here is the code if someone need it:

export interface AnnotationOptions {
  line_?: {
    yMin: number
    yMax: number
    borderColor: string
    borderWidth: number
  }
  label_?: {
    xValue: number
    yValue: number
    backgroundColor: string
    content: string[]
    borderWidth: number
    font: {
      size: number
    }
    color?: string
  }
}

export const inlineAnnotationPlugin = {
  id: 'inlineAnnotationPlugin',
  afterDraw: (chart: any, args: any, options: AnnotationOptions) => {
    const { ctx, chartArea } = chart
    const { left, right } = chartArea

    Object.entries(options).forEach(([key, val]) => {
      if (key.startsWith('line_')) {
        const { yMin, borderColor, borderWidth } = val
        const yScale = chart.scales.y
        const y = yScale.getPixelForValue(yMin)

        ctx.save()
        ctx.beginPath()
        ctx.moveTo(left, y) // Start the line at the left edge of the chart area
        ctx.lineTo(right, y) // End the line at the right edge of the chart area
        ctx.lineTo(chart.width, y)
        ctx.lineWidth = borderWidth
        ctx.strokeStyle = borderColor
        ctx.stroke()
        ctx.restore()
      }

      if (key.startsWith('label_')) {
        const { xValue, yValue, backgroundColor, content, font, color } = val
        const xScale = chart.scales.x
        const yScale = chart.scales.y
        const x = xScale.getPixelForValue(xValue)
        const y = yScale.getPixelForValue(yValue)

        const labelWidth = ctx.measureText(content[0]).width + 10
        const labelHeight = font.size + 6

        ctx.save()
        ctx.fillStyle = backgroundColor
        ctx.fillRect(x - labelWidth / 2, y - labelHeight / 2, labelWidth, labelHeight)
        ctx.restore()

        ctx.save()
        ctx.fillStyle = color || '#000'
        ctx.font = `${font.size}px sans-serif`
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.fillText(content[0], x, y)
        ctx.restore()
      }
    })
  },
}

The results are iso, or even better ( look at the lower one ) CleanShot 2024-06-06 at 15 15 05@2x And can be inlined by passing the plugin like this:

 <Line  :data="chartData" :options="chartOptions" :plugins="[inlineAnnotationPlugin]" />

I init it this way:

function createAnotation(id: string, y: number, title: string, lineColor: string, bgColor: string) {
  const obj: any = {}
  obj[`line_${id}`] = {
    type: 'line',
    yMin: y,
    yMax: y,
    borderColor: lineColor,
    borderWidth: 2,
  }
  obj[`label_${id}`] = {
    type: 'label',
    xValue: getDaysInCurrentMonth() / 2,
    yValue: y,
    backgroundColor: bgColor,
    content: [title],
    font: {
      size: 10,
    },
    color: '#000',
  }
  return obj
}

const generateAnnotations = computed(() => {
  // find biggest value in data
  let annotations: any = {}
  const min = Math.min(...accumulateData.value.filter((val: any) => val !== undefined) as number[])
  const max = Math.max(...projectionData.value.filter((val: any) => val !== undefined) as number[])
  Object.entries(props.limits as { [key: string]: number }).forEach(([key, val], i) => {
    if (val && val > min && val < (max * 1.2)) {
      const color1 = (i + 1) * 100
      const color2 = (i + 2) * 100
      annotations = {
        ...annotations,
        ...createAnotation(key, val, key, props.colors[color1], props.colors[color2]),
      }
    }
  })
  console.log('annotations', annotations)
  return annotations
})
const chartOptions = ref<ChartOptions & { plugins: { inlineAnnotationPlugin: AnnotationOptions } }>({
  maintainAspectRatio: false,
  scales: {
    y: {
      ticks: {
        color: `${isDark.value ? 'white' : 'black'}`,
      },
    },
    x: {
      ticks: {
        color: `${isDark.value ? 'white' : 'black'}`,
      },
    }
    ,
  },
  plugins: {
    inlineAnnotationPlugin: generateAnnotations.value,
    legend: {
      display: false,
    },
    title: {
      display: false,
    },
  },
})
riderx commented 5 months ago

I auto-close as this is to help people with the same issue, I don't ask anything (unless we could have inline in the future <3)