The first problem when receiving the ticks doubles is to round them with the smallest number of digits that make them distinct. This is what the below function ScaleForTicks does. If finds the largest power of 10 that can scale all ticks to integers while keeping them distinct. For ticks >= 0 , scaling means dividing by the power of 10, and for ticks < 1 , it means multiplying by the power of 10. Once the ticks have been scaled to integer, we round them to 0 decimals. This gives us our base labels. They still require additional processing depending on the power of 10 applied.

The question did not say how many consecutive 0's it is acceptable to have in a label. So, I added the maxZeroDigits parameter to the LabelsForTicks function. So, a label will not be displayed with scientific notation, if it contains maxZeroDigits or less consecutive 0's. Otherwise, scientific notation is used.

Another difficulty is what is illustrated by the ticks 20.0000001 20.0000002 20.0000003 in the question. The problem is to extract the common offset of all labels so as to show the actual small variation 1.0e-07 2.0e-07 3.0e-07 . This problem is solved by extracting that common offset from the set of integer labels obtained after scaling. The maxZeroDigits parameter is used to determine whether to format the offset in scientific notation or not.

The question asked for fully formatted labels consisting of an optional offset, a label, and an optional exponent. Because the offset and the exponent are the same for all labels, they can be returned as separate parts. This is what the below LabelsForTicks function does. For n ticks, the first n elements of the returned array are the formatted labels without offset and exponent. The next two elements of the returned array are the label and exponent of the offset. The last element of the returned array is the exponent of the labels. The different parts may be assembled to get fully formatted labels, or they may be used separately, for example to indicate a multiplying factor (x10^2) , or an offset (+1.34e+04) for the labels, along the graph axes.

Here is the code.

static string[] LabelsForTicks(double[] ticks, int maxZeroDigits) { int scale = ScaleForTicks(ticks); string[] labels = new string[ticks.Length + 3]; if (scale >= 0) { if (scale >= maxZeroDigits + 1) { for (int i = 0; i < ticks.Length; i++) labels[i] = ((long)Math.Round(ticks[i] / Math.Pow(10, scale))).ToString(CultureInfo.InvariantCulture); } else { for (int i = 0; i < ticks.Length; i++) labels[i] = ((long)ticks[i]).ToString(CultureInfo.InvariantCulture); } } else { for (int i = 0; i < ticks.Length; i++) labels[i] = ((long)Math.Round(ticks[i] * Math.Pow(10, -scale))).ToString(CultureInfo.InvariantCulture); } // Find common offset. char[] mask = labels[0].ToCharArray(); for (int i = 1; i < ticks.Length; i++) { for (int j = 0; j < labels[0].Length; j++) if (mask[j] != labels[i][j]) mask[j] = 'x'; } int k = mask.Length - 1; while (k >= 0 && mask[k] != 'x') k--; for (; k > 0; k--) { if (!(mask[k] == 'x' || mask[k] != '0')) { k++; break; } } // If there is an offset, and it contains a sequence of more than maxZeroDigits. string common = new string(mask, 0, k); if (common.Contains(new string('0', maxZeroDigits + 1))) { // Remove common offset from all labels. for (int i = 0; i < ticks.Length; i++) labels[i] = labels[i].Substring(k); // Add ofsset as the second-to-last label. labels[ticks.Length] = common + new string('0', labels[0].Length); // Reduce offset. string[] offset = LabelForNumber(Convert.ToDouble(labels[ticks.Length]) * Math.Pow(10, scale), maxZeroDigits); labels[ticks.Length] = offset[0]; labels[ticks.Length + 1] = offset[1]; } if (scale < 0) { int leadingDecimalDigits = (-scale) - labels[0].Length; if (leadingDecimalDigits <= maxZeroDigits) { string zeros = new string('0', leadingDecimalDigits); for (int i = 0; i < ticks.Length; i++) labels[i] = "0." + zeros + labels[i]; scale = 0; } else { // If only one digit, append "0". if (labels[0].Length == 1) { scale -= 1; for (int i = 0; i < ticks.Length; i++) labels[i] = labels[i] + "0"; } // Put decimal point immediately after the first digit. scale += labels[0].Length - 1; for (int i = 0; i < ticks.Length; i++) labels[i] = labels[i][0] + "." + labels[i].Substring(1); } } else if (scale > maxZeroDigits) { // If only one digit, append "0". if (labels[0].Length == 1) { for (int i = 0; i < ticks.Length; i++) labels[i] = labels[i] + "0"; } // Put decimal point immediately after the first digit. scale += labels[0].Length - 1; for (int i = 0; i < ticks.Length; i++) labels[i] = labels[i][0] + "." + labels[i].Substring(1); } // Add exponent as last labels. if (scale < 0 || scale > maxZeroDigits) { string exponent; if (scale < 0) { exponent = (-scale).ToString(); if (exponent.Length == 1) exponent = "0" + exponent; exponent = "-" + exponent; } else { exponent = scale.ToString(); if (exponent.Length == 1) exponent = "0" + exponent; exponent = "+" + exponent; } labels[ticks.Length + 2] = "e" + exponent; } return labels; } static int ScaleForTicks(double[] ticks) { int scale = -1 + (int)Math.Ceiling(Math.Log10(ticks.Last())); int bound = Math.Max(scale - 15, 0); while (scale >= bound) { double t1 = Math.Round(ticks[0] / Math.Pow(10, scale)); bool success = true; for (int i = 1; i < ticks.Length; i++) { double t2 = Math.Round(ticks[i] / Math.Pow(10, scale)); if (t1 == t2) { success = false; break; } t1 = t2; } if (success) return scale; scale--; } bound = Math.Min(-1, scale - 15); while (scale >= bound) { double t1 = Math.Round(ticks[0] * Math.Pow(10, -scale)); bool success = true; for (int i = 1; i < ticks.Length; i++) { double t2 = Math.Round(ticks[i] * Math.Pow(10, -scale)); if (t1 == t2) { success = false; break; } t1 = t2; } if (success) return scale; scale--; } return scale; } static string[] LabelForNumber(double number, int maxZeroDigits) { int scale = ScaleNumber(number); string[] labels = new string[2]; if (scale >= 0) { if (scale >= maxZeroDigits + 1) labels[0] = ((long)Math.Round(number / Math.Pow(10, scale))).ToString(CultureInfo.InvariantCulture); else labels[0] = ((long)number).ToString(CultureInfo.InvariantCulture); } else { labels[0] = ((long)Math.Round(number * Math.Pow(10, -scale))).ToString(CultureInfo.InvariantCulture); } if (scale < 0) { int leadingDecimalDigits = (-scale) - labels[0].Length; if (leadingDecimalDigits <= maxZeroDigits) { string zeros = new string('0', leadingDecimalDigits); labels[0] = "0." + zeros + labels[0].TrimEnd(new char[] { '0' }); scale = 0; } else { // Put decimal point immediately after the first digit. scale += labels[0].Length - 1; labels[0] = labels[0][0] + "." + labels[0].Substring(1); labels[0] = labels[0].TrimEnd(new char[] { '0' }); // If only one digit, append "0". if (labels[0].Length == 2) labels[0] = labels[0] + "0"; } } else if (scale > maxZeroDigits) { // Put decimal point immediately after the first digit. scale -= labels[0].Length - 1; labels[0] = labels[0][0] + "." + labels[0].Substring(1); labels[0] = labels[0].TrimEnd(new char[] { '0' }); // If only one digit, append "0". if (labels[0].Length == 2) labels[0] = labels[0] + "0"; } // Add exponent as last labels. if (scale < 0 || scale > maxZeroDigits) { string exponent; if (scale < 0) { exponent = (-scale).ToString(); if (exponent.Length == 1) exponent = "0" + exponent; exponent = "-" + exponent; } else { exponent = scale.ToString(); if (exponent.Length == 1) exponent = "0" + exponent; exponent = "+" + exponent; } labels[1] = "e" + exponent; } return labels; } static int ScaleNumber(double number) { int scale = (int)Math.Ceiling(Math.Log10(number)); int bound = Math.Max(scale - 15, 0); while (scale >= bound) { if (Math.Round(number / Math.Pow(10, scale)) == number / Math.Pow(10, scale)) return scale; scale--; } bound = Math.Min(-1, scale - 15); while (scale >= bound) { if (Math.Round(number * Math.Pow(10, -scale)) == number * Math.Pow(10, -scale)) return scale; scale--; } return scale; }

Here are several examples with maxZeroDigits set to 3 and 2.